From 4f9dac7f5c78edaf34c0a96a26e3b283d9ad9ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0trauch?= Date: Sat, 11 Jul 2020 23:14:45 +0200 Subject: [PATCH] Rest of the endpoints finished --- api.http | 32 ++++++++++-- apps/types.go | 9 ++++ docker/docker.go | 41 ++++++++------- docker/types.go | 37 +++++++++++--- main.go | 128 +++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 208 insertions(+), 39 deletions(-) diff --git a/api.http b/api.http index eaac4a0..f158a4d 100644 --- a/api.http +++ b/api.http @@ -3,8 +3,8 @@ Content-type: application/json { "name": "test_1234", - "ssh_port": 10000, - "http_port": 10001, + "ssh_port": 46500, + "http_port": 46501, "image": "docker.io/rosti/runtime:2020.04-1", "cpu": 100, "memory": 128000 @@ -16,8 +16,8 @@ PUT http://localhost:1323/v1/apps/test_1234 Content-type: application/json { - "ssh_port": 36500, - "http_port": 36501 + "ssh_port": 46500, + "http_port": 46501 } @@ -25,3 +25,27 @@ Content-type: application/json PUT http://localhost:1323/v1/apps/test_1234/start Content-type: application/json + +### + + +PUT http://localhost:1323/v1/apps/test_1234/stop +Content-type: application/json + +### + +DELETE http://localhost:1323/v1/apps/test_1234 +Content-type: application/json + +### + + +GET http://localhost:1323/v1/apps/test_1234/stats +Content-type: application/json + + +### + + +GET http://localhost:1323/v1/apps +Content-type: application/json diff --git a/apps/types.go b/apps/types.go index a178b49..0c31a73 100644 --- a/apps/types.go +++ b/apps/types.go @@ -23,6 +23,13 @@ type Label struct { Value string `json:"value"` } +// AppState contains info about runnint application, it's not saved in the database +type AppState struct { + State string `json:"state"` + CPUUsage int `json:"cpu_usage"` + MemoryUsage int `json:"memory_usage"` +} + // App keeps info about hosted application type App struct { gorm.Model @@ -34,6 +41,8 @@ type App struct { 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 + + State AppState `json:"state" gorm:"-"` } // Validate do basic checks of the struct values diff --git a/docker/docker.go b/docker/docker.go index db62df2..79d5293 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "log" "os" - "strconv" "strings" "time" @@ -25,7 +24,7 @@ const dockerTimeout = 10 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" +const dockerAPIVersion = "1.24" // Driver keeps everything for connection to Docker type Driver struct{} @@ -76,8 +75,10 @@ func (d *Driver) Status(name string) (string, error) { } containerID, err := d.nameToID(name) + if err != nil && err.Error() == "no container found" { + return "no-container", err + } if err != nil { - status = "no-container" return status, err } @@ -117,7 +118,9 @@ func (d *Driver) Stats(name string) (float64, int, error) { if err != nil { return 0.0, 0, err } - log.Println(data) + // It returns one JSON: + // {"read":"2020-07-11T20:42:31.486726241Z","preread":"2020-07-11T20:42:30.484048602Z","pids_stats":{"current":7},"blkio_stats":{"io_service_bytes_recursive":[{"major":253,"minor":0,"op":"Read","value":0},{"major":253,"minor":0,"op":"Write","value":20480},{"major":253,"minor":0,"op":"Sync","value":12288},{"major":253,"minor":0,"op":"Async","value":8192},{"major":253,"minor":0,"op":"Discard","value":0},{"major":253,"minor":0,"op":"Total","value":20480}],"io_serviced_recursive":[{"major":253,"minor":0,"op":"Read","value":0},{"major":253,"minor":0,"op":"Write","value":5},{"major":253,"minor":0,"op":"Sync","value":3},{"major":253,"minor":0,"op":"Async","value":2},{"major":253,"minor":0,"op":"Discard","value":0},{"major":253,"minor":0,"op":"Total","value":5}],"io_queue_recursive":[],"io_service_time_recursive":[],"io_wait_time_recursive":[],"io_merged_recursive":[],"io_time_recursive":[],"sectors_recursive":[]},"num_procs":0,"storage_stats":{},"cpu_stats":{"cpu_usage":{"total_usage":758392753,"percpu_usage":[302688474,0,11507116,124238500,222136766,5656446,3009320,0,19406386,1397028,6201423,62151294,0,0,0,0],"usage_in_kernelmode":100000000,"usage_in_usermode":640000000},"system_cpu_usage":119385810000000,"online_cpus":12,"throttling_data":{"periods":21,"throttled_periods":1,"throttled_time":2995938}},"precpu_stats":{"cpu_usage":{"total_usage":758282347,"percpu_usage":[302688474,0,11507116,124238500,222026360,5656446,3009320,0,19406386,1397028,6201423,62151294,0,0,0,0],"usage_in_kernelmode":100000000,"usage_in_usermode":640000000},"system_cpu_usage":119373720000000,"online_cpus":12,"throttling_data":{"periods":21,"throttled_periods":1,"throttled_time":2995938}},"memory_stats":{"usage":21626880,"max_usage":22630400,"stats":{"active_anon":15949824,"active_file":0,"cache":0,"dirty":0,"hierarchical_memory_limit":144179200,"hierarchical_memsw_limit":288358400,"inactive_anon":0,"inactive_file":0,"mapped_file":0,"pgfault":13167,"pgmajfault":0,"pgpgin":7293,"pgpgout":3406,"rss":15900672,"rss_huge":0,"total_active_anon":15949824,"total_active_file":0,"total_cache":0,"total_dirty":0,"total_inactive_anon":0,"total_inactive_file":0,"total_mapped_file":0,"total_pgfault":13167,"total_pgmajfault":0,"total_pgpgin":7293,"total_pgpgout":3406,"total_rss":15900672,"total_rss_huge":0,"total_unevictable":0,"total_writeback":0,"unevictable":0,"writeback":0},"limit":144179200},"name":"/test_1234","id":"576878d645efecc8e5e2a57b88351f7b5c551e3fc72dc8473fd965d10dfddbec","networks":{"eth0":{"rx_bytes":6150,"rx_packets":37,"rx_errors":0,"rx_dropped":0,"tx_bytes":0,"tx_packets":0,"tx_errors":0,"tx_dropped":0}}} + log.Println(string(data)) return 0.0, 0, nil } @@ -248,24 +251,26 @@ func (d *Driver) Create(name string, image string, volumePath string, HTTPPort i return "", err } - portmaps := make(nat.PortMap, 1) + portmaps := nat.PortMap{} - portbindingsHTTP := make([]nat.PortBinding, 1) - portbindingsHTTP[0] = nat.PortBinding{ - HostPort: strconv.Itoa(HTTPPort) + "/tcp", - } - portmaps["8000/tcp"] = portbindingsHTTP + // portbindingsHTTP := make([]nat.PortBinding, 1) + // portbindingsHTTP[0] = nat.PortBinding{ + // HostIP: "0.0.0.0", + // 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 - } + // if SSHPort != 0 { + // portbindingsSSH := make([]nat.PortBinding, 1) + // portbindingsSSH[0] = nat.PortBinding{ + // HostIP: "0.0.0.0", + // HostPort: strconv.Itoa(SSHPort) + "/tcp", + // } + // portmaps["22/tcp"] = portbindingsSSH + // } createdContainer, err := cli.ContainerCreate( - context.TODO(), + context.Background(), &container.Config{ Hostname: name, Env: []string{}, diff --git a/docker/types.go b/docker/types.go index a3fbdd9..0e584b9 100644 --- a/docker/types.go +++ b/docker/types.go @@ -1,6 +1,7 @@ package docker import ( + "log" "path" "github.com/rosti-cz/apps-api/apps" @@ -8,7 +9,7 @@ import ( // Container extends App struct from App type Container struct { - App apps.App `json:"app"` + App *apps.App `json:"app"` } func (c *Container) getDriver() *Driver { @@ -21,8 +22,26 @@ func (c *Container) volumeHostPath() string { return path.Join("/srv", c.App.Name) } +// GetApp app object with populated state fields +func (c *Container) GetApp() (*apps.App, error) { + status, err := c.Status() + if err != nil { + return nil, err + } + + state := apps.AppState{ + State: status, + CPUUsage: 0, + MemoryUsage: 0, + } + + c.App.State = state + + return c.App, nil +} + // Status returns state of the container -// Possible values: running, stopped, data-only, unknown +// Possible values: running, stopped, no-container, unknown func (c *Container) Status() (string, error) { status := "unknown" @@ -32,15 +51,14 @@ func (c *Container) Status() (string, error) { driver := c.getDriver() containerStatus, err := driver.Status(c.App.Name) + if err != nil && err.Error() == "no container found" { + return "no-container", nil + } if err != nil { return status, err } - if containerStatus == "no-container" { - status = "data-only" - } else { - status = containerStatus - } + status = containerStatus return status, nil } @@ -121,6 +139,9 @@ func (c *Container) Delete() error { volumePath := path.Join("/srv", c.App.Name) err = removeDirectory(volumePath) + if err != nil { + log.Println(err) + } - return err + return nil } diff --git a/main.go b/main.go index 7ca0ba6..2117e4d 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "log" "net/http" "github.com/labstack/echo" @@ -25,13 +26,25 @@ func main() { // Returns list of apps e.GET("/v1/apps", func(c echo.Context) error { - apps, err := apps.List() + applications, err := apps.List() if err != nil { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - return c.JSON(http.StatusOK, apps) + populatedApps := []*apps.App{} + for _, app := range *applications { + container := docker.Container{ + App: &app, + } + populatedApp, err := container.GetApp() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + populatedApps = append(populatedApps, populatedApp) + } + + return c.JSON(http.StatusOK, populatedApps) }) // Returns one app @@ -43,6 +56,14 @@ func main() { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } + container := docker.Container{ + App: app, + } + _, err = container.GetApp() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + return c.JSON(http.StatusOK, app) }) @@ -63,7 +84,7 @@ func main() { } container := docker.Container{ - App: app, + App: &app, } err = container.Create() @@ -100,7 +121,7 @@ func main() { app = *appPointer container := docker.Container{ - App: app, + App: &app, } err = container.Destroy() @@ -123,7 +144,23 @@ func main() { // Stop existing app e.PUT("/v1/apps/:name/stop", func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{}) + name := c.Param("name") + + app, err := apps.Get(name) + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + container := docker.Container{ + App: app, + } + + err = container.Stop() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + return c.JSON(http.StatusOK, Message{Message: "ok"}) }) // Start existing app @@ -136,7 +173,7 @@ func main() { } container := docker.Container{ - App: *app, + App: app, } err = container.Start() @@ -149,19 +186,92 @@ func main() { // Stop existing app e.PUT("/v1/apps/:name/restart", func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{}) + name := c.Param("name") + + app, err := apps.Get(name) + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + container := docker.Container{ + App: app, + } + + err = container.Restart() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + return c.JSON(http.StatusOK, Message{Message: "ok"}) + }) + + // Stats for existing app + e.GET("/v1/apps/:name/stats", func(c echo.Context) error { + name := c.Param("name") + + app, err := apps.Get(name) + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + container := docker.Container{ + App: app, + } + + cpu, memory, err := container.ResourceUsage() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + log.Println("DEBUG", cpu, memory) + + return c.JSON(http.StatusOK, Message{Message: "ok"}) }) // Rebuilds existing app, it keeps the data but created the container again e.PUT("/v1/apps/:name/rebuild", func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{}) + name := c.Param("name") + + app, err := apps.Get(name) + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + container := docker.Container{ + App: app, + } + + err = container.Destroy() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + err = container.Create() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + return c.JSON(http.StatusOK, Message{Message: "ok"}) }) // Delete one app e.DELETE("/v1/apps/:name", func(c echo.Context) error { name := c.Param("name") - err := apps.Delete(name) + app, err := apps.Get(name) + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + container := docker.Container{ + App: app, + } + + err = container.Delete() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } + + err = apps.Delete(name) if err != nil { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) }