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:
parent
60f99d52b0
commit
9348504f9e
11 changed files with 171 additions and 62 deletions
2
api.http
2
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
|
||||
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
4
auth.go
4
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"}, " ")
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
27
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"})
|
||||
})
|
||||
|
|
70
stats.go
70
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())
|
||||
}
|
||||
|
|
5
types.go
5
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue