Implementation of Docker
This commit is contained in:
parent
837f629d8d
commit
064d6e6487
9 changed files with 416 additions and 15 deletions
32
apps/main.go
32
apps/main.go
|
@ -37,7 +37,7 @@ func List() (*[]App, error) {
|
|||
}
|
||||
|
||||
// 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{
|
||||
Name: name,
|
||||
SSHPort: SSHPort,
|
||||
|
@ -49,6 +49,13 @@ func New(name string, SSHPort int, HTTPPort int, image string, CPU string, memor
|
|||
|
||||
db := common.GetDBConnection()
|
||||
|
||||
validationErrors := app.Validate()
|
||||
if len(validationErrors) != 0 {
|
||||
return ValidationError{
|
||||
Errors: validationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Create(app).Error; err != nil {
|
||||
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
|
||||
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
|
||||
|
||||
db := common.GetDBConnection()
|
||||
|
@ -67,13 +74,20 @@ func Update(name string, SSHPort int, HTTPPort int, image string, CPU string, me
|
|||
return err
|
||||
}
|
||||
|
||||
err = db.Model(&app).Updates(App{
|
||||
SSHPort: SSHPort,
|
||||
HTTPPort: HTTPPort,
|
||||
Image: image,
|
||||
CPU: CPU,
|
||||
Memory: memory,
|
||||
}).Error
|
||||
app.SSHPort = SSHPort
|
||||
app.HTTPPort = HTTPPort
|
||||
app.Image = image
|
||||
app.CPU = CPU
|
||||
app.Memory = memory
|
||||
|
||||
validationErrors := app.Validate()
|
||||
if len(validationErrors) != 0 {
|
||||
return ValidationError{
|
||||
Errors: validationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
err = db.Update(&app).Error
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
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
|
||||
type Label struct {
|
||||
|
@ -15,12 +29,44 @@ type App struct {
|
|||
SSHPort int `json:"ssh_port"`
|
||||
HTTPPort int `json:"http_port"`
|
||||
Image string `json:"image"`
|
||||
Status string `json:"status"` // running, data (no container, data only), stopped
|
||||
CPU string `json:"cpu"`
|
||||
CPU int `json:"cpu"` // percentage, 200 means two CPU
|
||||
Memory int `json:"memory"` // Limit in MB
|
||||
Labels []Label `json:"labels"` // username:cx or user_id:1
|
||||
|
||||
MemoryUsage int `json:"memory_usage"` // Usage in MB
|
||||
DiskUsage int `json:"disk_usage"` // Usage in MB
|
||||
Status string `json:"status"` // running, data-only (no container, data only), stopped
|
||||
MemoryUsage int `json:"memory_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
222
docker/docker.go
Normal 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
50
docker/du.go
Normal 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
1
docker/main.go
Normal file
|
@ -0,0 +1 @@
|
|||
package docker
|
58
docker/types.go
Normal file
58
docker/types.go
Normal 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
2
go.mod
|
@ -7,4 +7,6 @@ require (
|
|||
github.com/labstack/echo v3.3.10+incompatible
|
||||
github.com/labstack/gommon v0.3.0 // 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
|
||||
)
|
||||
|
|
6
main.go
6
main.go
|
@ -53,6 +53,9 @@ func main() {
|
|||
|
||||
err = apps.New(app.Name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -71,6 +74,9 @@ func main() {
|
|||
|
||||
err = apps.Update(name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
4
types.go
4
types.go
|
@ -1,5 +1,7 @@
|
|||
package main
|
||||
|
||||
// Message represents response with information about results of something
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue