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
This commit is contained in:
Adam Štrauch 2020-07-26 00:34:16 +02:00
parent 60f99d52b0
commit 9348504f9e
Signed by: cx
GPG Key ID: 018304FFA8988F8D
11 changed files with 171 additions and 62 deletions

View File

@ -63,7 +63,7 @@ Content-type: application/json
# Get # Get
GET http://localhost:1323/v1/apps/test_1235 GET http://localhost:1323/v1/apps/test_1234
Content-type: application/json Content-type: application/json

View File

@ -111,8 +111,8 @@ func Update(name string, SSHPort int, HTTPPort int, image string, CPU int, memor
return &app, err return &app, err
} }
// UpdateState sets state // UpdateResources updates various metrics saved in the database
func UpdateState(name string, state string, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int) error { func UpdateResources(name string, state string, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int) error {
db := common.GetDBConnection() db := common.GetDBConnection()
err := db.Model(&App{}).Where("name = ?", name).Updates(App{ 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 return err
} }
// UpdateContainerState sets container's state // UpdateState sets container's state
func UpdateContainerState(name string, state string) error { func UpdateState(name string, state string) error {
db := common.GetDBConnection() db := common.GetDBConnection()
err := db.Model(&App{}).Where("name = ?", name).Updates(App{ err := db.Model(&App{}).Where("name = ?", name).Updates(App{

View File

@ -20,6 +20,10 @@ func TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
tokenHeader := c.Request().Header.Get("Authorization") tokenHeader := c.Request().Header.Get("Authorization")
token := strings.Replace(tokenHeader, "Token ", "", -1) token := strings.Replace(tokenHeader, "Token ", "", -1)
if token == "" {
token = c.QueryParam("token")
}
if token != configuredToken || configuredToken == "" { if token != configuredToken || configuredToken == "" {
return c.JSONPretty(403, map[string]string{"message": "access denied"}, " ") return c.JSONPretty(403, map[string]string{"message": "access denied"}, " ")
} }

View File

@ -20,7 +20,7 @@ import (
) )
// Stats delay in seconds // Stats delay in seconds
const statsDelay = 5 const statsDelay = 1
// Docker timeout // Docker timeout
const dockerTimeout = 10 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 // Stats returns current CPU and memory usage
func (d *Driver) Stats(name string) (float64, int, error) { func (d *Driver) Stats(name string) (float64, int, error) {
cli, err := d.getClient() cli, err := d.getClient()

View File

@ -7,7 +7,7 @@ type ContainerStats struct {
} `json:"pids_stats"` } `json:"pids_stats"`
CPU struct { CPU struct {
Usage struct { Usage struct {
Total int `json:"total_usage"` Total int64 `json:"total_usage"`
} `json:"cpu_usage"` } `json:"cpu_usage"`
} `json:"cpu_stats"` } `json:"cpu_stats"`
Memory struct { Memory struct {

View File

@ -8,6 +8,9 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/rosti-cz/node-api/apps"
) )
// Return bytes, inodes occupied by a directory and/or error if there is any // 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 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
}

View File

@ -22,7 +22,14 @@ func (c *Container) volumeHostPath() string {
return path.Join("/srv", c.App.Name) 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) { func (c *Container) GetState() (*apps.AppState, error) {
status, err := c.Status() status, err := c.Status()
if err != nil { if err != nil {
@ -73,7 +80,7 @@ func (c *Container) Status() (string, error) {
return status, nil 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) { func (c *Container) DiskUsage() (int, int, error) {
return du(c.volumeHostPath()) return du(c.volumeHostPath())
} }

27
main.go
View File

@ -26,7 +26,7 @@ func main() {
// Stats loop // Stats loop
go func() { go func() {
for { for {
err := gatherContainersStats() err := gatherStats()
if err != nil { if err != nil {
log.Println("LOOP ERROR:", err.Error()) log.Println("LOOP ERROR:", err.Error())
} }
@ -49,14 +49,16 @@ func main() {
e := echo.New() e := echo.New()
e.Renderer = t e.Renderer = t
// e.Use(TokenMiddleware) e.Use(TokenMiddleware)
// Returns list of apps // Returns list of apps
e.GET("/", func(c echo.Context) error { 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 { e.GET("/v1/apps", func(c echo.Context) error {
err := gatherContainersStates() err := gatherStates()
if err != nil { if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) 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 { e.GET("/v1/apps/:name", func(c echo.Context) error {
name := c.Param("name") name := c.Param("name")
app, err := apps.Get(name) err := updateState(name)
if err != nil { if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }
err = updateContainerState(app) app, err := apps.Get(name)
if err != nil { if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) 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) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }
go updateContainerStats(&app)
return c.JSON(http.StatusOK, Message{Message: "ok"}) return c.JSON(http.StatusOK, Message{Message: "ok"})
}) })
@ -186,8 +186,6 @@ func main() {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }
go updateContainerStats(app)
return c.JSON(http.StatusOK, Message{Message: "ok"}) return c.JSON(http.StatusOK, Message{Message: "ok"})
}) })
@ -209,8 +207,6 @@ func main() {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }
go updateContainerStats(app)
return c.JSON(http.StatusOK, Message{Message: "ok"}) return c.JSON(http.StatusOK, Message{Message: "ok"})
}) })
@ -232,8 +228,6 @@ func main() {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
} }
go updateContainerStats(app)
return c.JSON(http.StatusOK, Message{Message: "ok"}) return c.JSON(http.StatusOK, Message{Message: "ok"})
}) })
@ -286,7 +280,10 @@ func main() {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent) 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"}) return c.JSON(http.StatusOK, Message{Message: "ok"})
}) })

View File

@ -7,8 +7,41 @@ import (
"github.com/rosti-cz/node-api/docker" "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 // 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{ container := docker.Container{
App: app, App: app,
} }
@ -17,43 +50,22 @@ func updateContainerState(app *apps.App) error {
return err return err
} }
err = apps.UpdateContainerState( err = apps.UpdateState(
app.Name, app.Name,
state, state,
) )
return err return err
} }
// Updates info about all containers // gatherStats loops over all applications and calls updateUsage to write various metric into the database.
func updateContainerStats(app *apps.App) error { func gatherStats() 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 {
appList, err := apps.List() appList, err := apps.List()
if err != nil { if err != nil {
return err return err
} }
for _, app := range *appList { for _, app := range *appList {
err := updateContainerStats(&app) err := updateUsage(app.Name)
if err != nil { if err != nil {
log.Println("STATS ERROR:", err.Error()) log.Println("STATS ERROR:", err.Error())
} }
@ -62,15 +74,15 @@ func gatherContainersStats() error {
return nil return nil
} }
// gatherContainersStates refreshes all container's state // gatherStates loops over all apps and updates their container state
func gatherContainersStates() error { func gatherStates() error {
appList, err := apps.List() appList, err := apps.List()
if err != nil { if err != nil {
return err return err
} }
for _, app := range *appList { for _, app := range *appList {
err := updateContainerState(&app) err := updateState(app.Name)
if err != nil { if err != nil {
log.Println("STATE ERROR:", err.Error()) log.Println("STATE ERROR:", err.Error())
} }

View File

@ -5,3 +5,8 @@ type Message struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Errors []string `json:"errors,omitempty"` Errors []string `json:"errors,omitempty"`
} }
// data passed into the template
type templateData struct {
Token string
}

View File

@ -110,9 +110,9 @@
node: {}, node: {},
}, },
created() { created() {
fetch('/v1/apps').then(response => response.json()) fetch('/v1/apps?token={{ .Token }}').then(response => response.json())
.then(data => this.apps = data); .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); .then(data => this.node = data);
}, },
methods: { methods: {
@ -123,14 +123,14 @@
return this.api_status_code >= 400 && this.api_status_code < 505 return this.api_status_code >= 400 && this.api_status_code < 505
}, },
refresh: () => { refresh: () => {
fetch('/v1/apps').then(response => response.json()) fetch('/v1/apps?token={{ .Token }}').then(response => response.json())
.then(data => this.apps = data); .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); .then(data => this.node = data);
}, },
start: (id) => { start: (id) => {
app.api_response = "working" app.api_response = "working"
fetch('/v1/apps/'+id+'/start', {method: 'PUT'}) fetch('/v1/apps/'+id+'/start?token={{ .Token }}', {method: 'PUT'})
.then(response => { .then(response => {
app.api_status_code = response.status app.api_status_code = response.status
return response.json() return response.json()
@ -139,7 +139,7 @@
}, },
stop: (id) => { stop: (id) => {
app.api_response = "working" app.api_response = "working"
fetch('/v1/apps/'+id+'/stop', {method: 'PUT'}) fetch('/v1/apps/'+id+'/stop?token={{ .Token }}', {method: 'PUT'})
.then(response => { .then(response => {
app.api_status_code = response.status app.api_status_code = response.status
return response.json() return response.json()
@ -148,7 +148,7 @@
}, },
restart: (id) => { restart: (id) => {
app.api_response = "working" app.api_response = "working"
fetch('/v1/apps/'+id+'/restart', {method: 'PUT'}) fetch('/v1/apps/'+id+'/restart?token={{ .Token }}', {method: 'PUT'})
.then(response => { .then(response => {
app.api_status_code = response.status app.api_status_code = response.status
return response.json() return response.json()
@ -157,7 +157,7 @@
}, },
rebuild: (id) => { rebuild: (id) => {
app.api_response = "working" app.api_response = "working"
fetch('/v1/apps/'+id+'/rebuild', {method: 'PUT'}) fetch('/v1/apps/'+id+'/rebuild?token={{ .Token }}', {method: 'PUT'})
.then(response => { .then(response => {
app.api_status_code = response.status app.api_status_code = response.status
return response.json() return response.json()
@ -168,7 +168,7 @@
}, },
remove: (id) => { remove: (id) => {
app.api_response = "working" app.api_response = "working"
fetch('/v1/apps/'+id, {method: 'DELETE'}) fetch('/v1/apps/'+id+"?token={{ .Token }}", {method: 'DELETE'})
.then(response => { .then(response => {
response.json() response.json()
app.api_status_code = response.status app.api_status_code = response.status