From 9348504f9ef14cf2e730bf8154652ef0bc9c8fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0trauch?= Date: Sun, 26 Jul 2020 00:34:16 +0200 Subject: [PATCH] Final touches * Possibility to pass token in query param * More appropriate names for gathering and resource updating functions * Shorter measuring delay * Container status available immediatelly when needed --- api.http | 2 +- apps/main.go | 8 +++--- auth.go | 4 +++ docker/docker.go | 35 +++++++++++++++++++++++- docker/stats.go | 2 +- docker/tools.go | 51 +++++++++++++++++++++++++++++++++++ docker/types.go | 11 ++++++-- main.go | 27 +++++++++---------- stats.go | 70 ++++++++++++++++++++++++++++-------------------- types.go | 5 ++++ ui/index.html | 18 ++++++------- 11 files changed, 171 insertions(+), 62 deletions(-) diff --git a/api.http b/api.http index 5278f08..0fa95d9 100644 --- a/api.http +++ b/api.http @@ -63,7 +63,7 @@ Content-type: application/json # Get -GET http://localhost:1323/v1/apps/test_1235 +GET http://localhost:1323/v1/apps/test_1234 Content-type: application/json diff --git a/apps/main.go b/apps/main.go index 64059ca..e2232c3 100644 --- a/apps/main.go +++ b/apps/main.go @@ -111,8 +111,8 @@ func Update(name string, SSHPort int, HTTPPort int, image string, CPU int, memor return &app, err } -// UpdateState sets state -func UpdateState(name string, state string, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int) error { +// UpdateResources updates various metrics saved in the database +func UpdateResources(name string, state string, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int) error { db := common.GetDBConnection() err := db.Model(&App{}).Where("name = ?", name).Updates(App{ @@ -125,8 +125,8 @@ func UpdateState(name string, state string, CPUUsage float64, memory int, diskUs return err } -// UpdateContainerState sets container's state -func UpdateContainerState(name string, state string) error { +// UpdateState sets container's state +func UpdateState(name string, state string) error { db := common.GetDBConnection() err := db.Model(&App{}).Where("name = ?", name).Updates(App{ diff --git a/auth.go b/auth.go index 1e5b1ff..35fc064 100644 --- a/auth.go +++ b/auth.go @@ -20,6 +20,10 @@ func TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc { tokenHeader := c.Request().Header.Get("Authorization") token := strings.Replace(tokenHeader, "Token ", "", -1) + if token == "" { + token = c.QueryParam("token") + } + if token != configuredToken || configuredToken == "" { return c.JSONPretty(403, map[string]string{"message": "access denied"}, " ") } diff --git a/docker/docker.go b/docker/docker.go index 69a006f..0ff28d1 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -20,7 +20,7 @@ import ( ) // Stats delay in seconds -const statsDelay = 5 +const statsDelay = 1 // Docker timeout const dockerTimeout = 10 @@ -104,6 +104,39 @@ func (d *Driver) Status(name string) (string, error) { } +// RawStats returns snapshot of current cpu and memory usage, CPU cannot be used +// for % usage because it's number of used tics. Call this twice to get the % usage. +// One second is 10 000 000 ticks. +func (d *Driver) RawStats(name string) (int64, int, error) { + cli, err := d.getClient() + if err != nil { + return 0.0, 0, err + } + + containerID, err := d.nameToID(name) + if err != nil { + return 0.0, 0, err + } + + stats, err := cli.ContainerStats(context.TODO(), containerID, false) + if err != nil { + return 0.0, 0, err + } + + row := ContainerStats{} + + data, err := ioutil.ReadAll(stats.Body) + if err != nil { + return 0.0, 0, err + } + err = json.Unmarshal(data, &row) + if err != nil { + return 0.0, 0, err + } + + return row.CPU.Usage.Total, row.Memory.Usage, nil +} + // Stats returns current CPU and memory usage func (d *Driver) Stats(name string) (float64, int, error) { cli, err := d.getClient() diff --git a/docker/stats.go b/docker/stats.go index 7e9a073..1a50dc6 100644 --- a/docker/stats.go +++ b/docker/stats.go @@ -7,7 +7,7 @@ type ContainerStats struct { } `json:"pids_stats"` CPU struct { Usage struct { - Total int `json:"total_usage"` + Total int64 `json:"total_usage"` } `json:"cpu_usage"` } `json:"cpu_stats"` Memory struct { diff --git a/docker/tools.go b/docker/tools.go index fb9f1c8..bf4cec9 100644 --- a/docker/tools.go +++ b/docker/tools.go @@ -8,6 +8,9 @@ import ( "path/filepath" "strconv" "strings" + "time" + + "github.com/rosti-cz/node-api/apps" ) // Return bytes, inodes occupied by a directory and/or error if there is any @@ -78,3 +81,51 @@ func removeDirectory(dir string) error { } return nil } + +// CPUMemoryStats returns list of applications with updated CPU and memory usage effectively. +// Sample is number of seconds between two measurements of the CPU usage. More means more precise. +func CPUMemoryStats(applist *[]apps.App, sample int) (*[]apps.App, error) { + if sample <= 0 { + sample = 1 + } + + containers := []Container{} + for _, app := range *applist { + containers = append(containers, Container{App: &app}) + } + + startMap := make(map[string]int64) + endMap := make(map[string]int64) + + start := time.Now().UnixNano() + for _, container := range containers { + cpu, _, err := container.GetRawResourceStats() + if err != nil { + return nil, err + } + startMap[container.App.Name] = cpu + } + + time.Sleep(time.Duration(sample) * time.Second) + + for idx, container := range containers { + cpu, memory, err := container.GetRawResourceStats() + if err != nil { + return nil, err + } + endMap[container.App.Name] = cpu + containers[idx].App.MemoryUsage = memory + } + + end := time.Now().UnixNano() + difference := (float64(end) - float64(start)) / float64(1000000000) + + updatedApps := []apps.App{} + for _, container := range containers { + app := *container.App + app.CPUUsage = (float64(endMap[app.Name]) - float64(startMap[app.Name])) / difference / 10000000.0 + updatedApps = append(updatedApps, app) + } + + return &updatedApps, nil +} diff --git a/docker/types.go b/docker/types.go index a184900..e41a8cc 100644 --- a/docker/types.go +++ b/docker/types.go @@ -22,7 +22,14 @@ func (c *Container) volumeHostPath() string { return path.Join("/srv", c.App.Name) } -// GetState app object with populated state fields +// GetRawResourceStats returns RAW CPU and memory usage directly from Docker API +func (c *Container) GetRawResourceStats() (int64, int, error) { + driver := c.getDriver() + cpu, memory, err := driver.RawStats(c.App.Name) + return cpu, memory, err +} + +// GetState returns app state object with populated state fields func (c *Container) GetState() (*apps.AppState, error) { status, err := c.Status() if err != nil { @@ -73,7 +80,7 @@ func (c *Container) Status() (string, error) { return status, nil } -// DiskUsage returns number of MB and inodes used by the container in it's mounted volume +// DiskUsage returns number of bytes and inodes used by the container in it's mounted volume func (c *Container) DiskUsage() (int, int, error) { return du(c.volumeHostPath()) } diff --git a/main.go b/main.go index 8cb14ee..79c3e03 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ func main() { // Stats loop go func() { for { - err := gatherContainersStats() + err := gatherStats() if err != nil { log.Println("LOOP ERROR:", err.Error()) } @@ -49,14 +49,16 @@ func main() { e := echo.New() e.Renderer = t - // e.Use(TokenMiddleware) + e.Use(TokenMiddleware) // Returns list of apps e.GET("/", func(c echo.Context) error { - return c.Render(http.StatusOK, "index.html", "") + return c.Render(http.StatusOK, "index.html", templateData{ + Token: configuredToken, + }) }) e.GET("/v1/apps", func(c echo.Context) error { - err := gatherContainersStates() + err := gatherStates() if err != nil { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } @@ -74,12 +76,12 @@ func main() { e.GET("/v1/apps/:name", func(c echo.Context) error { name := c.Param("name") - app, err := apps.Get(name) + err := updateState(name) if err != nil { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - err = updateContainerState(app) + app, err := apps.Get(name) if err != nil { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } @@ -163,8 +165,6 @@ func main() { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - go updateContainerStats(&app) - return c.JSON(http.StatusOK, Message{Message: "ok"}) }) @@ -186,8 +186,6 @@ func main() { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - go updateContainerStats(app) - return c.JSON(http.StatusOK, Message{Message: "ok"}) }) @@ -209,8 +207,6 @@ func main() { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - go updateContainerStats(app) - return c.JSON(http.StatusOK, Message{Message: "ok"}) }) @@ -232,8 +228,6 @@ func main() { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - go updateContainerStats(app) - return c.JSON(http.StatusOK, Message{Message: "ok"}) }) @@ -286,7 +280,10 @@ func main() { return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) } - go updateContainerStats(app) + err = container.Start() + if err != nil { + return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) + } return c.JSON(http.StatusOK, Message{Message: "ok"}) }) diff --git a/stats.go b/stats.go index fe17ceb..fd87488 100644 --- a/stats.go +++ b/stats.go @@ -7,8 +7,41 @@ import ( "github.com/rosti-cz/node-api/docker" ) +// updateUsage updates various resource usage of the container/app in the database +func updateUsage(name string) error { + app, err := apps.Get(name) + if err != nil { + return err + } + + container := docker.Container{ + App: app, + } + + state, err := container.GetState() + if err != nil { + return err + } + + err = apps.UpdateResources( + name, + state.State, + state.CPUUsage, + state.MemoryUsage, + state.DiskUsageBytes, + state.DiskUsageInodes, + ) + + return err +} + // Updates only container's state -func updateContainerState(app *apps.App) error { +func updateState(name string) error { + app, err := apps.Get(name) + if err != nil { + return err + } + container := docker.Container{ App: app, } @@ -17,43 +50,22 @@ func updateContainerState(app *apps.App) error { return err } - err = apps.UpdateContainerState( + err = apps.UpdateState( app.Name, state, ) return err } -// Updates info about all containers -func updateContainerStats(app *apps.App) error { - container := docker.Container{ - App: app, - } - state, err := container.GetState() - if err != nil { - return err - } - - err = apps.UpdateState( - app.Name, - state.State, - state.CPUUsage, - state.MemoryUsage, - state.DiskUsageBytes, - state.DiskUsageInodes, - ) - return err -} - -// gatherContainersStats gathers information about containers and saves it into the database -func gatherContainersStats() error { +// gatherStats loops over all applications and calls updateUsage to write various metric into the database. +func gatherStats() error { appList, err := apps.List() if err != nil { return err } for _, app := range *appList { - err := updateContainerStats(&app) + err := updateUsage(app.Name) if err != nil { log.Println("STATS ERROR:", err.Error()) } @@ -62,15 +74,15 @@ func gatherContainersStats() error { return nil } -// gatherContainersStates refreshes all container's state -func gatherContainersStates() error { +// gatherStates loops over all apps and updates their container state +func gatherStates() error { appList, err := apps.List() if err != nil { return err } for _, app := range *appList { - err := updateContainerState(&app) + err := updateState(app.Name) if err != nil { log.Println("STATE ERROR:", err.Error()) } diff --git a/types.go b/types.go index 8d59c67..a157d26 100644 --- a/types.go +++ b/types.go @@ -5,3 +5,8 @@ type Message struct { Message string `json:"message,omitempty"` Errors []string `json:"errors,omitempty"` } + +// data passed into the template +type templateData struct { + Token string +} diff --git a/ui/index.html b/ui/index.html index ecf3431..82609fd 100644 --- a/ui/index.html +++ b/ui/index.html @@ -110,9 +110,9 @@ node: {}, }, created() { - fetch('/v1/apps').then(response => response.json()) + fetch('/v1/apps?token={{ .Token }}').then(response => response.json()) .then(data => this.apps = data); - fetch('/v1/node').then(response => response.json()) + fetch('/v1/node?token={{ .Token }}').then(response => response.json()) .then(data => this.node = data); }, methods: { @@ -123,14 +123,14 @@ return this.api_status_code >= 400 && this.api_status_code < 505 }, refresh: () => { - fetch('/v1/apps').then(response => response.json()) + fetch('/v1/apps?token={{ .Token }}').then(response => response.json()) .then(data => this.apps = data); - fetch('/v1/node').then(response => response.json()) + fetch('/v1/node?token={{ .Token }}').then(response => response.json()) .then(data => this.node = data); }, start: (id) => { app.api_response = "working" - fetch('/v1/apps/'+id+'/start', {method: 'PUT'}) + fetch('/v1/apps/'+id+'/start?token={{ .Token }}', {method: 'PUT'}) .then(response => { app.api_status_code = response.status return response.json() @@ -139,7 +139,7 @@ }, stop: (id) => { app.api_response = "working" - fetch('/v1/apps/'+id+'/stop', {method: 'PUT'}) + fetch('/v1/apps/'+id+'/stop?token={{ .Token }}', {method: 'PUT'}) .then(response => { app.api_status_code = response.status return response.json() @@ -148,7 +148,7 @@ }, restart: (id) => { app.api_response = "working" - fetch('/v1/apps/'+id+'/restart', {method: 'PUT'}) + fetch('/v1/apps/'+id+'/restart?token={{ .Token }}', {method: 'PUT'}) .then(response => { app.api_status_code = response.status return response.json() @@ -157,7 +157,7 @@ }, rebuild: (id) => { app.api_response = "working" - fetch('/v1/apps/'+id+'/rebuild', {method: 'PUT'}) + fetch('/v1/apps/'+id+'/rebuild?token={{ .Token }}', {method: 'PUT'}) .then(response => { app.api_status_code = response.status return response.json() @@ -168,7 +168,7 @@ }, remove: (id) => { app.api_response = "working" - fetch('/v1/apps/'+id, {method: 'DELETE'}) + fetch('/v1/apps/'+id+"?token={{ .Token }}", {method: 'DELETE'}) .then(response => { response.json() app.api_status_code = response.status