Initial commit
This commit is contained in:
commit
f8b9c4f748
20 changed files with 1494 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
tmp/
|
||||
__debug*
|
||||
main
|
54
.vscode/launch.json
vendored
Normal file
54
.vscode/launch.json
vendored
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
5
README.md
Normal file
5
README.md
Normal file
|
@ -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.
|
9
Taskfile.yml
Normal file
9
Taskfile.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
docs:
|
||||
cmds:
|
||||
- swag i --parseDependency --dir api
|
||||
silent: true
|
41
api/auth.go
Normal file
41
api/auth.go
Normal file
|
@ -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 <token>"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
113
api/main.go
Normal file
113
api/main.go
Normal file
|
@ -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)
|
||||
}
|
12
api/types.go
Normal file
12
api/types.go
Normal file
|
@ -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"`
|
||||
}
|
54
cli/actions.go
Normal file
54
cli/actions.go
Normal file
|
@ -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
|
||||
}
|
28
cli/config.go
Normal file
28
cli/config.go
Normal file
|
@ -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
|
||||
}
|
37
cli/main.go
Normal file
37
cli/main.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
209
docs/docs.go
Normal file
209
docs/docs.go
Normal file
|
@ -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)
|
||||
}
|
185
docs/swagger.json
Normal file
185
docs/swagger.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
119
docs/swagger.yaml
Normal file
119
docs/swagger.yaml
Normal file
|
@ -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"
|
46
go.mod
Normal file
46
go.mod
Normal file
|
@ -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
|
||||
)
|
111
go.sum
Normal file
111
go.sum
Normal file
|
@ -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=
|
176
nodes/main.go
Normal file
176
nodes/main.go
Normal file
|
@ -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)
|
||||
}
|
92
nodes/main_test.go
Normal file
92
nodes/main_test.go
Normal file
|
@ -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
|
14
nodes/types.go
Normal file
14
nodes/types.go
Normal file
|
@ -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
|
178
refresher/main.go
Normal file
178
refresher/main.go
Normal file
|
@ -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
|
||||
|
||||
}
|
8
refresher/types.go
Normal file
8
refresher/types.go
Normal file
|
@ -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"`
|
||||
}
|
Loading…
Reference in a new issue