diff --git a/.gitignore b/.gitignore index 0a57693..8202cae 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node-api .history/ api-node-17.http *-packr.go +*.sqlite diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..046a38f --- /dev/null +++ b/.vscode/launch.json @@ -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", + }, + } + ] +} diff --git a/api.http b/api.http index d608399..d116b24 100644 --- a/api.http +++ b/api.http @@ -1,5 +1,5 @@ POST http://localhost:1323/v1/apps -Authorization: Token fee60059-f554-4c35-b44f-74b6be377095 +Authorization: Token ABCD Content-type: application/json { diff --git a/auth.go b/auth.go index 9476a33..ff06f58 100644 --- a/auth.go +++ b/auth.go @@ -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"}, " ") } diff --git a/common/config.go b/common/config.go index 41e7cfa..f7eee62 100644 --- a/common/config.go +++ b/common/config.go @@ -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 diff --git a/common/db.go b/common/db.go index fd33d2b..5eb2dea 100644 --- a/common/db.go +++ b/common/db.go @@ -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) } diff --git a/config.go b/config.go deleted file mode 100644 index ab6fa2f..0000000 --- a/config.go +++ /dev/null @@ -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 -} diff --git a/contrib/test_messages.sh b/contrib/test_messages.sh new file mode 100644 index 0000000..91b0a15 --- /dev/null +++ b/contrib/test_messages.sh @@ -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}}' diff --git a/go.mod b/go.mod index f414968..f52d9cc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index acd4abf..cb6e7b7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers.go b/handlers.go index a7c0e6c..8231cf5 100644 --- a/handlers.go +++ b/handlers.go @@ -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) } diff --git a/handlers_nats.go b/handlers_nats.go new file mode 100644 index 0000000..f4074c6 --- /dev/null +++ b/handlers_nats.go @@ -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 +} diff --git a/main.go b/main.go index a3e5fbc..a9f73af 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..eaffebd --- /dev/null +++ b/tools.go @@ -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") +} diff --git a/types.go b/types.go index 3844a9c..bf344e5 100644 --- a/types.go +++ b/types.go @@ -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.