Implementation of Docker

This commit is contained in:
Adam Štrauch 2020-07-10 00:27:23 +02:00
parent 837f629d8d
commit 064d6e6487
Signed by: cx
GPG Key ID: 018304FFA8988F8D
9 changed files with 416 additions and 15 deletions

View File

@ -37,7 +37,7 @@ func List() (*[]App, error) {
} }
// New creates new record about application in the database // New creates new record about application in the database
func New(name string, SSHPort int, HTTPPort int, image string, CPU string, memory int) error { func New(name string, SSHPort int, HTTPPort int, image string, CPU int, memory int) error {
app := App{ app := App{
Name: name, Name: name,
SSHPort: SSHPort, SSHPort: SSHPort,
@ -49,6 +49,13 @@ func New(name string, SSHPort int, HTTPPort int, image string, CPU string, memor
db := common.GetDBConnection() db := common.GetDBConnection()
validationErrors := app.Validate()
if len(validationErrors) != 0 {
return ValidationError{
Errors: validationErrors,
}
}
if err := db.Create(app).Error; err != nil { if err := db.Create(app).Error; err != nil {
return err return err
} }
@ -57,7 +64,7 @@ func New(name string, SSHPort int, HTTPPort int, image string, CPU string, memor
} }
// Update changes value about app in the database // Update changes value about app in the database
func Update(name string, SSHPort int, HTTPPort int, image string, CPU string, memory int) error { func Update(name string, SSHPort int, HTTPPort int, image string, CPU int, memory int) error {
var app App var app App
db := common.GetDBConnection() db := common.GetDBConnection()
@ -67,13 +74,20 @@ func Update(name string, SSHPort int, HTTPPort int, image string, CPU string, me
return err return err
} }
err = db.Model(&app).Updates(App{ app.SSHPort = SSHPort
SSHPort: SSHPort, app.HTTPPort = HTTPPort
HTTPPort: HTTPPort, app.Image = image
Image: image, app.CPU = CPU
CPU: CPU, app.Memory = memory
Memory: memory,
}).Error validationErrors := app.Validate()
if len(validationErrors) != 0 {
return ValidationError{
Errors: validationErrors,
}
}
err = db.Update(&app).Error
return err return err
} }

View File

@ -1,6 +1,20 @@
package apps package apps
import "github.com/jinzhu/gorm" import (
"regexp"
"strings"
"github.com/jinzhu/gorm"
)
// ValidationError is error that holds multiple validation error messages
type ValidationError struct {
Errors []string
}
func (v ValidationError) Error() string {
return strings.Join(v.Errors, "\n")
}
// Label holds metadata about the application // Label holds metadata about the application
type Label struct { type Label struct {
@ -15,12 +29,44 @@ type App struct {
SSHPort int `json:"ssh_port"` SSHPort int `json:"ssh_port"`
HTTPPort int `json:"http_port"` HTTPPort int `json:"http_port"`
Image string `json:"image"` Image string `json:"image"`
Status string `json:"status"` // running, data (no container, data only), stopped CPU int `json:"cpu"` // percentage, 200 means two CPU
CPU string `json:"cpu"`
Memory int `json:"memory"` // Limit in MB Memory int `json:"memory"` // Limit in MB
Labels []Label `json:"labels"` // username:cx or user_id:1 Labels []Label `json:"labels"` // username:cx or user_id:1
Status string `json:"status"` // running, data-only (no container, data only), stopped
MemoryUsage int `json:"memory_usage"` // Usage in MB MemoryUsage int `json:"memory_usage"` // Usage in MB
DiskUsage int `json:"disk_usage"` // Usage in MB DiskUsage int `json:"disk_usage"` // Usage in MB
} }
// Validate do basic checks of the struct values
func (a *App) Validate() []string {
var errors []string
nameRegExp := regexp.MustCompile(`^[a-z0-9_]{3,48}$`)
if !nameRegExp.MatchString(a.Name) {
errors = append(errors, "name can contain only characters, numbers and underscores and has to be between 3 and 48 characters")
}
if a.SSHPort < 0 && a.SSHPort > 65536 {
errors = append(errors, "SSH port has to be between 0 and 65536, where 0 means disabled")
}
if a.HTTPPort < 1 && a.HTTPPort > 65536 {
errors = append(errors, "HTTP port has to be between 1 and 65536")
}
if a.Image == "" {
errors = append(errors, "image cannot be empty")
}
if a.CPU < 10 && a.CPU > 800 {
errors = append(errors, "CPU value has be between 10 and 800")
}
if a.Memory < 32 && a.Memory > 16384 {
errors = append(errors, "Memory value has be between 32 and 16384")
}
return errors
}

222
docker/docker.go Normal file
View File

@ -0,0 +1,222 @@
package docker
import (
"context"
"errors"
"log"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
dockerClient "github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
)
// Docker timeout
const dockerTimeout = 10
// DOCKER_SOCK tells where to connect to docker, it will be always local sock
const dockerSock = "unix:///var/run/docker.sock"
// DOCKER_API_VERSION set API version of Docker, 1.40 belongs to Docker 19.03.11
const dockerAPIVersion = "1.40"
// Driver keeps everything for connection to Docker
type Driver struct{}
func (d *Driver) getClient() (*dockerClient.Client, error) {
cli, err := dockerClient.NewClient(dockerSock, dockerAPIVersion, nil, nil)
return cli, err
}
// ConnectionStatus checks connection to the Docker daemon
func (d *Driver) ConnectionStatus() (bool, error) {
cli, err := d.getClient()
if err != nil {
return false, err
}
_, err = cli.ServerVersion(context.TODO())
if err != nil {
return false, err
}
return true, nil
}
func (d *Driver) nameToID(name string) (string, error) {
containerIDs, err := d.IsExist(name)
if err != nil {
return "", err
}
if len(containerIDs) == 0 {
return "", errors.New("no container found")
}
if len(containerIDs) > 1 {
return "", errors.New("multiple containers with the same name")
}
return containerIDs[0], nil
}
// Remove removes container represented by containerID
func (d *Driver) Remove(name string) error {
log.Println("Removing container " + name)
cli, err := d.getClient()
if err != nil {
return err
}
containerID, err := d.nameToID(name)
if err != nil {
return err
}
timeout := time.Duration(dockerTimeout * time.Second)
err = cli.ContainerStop(context.TODO(), containerID, &timeout)
if err != nil {
return err
}
err = cli.ContainerRemove(context.TODO(), containerID, types.ContainerRemoveOptions{})
return err
}
// Start starts container represented by containerID
func (d *Driver) Start(name string) error {
log.Println("Starting container " + name)
cli, err := d.getClient()
if err != nil {
return err
}
containerID, err := d.nameToID(name)
if err != nil {
return err
}
err = cli.ContainerStart(context.TODO(), containerID, types.ContainerStartOptions{})
return err
}
// Stop stops container represented by containerID
func (d *Driver) Stop(name string) error {
log.Println("Stopping container " + name)
cli, err := d.getClient()
if err != nil {
return err
}
containerID, err := d.nameToID(name)
if err != nil {
return err
}
timeout := time.Duration(dockerTimeout * time.Second)
err = cli.ContainerStop(context.TODO(), containerID, &timeout)
return err
}
// IsExist checks existence of the container based on container name
// Returns container IDs in case of existence. Otherwise
// empty slice.
func (d *Driver) IsExist(name string) ([]string, error) {
var containerIDs = make([]string, 0)
cli, err := d.getClient()
if err != nil {
return containerIDs, err
}
containers, err := cli.ContainerList(context.TODO(), types.ContainerListOptions{})
if err != nil {
return containerIDs, err
}
// We go through the containers and pick the ones which match the task name
for _, containerObject := range containers {
for _, name := range containerObject.Names {
name = strings.Trim(name, "/")
if strings.Split(name, ".")[0] == name {
containerIDs = append(containerIDs, containerObject.ID)
}
}
}
return containerIDs, nil
}
// Create creates the container
// image - docker image
// cmd - string slice of command and its arguments
// volumePath - host's directory to mount into the container
// returns container ID
func (d *Driver) Create(name string, image string, volumePath string, HTTPPort int, SSHPort int, CPU int, memory int, cmd []string) (string, error) {
log.Println("Creating container " + name)
cli, err := d.getClient()
if err != nil {
return "", err
}
portmaps := make(nat.PortMap, 1)
portbindingsHTTP := make([]nat.PortBinding, 1)
portbindingsHTTP[0] = nat.PortBinding{
HostPort: strconv.Itoa(HTTPPort) + "/tcp",
}
portmaps["8000/tcp"] = portbindingsHTTP
if SSHPort != 0 {
portbindingsSSH := make([]nat.PortBinding, 1)
portbindingsSSH[0] = nat.PortBinding{
HostPort: strconv.Itoa(SSHPort) + "/tcp",
}
portmaps["22/tcp"] = portbindingsSSH
}
createdContainer, err := cli.ContainerCreate(
context.TODO(),
&container.Config{
Hostname: name,
Env: []string{},
Image: image,
Cmd: cmd,
},
&container.HostConfig{
Resources: container.Resources{
CPUPeriod: 100,
CPUQuota: int64(CPU),
Memory: int64(memory*110/100)*1024 ^ 2, // Allow 10 % more memory so we have space for MemoryReservation
MemoryReservation: int64(memory)*1024 ^ 2, // This should provide softer way how to limit the memory of our containers
},
PortBindings: portmaps,
AutoRemove: false,
RestartPolicy: container.RestartPolicy{
Name: "unless-stopped",
MaximumRetryCount: 3,
},
Binds: []string{
volumePath + ":/srv",
},
},
&network.NetworkingConfig{},
name,
)
if err != nil {
return "", err
}
containerID := createdContainer.ID
// I dunno if we want this
// err = cli.ContainerStart(context.TODO(), createdContainer.ID, types.ContainerStartOptions{})
return containerID, nil
}

50
docker/du.go Normal file
View File

@ -0,0 +1,50 @@
package docker
import (
"bytes"
"os/exec"
"strconv"
"strings"
)
// Return bytes, inodes occupied by a directory and/or error if there is any
func du(path string) (int, int, error) {
space, inodes := 0, 0
// Occupied space
var out bytes.Buffer
command := exec.Command("/usr/bin/du -m -s " + path)
command.Stdout = &out
err := command.Run()
if err != nil {
return space, inodes, err
}
fields := strings.Fields(strings.TrimSpace(out.String()))
out.Reset()
if len(fields) == 2 {
space, err = strconv.Atoi(fields[0])
if err != nil {
return space, inodes, err
}
}
// Occupied inodes
command = exec.Command("/usr/bin/du --inodes -s " + path)
command.Stdout = &out
err = command.Run()
if err != nil {
return space, inodes, err
}
fields = strings.Fields(strings.TrimSpace(out.String()))
out.Reset()
if len(fields) == 2 {
space, err = strconv.Atoi(fields[0])
if err != nil {
return inodes, inodes, err
}
}
return space, inodes, nil
}

1
docker/main.go Normal file
View File

@ -0,0 +1 @@
package docker

58
docker/types.go Normal file
View File

@ -0,0 +1,58 @@
package docker
import (
"path"
"github.com/rosti-cz/apps-api/apps"
)
// Container extends App struct from App
type Container struct {
App apps.App
}
// volumeHostPath each container has one volume mounted into it,
func (c *Container) volumeHostPath() string {
return path.Join("/srv", c.App.Name)
}
// Status returns state of the container
// Possible values: running, stopped, data-only
func (c *Container) Status() (string, error) {
return "", nil
}
// DiskSpace returns number of MB and inodes used by the container in it's mounted volume
func (c *Container) DiskSpace() (int, int, error) {
return du(c.volumeHostPath())
}
// Create creates the container
func (c *Container) Create() error {
return nil
}
// Start starts the container
func (c *Container) Start() error {
return nil
}
// Stop stops the container
func (c *Container) Stop() error {
return nil
}
// Reset restarts the container
func (c *Container) Reset() error {
return nil
}
// Destroy removes the container but keeps the data so it can be created again
func (c *Container) Destroy() error {
return nil
}
// Delete removes both data and the container
func (c *Container) Delete() error {
return nil
}

2
go.mod
View File

@ -7,4 +7,6 @@ require (
github.com/labstack/echo v3.3.10+incompatible github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect github.com/labstack/gommon v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0
) )

View File

@ -53,6 +53,9 @@ func main() {
err = apps.New(app.Name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory) err = apps.New(app.Name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory)
if err != nil { if err != nil {
if validationError, ok := err.(apps.ValidationError); ok {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: validationError.Errors}, JSONIndent)
}
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }
@ -71,6 +74,9 @@ func main() {
err = apps.Update(name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory) err = apps.Update(name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory)
if err != nil { if err != nil {
if validationError, ok := err.(apps.ValidationError); ok {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: validationError.Errors}, JSONIndent)
}
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }

View File

@ -1,5 +1,7 @@
package main package main
// Message represents response with information about results of something
type Message struct { type Message struct {
Message string `json:"message"` Message string `json:"message,omitempty"`
Errors []string `json:"errors,omitempty"`
} }