Handling apps via NATS messaging

This commit is contained in:
Adam Štrauch 2021-04-26 00:57:05 +02:00
parent e9c46f5d5c
commit 162daf6cba
Signed by: cx
GPG key ID: 018304FFA8988F8D
15 changed files with 861 additions and 61 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ node-api
.history/
api-node-17.http
*-packr.go
*.sqlite

21
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}",
"env": {
"NATS_URL": "nats://192.168.122.127:4222",
"NATS_ALIAS": "node-x",
"DATABASE_PATH": "./node-x.sqlite",
"TOKEN": "ABCD",
},
}
]
}

View file

@ -1,5 +1,5 @@
POST http://localhost:1323/v1/apps
Authorization: Token fee60059-f554-4c35-b44f-74b6be377095
Authorization: Token ABCD
Content-type: application/json
{

10
auth.go
View file

@ -1,7 +1,6 @@
package main
import (
"log"
"strings"
"github.com/labstack/echo"
@ -9,13 +8,6 @@ import (
var skipPaths []string = []string{"/metrics"}
var configuredToken string
func init() {
configuredToken = setToken()
log.Println("Access token:", configuredToken)
}
// TokenMiddleware handles authentication
func TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
@ -34,7 +26,7 @@ func TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
token = c.QueryParam("token")
}
if (token != configuredToken || configuredToken == "") && !skip {
if (token != config.Token || config.Token == "") && !skip {
return c.JSONPretty(403, map[string]string{"message": "access denied"}, " ")
}

View file

@ -8,9 +8,13 @@ import (
// Config keeps info about configuration of this daemon
type Config struct {
Token string `envconfig:"TOKEN" required:"true"`
AppsPath string `envconfig:"APPS_PATH" default:"/srv"` // Where applications are located
AppsBindIPHTTP string `envconfig:"APPS_BIND_IP_HTTP" default:"0.0.0.0"` // On what IP apps' HTTP port gonna be bound
AppsBindIPSSH string `envconfig:"APPS_BIND_IP_SSH" default:"0.0.0.0"` // On what IP apps' SSH ports gonna be bound
NATSURL string `envconfig:"NATS_URL" required:"true"`
NATSAlias string `envconfig:"NATS_ALIAS" required:"true"` // name/alias of the instance, ex. node-18
DatabasePath string `envconfig:"DATABASE_PATH" default:"/var/lib/node-api/rosti.db"`
}
// GetConfig return configuration created based on environment variables

View file

@ -13,7 +13,9 @@ var db *gorm.DB
func init() {
var err error
db, err = gorm.Open("sqlite3", "/var/lib/node-api/rosti.db")
config := GetConfig()
db, err = gorm.Open("sqlite3", config.DatabasePath)
if err != nil {
log.Fatalln(err)
}

View file

@ -1,41 +0,0 @@
package main
import (
"io/ioutil"
"log"
"os"
uuid "github.com/satori/go.uuid"
)
const configDirectory = "/var/lib/node-api"
const tokenFilename = "/var/lib/node-api/token"
func setToken() string {
if _, err := os.Stat(configDirectory); os.IsNotExist(err) {
err = os.MkdirAll(configDirectory, 0700)
if err != nil {
log.Fatalln(err)
}
}
// Load token from the file
var token string
if _, err := os.Stat(tokenFilename); os.IsNotExist(err) {
token = uuid.NewV4().String()
err = ioutil.WriteFile(tokenFilename, []byte(token), 0600)
if err != nil {
log.Fatalln(err)
}
} else {
tokenRaw, err := ioutil.ReadFile(tokenFilename)
if err != nil {
log.Fatalln(err)
}
token = string(tokenRaw)
}
return token
}

6
contrib/test_messages.sh Normal file
View file

@ -0,0 +1,6 @@
#!/bin/bash
nats pub --count 1 admin.apps.node-x.requests -w '{"type": "list"}'
nats pub --count 1 admin.apps.node-x.requests -w '{"type": "get", "app_name": "test_1234"}'
nats pub --count 1 admin.apps.node-x.requests -w '{"type": "create", "app_name": "natstest_0004", "payload": {"name": "natstest_0004", "image": "docker.io/rosti/runtime:2020.09-1", "cpu": 50, "memory": 256, "ssh_port": 20004, "http_port": 30004}}'

6
go.mod
View file

@ -3,17 +3,21 @@ module github.com/rosti-cz/node-api
go 1.14
require (
github.com/Microsoft/go-winio v0.4.18 // indirect
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/gobuffalo/packr v1.30.1
github.com/jinzhu/gorm v1.9.14
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect
github.com/nats-io/nats.go v1.10.0
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v2.20.6+incompatible
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect

22
go.sum
View file

@ -3,6 +3,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.18 h1:yjwCO1nhWEShaA5qsmPOBzAOjRCa2PRLsDNZ5yBWXpg=
github.com/Microsoft/go-winio v0.4.18/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@ -11,6 +13,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -41,6 +45,8 @@ github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
@ -121,6 +127,15 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats.go v1.10.0 h1:L8qnKaofSfNFbXg0C5F71LdjPRnmQwSsA4ukmkt1TvY=
github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.4 h1:aEsHIssIk6ETN5m2/MD8Y4B2X7FfXrBAUdkyRvbVYzA=
github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@ -138,6 +153,7 @@ github.com/shirou/gopsutil v2.20.6+incompatible h1:P37G9YH8M4vqkKcwBosp+URN5O8Ta
github.com/shirou/gopsutil v2.20.6+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@ -176,7 +192,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -210,8 +228,12 @@ golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

View file

@ -15,7 +15,7 @@ import (
func homeHandler(c echo.Context) error {
return c.Render(http.StatusOK, "index.html", templateData{
Token: configuredToken,
Token: config.Token,
})
}
@ -48,11 +48,6 @@ func getAppHandler(c echo.Context) error {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
app, err = apps.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSON(http.StatusOK, app)
}

648
handlers_nats.go Normal file
View file

@ -0,0 +1,648 @@
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/nats-io/nats.go"
"github.com/pkg/errors"
"github.com/rosti-cz/node-api/apps"
"github.com/rosti-cz/node-api/docker"
"github.com/rosti-cz/node-api/node"
)
// This handler only passes messages to another function for easier testing
func messageHandler(msg *nats.Msg) {
go _messageHandler(msg)
}
func _messageHandler(m *nats.Msg) error {
message := RequestMessage{}
err := json.Unmarshal(m.Data, &message)
if err != nil {
log.Println(errors.Wrap(err, "invalid JSON data in the incoming message"))
return err
}
fmt.Printf("Received a message: %v\n", message)
eventHandlerMap := map[string](func(m *nats.Msg, message *RequestMessage) error){
"list": listEventHandler,
"get": getEventHandler,
"create": createEventHandler,
"update": updateEventHandler,
"delete": deleteEventHandler,
"stop": stopEventHandler,
"start": startEventHandler,
"restart": restartEventHandler,
"update_keys": updateKeysEventHandler,
"set_password": setPasswordEventHandler,
"processes": processesEventHandler,
"enable_tech": enableTechEventHandler,
"rebuild": rebuildEventHandler,
"add_label": addLabelEventHandler,
"remove_label": removeLabelEventHandler,
"list_orphans": listOrphansEventHandler,
"node": getNoteEventHandler,
}
if eventHandler, ok := eventHandlerMap[message.Type]; ok {
return eventHandler(m, &message)
} else {
log.Println("ERROR: event handler not defined for " + message.Type)
}
// Set password for the app user in the container
// Application processes
// Enable one of the supported technologies or services (python, node, redis, ...)
// Rebuilds existing app, it keeps the data but creates the container again
// Adds new label
// Removes existing label
// Orphans returns directories in /srv that doesn't match any hosted application
// Return info about the node including performance index
return nil
}
// Returns list of apps
func listEventHandler(m *nats.Msg, message *RequestMessage) error {
log.Println("> List")
err := gatherStates()
if err != nil {
return errorReplyFormater(m, "backend error", err)
}
applications, err := apps.List()
if err != nil {
return errorReplyFormater(m, "backend error", err)
}
reply := ReplyMessage{
Payload: applications,
}
data, err := json.Marshal(reply)
if err != nil {
return errorReplyFormater(m, "reply formatter error", err)
}
err = m.Respond(data)
if err != nil {
log.Println("ERROR: list apps:", err.Error())
}
return err
}
// Returns one app
func getEventHandler(m *nats.Msg, message *RequestMessage) error {
err := updateState(message.AppName)
if err != nil {
return errorReplyFormater(m, "backend error", err)
}
app, err := apps.Get(message.AppName)
if err != nil {
return errorReplyFormater(m, "backend error", err)
}
reply := ReplyMessage{
AppName: app.Name,
Payload: app,
}
data, err := json.Marshal(reply)
if err != nil {
return errorReplyFormater(m, "reply formatter error", err)
}
err = m.Respond(data)
if err != nil {
log.Println("ERROR: get app:", err.Error())
}
return err
}
// Create a new app
func createEventHandler(m *nats.Msg, message *RequestMessage) error {
appTemplate := apps.App{}
body := []byte(message.Payload)
err := json.Unmarshal(body, &appTemplate)
if err != nil {
log.Println("ERROR create application problem: " + err.Error())
publish(appTemplate.Name, "payload parsing problem", true)
return err
}
err = apps.New(appTemplate.Name, appTemplate.SSHPort, appTemplate.HTTPPort, appTemplate.Image, appTemplate.CPU, appTemplate.Memory)
if err != nil {
if validationError, ok := err.(apps.ValidationError); ok {
log.Println("ERROR create application problem: " + validationError.Error())
publish(appTemplate.Name, "validation problem", true)
return err
}
log.Println("ERROR create application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
container := docker.Container{
App: &appTemplate,
}
err = container.Create()
if err != nil {
log.Println("ERROR create application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
err = container.Start()
if err != nil {
log.Println("ERROR create application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
publish(appTemplate.Name, "created", false)
return nil
}
// Update existing app
func updateEventHandler(m *nats.Msg, message *RequestMessage) error {
appTemplate := apps.App{}
body := []byte(message.Payload)
err := json.Unmarshal(body, &appTemplate)
if err != nil {
log.Println("ERROR update application problem: " + err.Error())
publish(appTemplate.Name, "payload parsing problem", true)
return err
}
app, err := apps.Update(message.AppName, appTemplate.SSHPort, appTemplate.HTTPPort, appTemplate.Image, appTemplate.CPU, appTemplate.Memory)
if err != nil {
if validationError, ok := err.(apps.ValidationError); ok {
log.Println("ERROR update application problem: " + validationError.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
log.Println("ERROR update application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
err = container.Destroy()
if err != nil && err.Error() == "no container found" {
// We don't care if the container didn't exist anyway
err = nil
}
if err != nil {
log.Println("ERROR update application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
err = container.Create()
if err != nil {
log.Println("ERROR update application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
err = container.Start()
if err != nil {
log.Println("ERROR update application problem: " + err.Error())
publish(appTemplate.Name, "backend problem", true)
return err
}
publish(appTemplate.Name, "updated", false)
return nil
}
// Delete one app
func deleteEventHandler(m *nats.Msg, message *RequestMessage) error {
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR: delete app:", err.Error())
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if err != nil {
log.Println("ERROR delete application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
if status != "no-container" {
// We stop the container first
err = container.Stop()
if err != nil {
log.Println("ERROR delete application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
// Then delete it
err = container.Delete()
if err != nil {
log.Println("ERROR delete application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
}
err = apps.Delete(app.Name)
if err != nil {
log.Println("ERROR delete application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "deleted", false)
return nil
}
// Stop existing app
func stopEventHandler(m *nats.Msg, message *RequestMessage) error {
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR stop application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if err != nil {
log.Println("ERROR stop application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
// Stop the container only when it exists
if status != "no-container" {
err = container.Stop()
if err != nil {
log.Println("ERROR stop application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
}
publish(app.Name, "stopped", false)
return nil
}
// Start existing app
func startEventHandler(m *nats.Msg, message *RequestMessage) error {
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR start application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if err != nil {
return err
}
if status == "no-container" {
err = container.Create()
if err != nil {
log.Println("ERROR start application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
}
err = container.Start()
if err != nil {
log.Println("ERROR start application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "started", false)
return nil
}
// Restart existing app
func restartEventHandler(m *nats.Msg, message *RequestMessage) error {
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR restart application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
err = container.Restart()
if err != nil {
log.Println("ERROR restart application problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "restarted", false)
return nil
}
// Copies body of the request into /srv/.ssh/authorized_keys
func updateKeysEventHandler(m *nats.Msg, message *RequestMessage) error {
err := waitForApp(message.AppName)
if err != nil {
log.Println("ERROR enable tech problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
body := message.Payload
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR keys update problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
err = container.SetFileContent(sshPubKeysLocation, body+"\n", "0600")
if err != nil {
log.Println("ERROR keys update problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "keys updated", false)
return nil
}
// Set password for the app user in the container
func setPasswordEventHandler(m *nats.Msg, message *RequestMessage) error {
err := waitForApp(message.AppName)
if err != nil {
log.Println("ERROR enable tech problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
password := message.Payload
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR password update problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
err = container.SetPassword(password)
if err != nil {
log.Println("ERROR password update problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "password updated", false)
return nil
}
// Application processes
func processesEventHandler(m *nats.Msg, message *RequestMessage) error {
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR processes list problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
processes, err := container.GetProcessList()
if err != nil {
log.Println("ERROR processes list problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
reply := ReplyMessage{
Payload: processes,
}
data, err := json.Marshal(reply)
if err != nil {
log.Println("ERROR processes list problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
err = m.Respond(data)
if err != nil {
log.Println("ERROR processes list problem: " + err.Error())
return err
}
return nil
}
// Enable one of the supported technologies or services (python, node, redis, ...)
func enableTechEventHandler(m *nats.Msg, message *RequestMessage) error {
service := message.Payload
err := waitForApp(message.AppName)
if err != nil {
log.Println("ERROR enable tech problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR enable tech problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
err = container.SetTechnology(service)
if err != nil {
log.Println("ERROR enable tech problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "tech updated", false)
return nil
}
// Rebuilds existing app, it keeps the data but creates the container again
func rebuildEventHandler(m *nats.Msg, message *RequestMessage) error {
app, err := apps.Get(message.AppName)
if err != nil {
log.Println("ERROR rebuild app problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
container := docker.Container{
App: app,
}
err = container.Destroy()
if err != nil && err.Error() == "no container found" {
// We don't care if the container didn't exist anyway
err = nil
}
if err != nil {
log.Println("ERROR rebuild app problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
err = container.Create()
if err != nil {
log.Println("ERROR rebuild app problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
err = container.Start()
if err != nil {
log.Println("ERROR rebuild app problem: " + err.Error())
publish(app.Name, "backend problem", true)
return err
}
publish(app.Name, "app rebuild", false)
return nil
}
// Adds new label
func addLabelEventHandler(m *nats.Msg, message *RequestMessage) error {
label := message.Payload
err := apps.AddLabel(message.AppName, label)
if err != nil {
log.Println("ERROR add label problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
publish(message.AppName, "label added", false)
return nil
}
// Removes existing label
func removeLabelEventHandler(m *nats.Msg, message *RequestMessage) error {
label := message.Payload
err := apps.RemoveLabel(message.AppName, label)
if err != nil {
log.Println("ERROR remove label problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
publish(message.AppName, "label removed", false)
return nil
}
// Orphans returns directories in /srv that doesn't match any hosted application
func listOrphansEventHandler(m *nats.Msg, message *RequestMessage) error {
reply := ReplyMessage{
Error: true,
Payload: "not implemented yet",
}
data, err := json.Marshal(reply)
if err != nil {
log.Println("ERROR orphans list problem: " + err.Error())
return err
}
err = m.Respond(data)
if err != nil {
log.Println("ERROR orphans list problem: " + err.Error())
return err
}
return nil
}
// Return info about the node including performance index
func getNoteEventHandler(m *nats.Msg, message *RequestMessage) error {
node, err := node.GetNodeInfo()
if err != nil {
log.Println("ERROR performance index problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
reply := ReplyMessage{
Payload: node,
}
data, err := json.Marshal(reply)
if err != nil {
log.Println("ERROR performance index problem: " + err.Error())
publish(message.AppName, "backend problem", true)
return err
}
err = m.Respond(data)
if err != nil {
log.Println("ERROR performance index problem: " + err.Error())
return err
}
return nil
}

33
main.go
View file

@ -1,10 +1,12 @@
package main
import (
"fmt"
"log"
"time"
"github.com/labstack/echo"
"github.com/nats-io/nats.go"
"github.com/rosti-cz/node-api/common"
"github.com/rosti-cz/node-api/node"
)
@ -12,7 +14,26 @@ import (
// JSONIndent Indendation of JSON output format
const JSONIndent = " "
var config common.Config
var nc *nats.Conn
func _init() {
var err error
// Load config from environment variables
config = *common.GetConfig()
// Connect to the NATS service
nc, err = nats.Connect(config.NATSURL)
if err != nil {
log.Fatalln(err)
}
}
func main() {
_init()
defer nc.Drain()
// Close database at the end
db := common.GetDBConnection()
defer db.Close()
@ -31,7 +52,7 @@ func main() {
}
elapsed := time.Since(start)
log.Printf("Stats gathering elapsed time: %.2fs\n", elapsed.Seconds())
time.Sleep(30 * time.Second)
time.Sleep(300 * time.Second)
}
}()
@ -52,6 +73,13 @@ func main() {
e.Use(TokenMiddleware)
// NATS handling
// admin.apps.ALIAS.events
// admin.apps.ALIAS.states
subjectEvents := fmt.Sprintf("admin.apps.%s.requests", config.NATSAlias)
log.Println("> listening on " + subjectEvents)
nc.Subscribe(subjectEvents, messageHandler)
// UI
e.GET("/", homeHandler)
@ -89,9 +117,10 @@ func main() {
// Copies body of the request into /srv/.ssh/authorized_keys
e.PUT("/v1/apps/:name/keys", setKeysHandler)
// Enable one of the supported technologies or services (python, node, redis, ...)
e.PUT("/v1/apps/:name/set-services", setServicesHandler)
// Rebuilds existing app, it keeps the data but created the container again
// Rebuilds existing app, it keeps the data but creates the container again
e.PUT("/v1/apps/:name/rebuild", rebuildAppHandler)
// Adds new label

91
tools.go Normal file
View file

@ -0,0 +1,91 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"time"
"github.com/nats-io/nats.go"
"github.com/rosti-cz/node-api/apps"
"github.com/rosti-cz/node-api/docker"
)
func errorReplyFormater(m *nats.Msg, message string, err error) error {
reply := ReplyMessage{
Error: true,
Payload: message,
}
log.Println("ERROR:", err.Error())
data, err := json.Marshal(reply)
if err != nil {
log.Println("ERROR:", err.Error())
return err
}
err = m.Respond(data)
if err != nil {
log.Println("ERROR:", err.Error())
return err
}
return err
}
func publish(appName string, state string, isErr bool) {
stateMessage := StateMessage{
AppName: appName,
Error: isErr,
Message: state,
}
data, err := stateMessage.JSON()
if err != nil {
log.Println("ERROR: publish:", err.Error())
}
subjectEvents := fmt.Sprintf("admin.apps.%s.states", config.NATSAlias)
err = nc.Publish(subjectEvents, data)
if err != nil {
log.Println("ERROR: publish:", err.Error())
}
}
// waitForApp waits until app is ready or timeout is reached.
// It's used in some async calls that need at least part of the
// environment prepared.
func waitForApp(appName string) error {
sleepFor := 5 * time.Second
loops := 6
for i := 0; i < loops; i++ {
err := updateState(appName)
if err != nil {
time.Sleep(sleepFor)
continue
}
app, err := apps.Get(appName)
if err != nil {
return err
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if status == "running" {
return nil
}
time.Sleep(sleepFor)
continue
}
return errors.New("timeout reached")
}

View file

@ -1,8 +1,34 @@
package main
import "encoding/json"
// Path where authorized keys are
const sshPubKeysLocation = "/srv/.ssh/authorized_keys"
// RequestMessage message
type RequestMessage struct {
AppName string `json:"name"`
Type string `json:"type"`
Payload string `json:"payload"`
}
type StateMessage struct {
AppName string `json:"name"` // Name of the related application stored in the main database
Error bool `json:"error"` // True if the message is an error message
Message string `json:"message"`
}
func (s *StateMessage) JSON() ([]byte, error) {
return json.Marshal(s)
}
// ReplyMessage is returned as reply to a message that requires replying
type ReplyMessage struct {
AppName string `json:"name,omitempty"`
Error bool `json:"error,omitempty"` // True if this message is an error message and in that case Payload is string with an error message, otherwise it's standard response
Payload interface{} `json:"payload"`
}
// Message represents response with information about results of something
type Message struct {
// Message with different kind of information. Usually it's error message generated by dependencies or stdlib or simply ok.