commit f8b9c4f74810b59c4cbd854583a9617ee043fbf7 Author: Adam Štrauch Date: Sun Dec 8 02:30:07 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26ea98b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +tmp/ +__debug* +main diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..acea204 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,54 @@ +{ + // 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": "Master", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cli", + "env": { + "DUMP_PATH": "../tmp/nodes.json", + "CONFIG_PATH": "../tmp/config.json", + "NODE_DIR_PATH": "../tmp/node", + "TOKEN": "abcd" + }, + "args": [ + "master" + ] + }, + { + "name": "Node", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cli", + "env": { + "DUMP_PATH": "../tmp/nodes.json", + "CONFIG_PATH": "../tmp/config.json", + "NODE_DIR_PATH": "../tmp/node" + }, + "args": [ + "node" + ] + }, + { + "name": "Print", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cli", + "env": { + "DUMP_PATH": "../tmp/nodes.json", + "CONFIG_PATH": "../tmp/config.json", + "NODE_DIR_PATH": "../tmp/node" + }, + "args": [ + "print" + ] + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d29ef8 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Lobby 2 - simple service discovery + +This is second version of my Lobby projects that doesn't require NATS. All clients uses single service discovery server that keeps track of their presence. + +Each instance, except its liveness can share labels which describe what's hosted on this instance and that can used by others instances for various things. There is also a simple KV store for additional info. diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..cbfd07f --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,9 @@ +# https://taskfile.dev + +version: '3' + +tasks: + docs: + cmds: + - swag i --parseDependency --dir api + silent: true diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 0000000..d0b2106 --- /dev/null +++ b/api/auth.go @@ -0,0 +1,41 @@ +package api + +import ( + "net/http" + "strings" + + "github.com/labstack/echo/v4" +) + +func tokenMiddlware(configuredToken string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Ignore token check for swagger URLs + if strings.HasPrefix(c.Request().URL.Path, "/swagger") || c.Request().URL.Path == "/" { + return next(c) + } + + // Check for token in the Authorization header + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "please provide valid token") + } + + // The Authorization header should be in the format "Bearer " + parts := strings.Split(authHeader, " ") + if len(parts) == 1 && parts[0] == configuredToken { + return next(c) + } + + if len(parts) != 2 || parts[0] != "Bearer" { + return echo.NewHTTPError(http.StatusUnauthorized, "please provide valid token") + } + + if parts[1] != configuredToken { + return echo.NewHTTPError(http.StatusUnauthorized, "please provide valid token") + } + + return next(c) + } + } +} diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..a84f08f --- /dev/null +++ b/api/main.go @@ -0,0 +1,113 @@ +package api + +import ( + "log" + "net/http" + + _ "gitea.ceperka.net/rosti/lobby2/docs" // This line is necessary for swag to find your docs! + "gitea.ceperka.net/rosti/lobby2/nodes" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + echoSwagger "github.com/swaggo/echo-swagger" +) + +// @title Lobby2 API +// @version 2.0 +// @description API of Lobby 2 project that helps to discover and connect to other nodes and their services. +// @BasePath / +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization + +type API struct { + listen string + token string + np *nodes.NodesProcessor + + e *echo.Echo +} + +func NewAPI(np *nodes.NodesProcessor, listen string, token string) *API { + return &API{ + listen: listen, + token: token, + np: np, + } +} + +func (a *API) Run() error { + if a.token == "" { + log.Fatalln("TOKEN is required") + } + + a.e = echo.New() + + a.e.Use(middleware.Logger()) + a.e.Use(tokenMiddlware(a.token)) + + a.e.GET("/", func(c echo.Context) error { + return c.Redirect(http.StatusTemporaryRedirect, "/swagger/index.html") + }) + a.e.GET("/swagger/*", echoSwagger.WrapHandler) + + a.e.GET("/nodes", a.listHandler) + a.e.GET("/nodes/:hostname", a.getHandler) + a.e.POST("/nodes/:hostname", a.refreshHandler) + + // Start the server in a goroutine so that it doesn't block the signal listening + return a.e.Start(a.listen) +} + +// @Summary List of nodes +// @Description List of all discovered nodes and their labels. +// @Produce application/json +// @Success 200 {object} nodes.Nodes "List of nodes" +// @Failure 401 {object} Message "Forbidden access" +// @Security Bearer +// @Router /nodes [get] +func (a *API) listHandler(c echo.Context) error { + return c.JSON(http.StatusOK, a.np.List()) +} + +// @Summary Get node +// @Description Return one nodes based on given hostname +// @Produce application/json +// @Param hostname path string true "Node hostname" +// @Success 200 {array} nodes.Node "Node details" +// @Failure 401 {object} Message "Forbidden access" +// @Security Bearer +// @Router /nodes/{hostname} [get] +func (a *API) getHandler(c echo.Context) error { + hostname := c.Param("hostname") + + node, ok := a.np.Get(hostname) + if !ok { + return echo.NewHTTPError(http.StatusNotFound, "node not found") + } + + return c.JSON(http.StatusOK, node) +} + +// @Summary Refresh node +// @Description Send new data or update existing data about a node +// @Produce application/json +// @Param hostname path string true "Node hostname" +// @Param labels body nodes.Labels true "Node labels" +// @Param kv body nodes.KV true "Key-value" +// @Success 200 {array} nodes.Node "Node details" +// @Failure 401 {object} Message "Forbidden access" +// @Security Bearer +// @Router /nodes/{hostname} [post] +func (a *API) refreshHandler(c echo.Context) error { + hostname := c.Param("hostname") + + params := params{} + err := c.Bind(¶ms) + if err != nil { + return c.JSON(http.StatusBadRequest, Message{Message: err.Error()}) + } + + a.np.Refresh(hostname, params.Labels, params.KV) + + return c.NoContent(http.StatusNoContent) +} diff --git a/api/types.go b/api/types.go new file mode 100644 index 0000000..42eb56d --- /dev/null +++ b/api/types.go @@ -0,0 +1,12 @@ +package api + +import "gitea.ceperka.net/rosti/lobby2/nodes" + +type Message struct { + Message string `json:"message"` +} + +type params struct { + Labels nodes.Labels `json:"labels"` + KV nodes.KV `json:"kv"` +} diff --git a/cli/actions.go b/cli/actions.go new file mode 100644 index 0000000..5336439 --- /dev/null +++ b/cli/actions.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "gitea.ceperka.net/rosti/lobby2/api" + "gitea.ceperka.net/rosti/lobby2/nodes" + "gitea.ceperka.net/rosti/lobby2/refresher" + "github.com/urfave/cli/v2" +) + +func masterAction(c *cli.Context) error { + cfg := GetConfig() + + np := nodes.NewNodesProcessor(cfg.DumpPath, cfg.DropAfterSeconds) + + go np.DumpLoop() + go np.GarbageCollection() + + api := api.NewAPI(np, cfg.APIListen, cfg.APIToken) + api.Run() + + return nil +} + +func nodeAction(c *cli.Context) error { + cfg := GetConfig() + + r := refresher.NewRefresher(cfg.NodeDirPath, cfg.ConfigPath) + r.Loop() + + return nil +} + +func printAction(c *cli.Context) error { + cfg := GetConfig() + + np := nodes.NewNodesProcessor(cfg.DumpPath, cfg.DropAfterSeconds) + + nodes := np.List() + for _, node := range nodes { + body, err := json.MarshalIndent(node, "", " ") + if err != nil { + fmt.Printf("failed to marshal node: %v\n", err) + os.Exit(1) + } + + fmt.Println(string(body)) + } + + return nil +} diff --git a/cli/config.go b/cli/config.go new file mode 100644 index 0000000..ccf4299 --- /dev/null +++ b/cli/config.go @@ -0,0 +1,28 @@ +package main + +import ( + "log" + + "github.com/kelseyhightower/envconfig" +) + +type Config struct { + APIListen string `envconfig:"LISTEN" default:"0.0.0.0:1352"` + APIToken string `envconfig:"TOKEN" default:""` + + DumpPath string `envconfig:"DUMP_PATH" default:"/var/lib/lobby2/nodes.json"` + ConfigPath string `envconfig:"CONFIG_PATH" default:"/var/lib/lobby2/config.json"` + NodeDirPath string `envconfig:"NODE_DIR_PATH" default:"/var/lib/lobby2/node"` + + DropAfterSeconds int64 `envconfig:"DROP_AFTER_SECONDS" default:"60"` +} + +func GetConfig() Config { + var cfg Config + err := envconfig.Process("", &cfg) + if err != nil { + log.Fatal(err) + } + + return cfg +} diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..5457dd3 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "", + Usage: "", + Commands: []*cli.Command{ + { + Name: "master", + Usage: "Runs master node API", + Action: masterAction, + }, + { + Name: "node", + Usage: "Runs node on local machine", + Action: nodeAction, + }, + { + Name: "print", + Usage: "Prints all discovered nodes", + Action: printAction, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..b1670d7 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,209 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/nodes": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List of all discovered nodes and their labels.", + "produces": [ + "application/json" + ], + "summary": "List of nodes", + "responses": { + "200": { + "description": "List of nodes", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/nodes.Node" + } + } + }, + "401": { + "description": "Forbidden access", + "schema": { + "$ref": "#/definitions/api.Message" + } + } + } + } + }, + "/nodes/{hostname}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Return one nodes based on given hostname", + "produces": [ + "application/json" + ], + "summary": "Get node", + "parameters": [ + { + "type": "string", + "description": "Node hostname", + "name": "hostname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Node details", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/nodes.Node" + } + } + }, + "401": { + "description": "Forbidden access", + "schema": { + "$ref": "#/definitions/api.Message" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Send new data or update existing data about a node", + "produces": [ + "application/json" + ], + "summary": "Refresh node", + "parameters": [ + { + "type": "string", + "description": "Node hostname", + "name": "hostname", + "in": "path", + "required": true + }, + { + "description": "Node labels", + "name": "labels", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Key-value", + "name": "kv", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/nodes.KV" + } + } + ], + "responses": { + "200": { + "description": "Node details", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/nodes.Node" + } + } + }, + "401": { + "description": "Forbidden access", + "schema": { + "$ref": "#/definitions/api.Message" + } + } + } + } + } + }, + "definitions": { + "api.Message": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "nodes.KV": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nodes.Node": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "kv": { + "$ref": "#/definitions/nodes.KV" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "last_update": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "2.0", + Host: "", + BasePath: "/", + Schemes: []string{}, + Title: "Lobby2 API", + Description: "API of Lobby 2 project that helps to discover and connect to other nodes and their services.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..e5dba6b --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,185 @@ +{ + "swagger": "2.0", + "info": { + "description": "API of Lobby 2 project that helps to discover and connect to other nodes and their services.", + "title": "Lobby2 API", + "contact": {}, + "version": "2.0" + }, + "basePath": "/", + "paths": { + "/nodes": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List of all discovered nodes and their labels.", + "produces": [ + "application/json" + ], + "summary": "List of nodes", + "responses": { + "200": { + "description": "List of nodes", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/nodes.Node" + } + } + }, + "401": { + "description": "Forbidden access", + "schema": { + "$ref": "#/definitions/api.Message" + } + } + } + } + }, + "/nodes/{hostname}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Return one nodes based on given hostname", + "produces": [ + "application/json" + ], + "summary": "Get node", + "parameters": [ + { + "type": "string", + "description": "Node hostname", + "name": "hostname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Node details", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/nodes.Node" + } + } + }, + "401": { + "description": "Forbidden access", + "schema": { + "$ref": "#/definitions/api.Message" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Send new data or update existing data about a node", + "produces": [ + "application/json" + ], + "summary": "Refresh node", + "parameters": [ + { + "type": "string", + "description": "Node hostname", + "name": "hostname", + "in": "path", + "required": true + }, + { + "description": "Node labels", + "name": "labels", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Key-value", + "name": "kv", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/nodes.KV" + } + } + ], + "responses": { + "200": { + "description": "Node details", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/nodes.Node" + } + } + }, + "401": { + "description": "Forbidden access", + "schema": { + "$ref": "#/definitions/api.Message" + } + } + } + } + } + }, + "definitions": { + "api.Message": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "nodes.KV": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nodes.Node": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "kv": { + "$ref": "#/definitions/nodes.KV" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "last_update": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..69e6d8c --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,119 @@ +basePath: / +definitions: + api.Message: + properties: + message: + type: string + type: object + nodes.KV: + additionalProperties: + type: string + type: object + nodes.Node: + properties: + hostname: + type: string + kv: + $ref: '#/definitions/nodes.KV' + labels: + items: + type: string + type: array + last_update: + type: integer + type: object +info: + contact: {} + description: API of Lobby 2 project that helps to discover and connect to other + nodes and their services. + title: Lobby2 API + version: "2.0" +paths: + /nodes: + get: + description: List of all discovered nodes and their labels. + produces: + - application/json + responses: + "200": + description: List of nodes + schema: + items: + $ref: '#/definitions/nodes.Node' + type: array + "401": + description: Forbidden access + schema: + $ref: '#/definitions/api.Message' + security: + - Bearer: [] + summary: List of nodes + /nodes/{hostname}: + get: + description: Return one nodes based on given hostname + parameters: + - description: Node hostname + in: path + name: hostname + required: true + type: string + produces: + - application/json + responses: + "200": + description: Node details + schema: + items: + $ref: '#/definitions/nodes.Node' + type: array + "401": + description: Forbidden access + schema: + $ref: '#/definitions/api.Message' + security: + - Bearer: [] + summary: Get node + post: + description: Send new data or update existing data about a node + parameters: + - description: Node hostname + in: path + name: hostname + required: true + type: string + - description: Node labels + in: body + name: labels + required: true + schema: + items: + type: string + type: array + - description: Key-value + in: body + name: kv + required: true + schema: + $ref: '#/definitions/nodes.KV' + produces: + - application/json + responses: + "200": + description: Node details + schema: + items: + $ref: '#/definitions/nodes.Node' + type: array + "401": + description: Forbidden access + schema: + $ref: '#/definitions/api.Message' + security: + - Bearer: [] + summary: Refresh node +securityDefinitions: + Bearer: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e829c66 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module gitea.ceperka.net/rosti/lobby2 + +go 1.20 + +require ( + github.com/kelseyhightower/envconfig v1.4.0 + github.com/labstack/echo/v4 v4.12.0 + github.com/puzpuzpuz/xsync/v3 v3.4.0 + github.com/stretchr/testify v1.8.4 + github.com/swaggo/echo-swagger v1.4.1 + github.com/swaggo/swag v1.16.3 + github.com/urfave/cli/v2 v2.3.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.7.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f25a670 --- /dev/null +++ b/go.sum @@ -0,0 +1,111 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= +github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nodes/main.go b/nodes/main.go new file mode 100644 index 0000000..3ac4bc4 --- /dev/null +++ b/nodes/main.go @@ -0,0 +1,176 @@ +package nodes + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path" + "strings" + "time" + + "github.com/puzpuzpuz/xsync/v3" +) + +const dumpIntervalSeconds = 120 + +// Base nodes structure that covers all operations above nodes. +type NodesProcessor struct { + nodes *xsync.MapOf[string, Node] + dumpPath string + garbageCollectAfterSeconds int64 +} + +func NewNodesProcessor(dumpPath string, garbageCollectAfterSeconds int64) *NodesProcessor { + p := &NodesProcessor{ + dumpPath: dumpPath, + garbageCollectAfterSeconds: garbageCollectAfterSeconds, + nodes: xsync.NewMapOf[string, Node](), + } + + err := p.Load() + if err != nil { + log.Printf("failed to load nodes: %v\n", err) + } + + return p +} + +func (np *NodesProcessor) DumpLoop() { + log.Println(".. starting dump loop") + time.Sleep(time.Second * dumpIntervalSeconds) + for { + log.Println(".. dumping nodes") + err := np.Dump() + if err != nil { + log.Printf("failed to dump nodes: %v\n", err) + } + + time.Sleep(time.Second * dumpIntervalSeconds) + } +} + +// GarbageCollection removes nodes that haven't been updated for more than garbageCollectAfterSeconds. +func (np *NodesProcessor) GarbageCollection() { + for { + np.garbageCollect() + time.Sleep(time.Second * 10) + } +} + +func (np *NodesProcessor) garbageCollect() { + np.nodes.Range(func(key string, value Node) bool { + if time.Now().Unix()-value.LastUpdate > np.garbageCollectAfterSeconds { + np.nodes.Delete(value.HostName) + } + return true + }) +} + +// Dumps content of np.nodes to np.dumpPath. +func (np *NodesProcessor) Dump() error { + ns := []Node{} + np.nodes.Range(func(key string, value Node) bool { + ns = append(ns, value) + return true + }) + + body, err := json.Marshal(ns) + if err != nil { + return fmt.Errorf("failed to marshal nodes: %w", err) + } + + // Create directory if it doesn't exist. + if strings.Contains(np.dumpPath, "/") { + path := np.dumpPath[:strings.LastIndex(np.dumpPath, "/")] + + if len(path) != 0 { + err = os.MkdirAll(path, 0755) + if err != nil { + return fmt.Errorf("failed to create directory for nodes.json: %w", err) + } + } + } + + err = os.WriteFile(np.dumpPath, body, 0644) + if err != nil { + return fmt.Errorf("failed to write nodes.json: %w", err) + } + + return nil +} + +// Loads content of np.dumpPath to np.nodes. +func (np *NodesProcessor) Load() error { + filePath := path.Join(np.dumpPath) + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return nil + } + + body, err := os.ReadFile(np.dumpPath) + if err != nil { + return fmt.Errorf("failed to read nodes.json: %w", err) + } + + np.Reset() + + ns := []Node{} + + err = json.Unmarshal(body, &ns) + if err != nil { + return fmt.Errorf("failed to unmarshal nodes: %w", err) + } + + for _, n := range ns { + np.nodes.Store(n.HostName, n) + } + + return nil +} + +func (np *NodesProcessor) Reset() { + np.nodes.Clear() +} + +// Returns string with Prometheus metrics +// TODO: finish this +func (np *NodesProcessor) GetMetrics() string { + return "" +} + +// List returns all nodes. +func (np *NodesProcessor) List() Nodes { + nodes := Nodes{} + + np.nodes.Range(func(key string, value Node) bool { + nodes = append(nodes, value) + return true + }) + + return nodes +} + +// Get returns a node by hostname. +func (np *NodesProcessor) Get(hostname string) (Node, bool) { + node, ok := np.nodes.Load(hostname) + return node, ok +} + +// Refresh updates node with hostname and labels. If it doesn't exists it's created. +func (np *NodesProcessor) Refresh(hostname string, labels Labels, kv KV) { + node, ok := np.nodes.Load(hostname) + if !ok { + node = Node{} + } + node.LastUpdate = time.Now().Unix() + node.HostName = hostname + node.Labels = labels + node.KV = kv + np.nodes.Store(hostname, node) +} + +// Drop removes node with given hostname. +func (np *NodesProcessor) Drop(hostname string) { + np.nodes.Delete(hostname) +} diff --git a/nodes/main_test.go b/nodes/main_test.go new file mode 100644 index 0000000..4c1a907 --- /dev/null +++ b/nodes/main_test.go @@ -0,0 +1,92 @@ +package nodes + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var np *NodesProcessor + +const testDumpFile = "nodes.json" + +func TestMain(m *testing.M) { + np = NewNodesProcessor(testDumpFile, 2) + + os.Exit(m.Run()) +} + +func TestNodesProcessor_GarbageCollect(t *testing.T) { + np.Refresh("test", Labels{"mylabel"}, KV{"mykey": "my value"}) + assert.Equal(t, 1, len(np.List())) + time.Sleep(5 * time.Second) + np.garbageCollect() + assert.Equal(t, 0, len(np.List())) +} + +func TestNodesProcessor_Refresh(t *testing.T) { + np.Reset() + + np.Refresh("test", Labels{"mylabel"}, KV{"mykey": "my value"}) + np.Refresh("test2", Labels{"mylabel3"}, KV{"mykey3": "my value3"}) + + nodes := np.List() + assert.Equal(t, "test", nodes[0].HostName) + assert.Contains(t, nodes[0].Labels, "mylabel") + assert.Equal(t, "test2", nodes[1].HostName) + assert.Contains(t, nodes[1].Labels, "mylabel3") +} + +func TestNodesProcessor_DumpLoad(t *testing.T) { + np.Reset() + + np.Refresh("test", Labels{"mylabel"}, KV{"mykey": "my value"}) + np.Refresh("test2", Labels{"mylabel3"}, KV{"mykey3": "my value3"}) + + os.Remove(testDumpFile) + + np.Dump() + np.Reset() + assert.Equal(t, 0, len(np.List())) + + np.Load() + assert.Equal(t, 2, len(np.List())) + + nodes := np.List() + assert.Len(t, nodes, 2) +} + +func TestNodesProcessor_List(t *testing.T) { + np.Reset() + + np.Refresh("test", Labels{"mylabel"}, KV{"mykey": "my value"}) + nodes := np.List() + assert.Equal(t, "test", nodes[0].HostName) + assert.Contains(t, nodes[0].Labels, "mylabel") + +} + +func TestNodesProcessor_Get(t *testing.T) { + np.Reset() + np.Refresh("test", Labels{"mylabel"}, KV{"mykey": "my value"}) + node, _ := np.Get("test") + + assert.Contains(t, node.Labels, "mylabel") + +} + +func TestNodesProcessor_Drop(t *testing.T) { + np.Reset() + np.Refresh("test", Labels{"mylabel"}, KV{"mykey": "my value"}) + node, _ := np.Get("test") + + assert.Contains(t, node.Labels, "mylabel") + + np.Drop("test") + assert.Zero(t, len(np.List())) + +} + +// TODO: TestNodesProcessor_GetMetrics diff --git a/nodes/types.go b/nodes/types.go new file mode 100644 index 0000000..c8830f5 --- /dev/null +++ b/nodes/types.go @@ -0,0 +1,14 @@ +package nodes + +type KV map[string]string +type Labels []string +type JSONData map[string]interface{} + +type Node struct { + LastUpdate int64 `json:"last_update,omitempty"` + HostName string `json:"hostname"` + Labels Labels `json:"labels,omitempty"` + KV KV `json:"kv,omitempty"` +} + +type Nodes []Node diff --git a/refresher/main.go b/refresher/main.go new file mode 100644 index 0000000..849eae1 --- /dev/null +++ b/refresher/main.go @@ -0,0 +1,178 @@ +package refresher + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path" + "time" + + "gitea.ceperka.net/rosti/lobby2/nodes" +) + +const refreshIntervalSeconds = 15 +const nodeFileName = "node.json" + +// Refresher loads local node info and sends them to the master node. +type Refresher struct { + configPath string + nodeDirPath string +} + +func NewRefresher(nodeDirPath string, configPath string) *Refresher { + return &Refresher{ + nodeDirPath: nodeDirPath, + configPath: configPath, + } +} + +// Run loop the process that updates node in the master node. +func (r *Refresher) Loop() { + log.Println("Start refresher loop") + + for { + log.Println("Refreshing node") + err := r.Refresh() + if err != nil { + log.Printf("failed to refresh node: %v\n", err) + } + time.Sleep(time.Second * refreshIntervalSeconds) + } +} + +// Refresh loads labels from the local filesystem and sends them to the master node. +func (r *Refresher) Refresh() error { + // Load config + cfg, err := r.getConfig() + if err != nil { + return err + } + + // Load labels + node, err := r.loadNode() + if err != nil { + return err + } + + nodeBytes, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal labels: %w", err) + } + + // Send labels to master + req, err := http.NewRequest("POST", fmt.Sprintf("%s://%s:%d/nodes/%s", cfg.MasterProto, cfg.MasterHost, cfg.MasterPort, node.HostName), bytes.NewBuffer(nodeBytes)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+cfg.MasterToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("unexpected status: %s", resp.Status) + } + + return nil + +} + +// Returns the node config. +func (r *Refresher) getConfig() (NodeConfig, error) { + cfg := NodeConfig{} + + content, err := os.ReadFile(r.configPath) + if err != nil { + return cfg, fmt.Errorf("failed to read config file: %w", err) + } + + err = json.Unmarshal(content, &cfg) + if err != nil { + return cfg, fmt.Errorf("failed to unmarshal config: %w", err) + } + + if cfg.MasterProto == "" { + cfg.MasterProto = "http" + } + + if cfg.MasterPort == 0 { + cfg.MasterPort = 1352 + } + + return cfg, nil +} + +// TODO: rewrite this to load Node structure +func (r *Refresher) loadNode() (*nodes.Node, error) { + filePath := path.Join(r.nodeDirPath, nodeFileName) + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + err = r.initNodeFile() + if err != nil { + return nil, err + } + } + + content, err := os.ReadFile(path.Join(r.nodeDirPath, nodeFileName)) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + node := &nodes.Node{} + err = json.Unmarshal(content, &node) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal node: %w", err) + } + + return node, err +} + +func (r *Refresher) initNodeFile() error { + err := r.createNodePath() + if err != nil { + return fmt.Errorf("failed to create node path: %w", err) + } + + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed to get hostname: %w", err) + } + + node := &nodes.Node{ + HostName: hostname, + } + + nodeBytes, err := json.MarshalIndent(node, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal node: %w", err) + } + + err = os.WriteFile(path.Join(r.nodeDirPath, nodeFileName), nodeBytes, 0640) + if err != nil { + return fmt.Errorf("failed to write node file: %w", err) + } + + return nil +} + +func (r *Refresher) createNodePath() error { + _, err := os.Stat(r.nodeDirPath) + if os.IsNotExist(err) { + err = os.MkdirAll(r.nodeDirPath, 0755) + if err != nil { + return fmt.Errorf("failed to create node dir: %w", err) + } + } + + return nil + +} diff --git a/refresher/types.go b/refresher/types.go new file mode 100644 index 0000000..91900d3 --- /dev/null +++ b/refresher/types.go @@ -0,0 +1,8 @@ +package refresher + +type NodeConfig struct { + MasterHost string `json:"master_host"` + MasterProto string `json:"master_proto"` + MasterToken string `json:"master_token"` + MasterPort int `json:"master_port"` +}