Implementation of Docker
This commit is contained in:
parent
837f629d8d
commit
064d6e6487
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
|
// 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
|
||||||
}
|
}
|
||||||
|
@ -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
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/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
|
||||||
)
|
)
|
||||||
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
types.go
4
types.go
@ -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"`
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user