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 http://localhost:1323/v1/apps/test_1235
GET http://localhost:1323/v1/apps/test_1234
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
}
// 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{

View File

@ -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"}, " ")
}

View File

@ -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()

View File

@ -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 {

View File

@ -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
}

View File

@ -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())
}

27
main.go
View File

@ -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"})
})

View File

@ -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())
}

View File

@ -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
}

View File

@ -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