mirror of
https://github.com/akyaiy/GoSally-mvp.git
synced 2026-01-03 21:12:25 +00:00
Compare commits
18 Commits
v0.0.1-dev
...
deef4a891b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deef4a891b | ||
|
|
aa45a2e3e4 | ||
|
|
29d9f0487b | ||
|
|
ab8da05367 | ||
|
|
53572e4628 | ||
|
|
5bc334fd2c | ||
|
|
e71d69f3f1 | ||
|
|
cbc9a554df | ||
|
|
3e03c39644 | ||
|
|
9919f77c90 | ||
|
|
8d01314ded | ||
|
|
d78510e2e4 | ||
|
|
f90062efa7 | ||
|
|
90c7b1cd70 | ||
|
|
66f3d12412 | ||
|
|
2fdc32ce9f | ||
|
|
b70819e976 | ||
|
|
7093183140 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
bak/
|
||||
bin/
|
||||
cert/
|
||||
cert/
|
||||
tmp/
|
||||
.meta/
|
||||
.vscode
|
||||
|
||||
config.yaml
|
||||
12
Makefile
12
Makefile
@@ -2,6 +2,8 @@ APP_NAME := node
|
||||
BIN_DIR := bin
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
export CONFIG_PATH := ./config.yaml
|
||||
export NODE_PATH := $(shell pwd)
|
||||
|
||||
LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/core/config.NodeVersion=v0.0.1-dev'
|
||||
CGO_CFLAGS := -I/usr/local/include
|
||||
CGO_LDFLAGS := -L/usr/local/lib -llua5.1 -lm -ldl
|
||||
@@ -28,15 +30,19 @@ build:
|
||||
@# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'"
|
||||
@# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'"
|
||||
@# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)"
|
||||
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./cmd/$(APP_NAME)
|
||||
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./
|
||||
|
||||
run: build
|
||||
@echo "Running!"
|
||||
./$(BIN_DIR)/$(APP_NAME)
|
||||
exec ./$(BIN_DIR)/$(APP_NAME)
|
||||
|
||||
runq: build
|
||||
@echo "Running!"
|
||||
./$(BIN_DIR)/$(APP_NAME) | jq
|
||||
exec ./$(BIN_DIR)/$(APP_NAME) | jq
|
||||
|
||||
pure-run:
|
||||
@echo "Running!"
|
||||
exec ./$(BIN_DIR)/$(APP_NAME)
|
||||
|
||||
test:
|
||||
@go test ./... | grep -v '^?' || true
|
||||
|
||||
26
README.md
26
README.md
@@ -4,7 +4,7 @@
|
||||
System that allows you to build your own infrastructure based on identical nodes and various scripts written using built-in Lua 5.1, shebang scripts (scripts that start with the `#!` symbols), compiled binaries.
|
||||
|
||||
### Features
|
||||
Go Sally is not viable at the moment, but it already has the ability to run embedded scripts, log slog events to stdout, and handle RPC like requests.
|
||||
Go Sally is not viable at the moment, but it already has the ability to run embedded scripts, log slog events to stdout, handle RPC like requests, and independent automatic update from the repository (my pride, to be honest).
|
||||
|
||||
### Example of use
|
||||
The basic directory tree looks something like this
|
||||
@@ -13,20 +13,21 @@ The basic directory tree looks something like this
|
||||
├── bin
|
||||
│ └── node Node core binary file
|
||||
├── com
|
||||
│ ├── echo.lua
|
||||
│ ├── _globals.lua Declaring global variables and functions for all internal scripts (also required for luarc to work correctly)
|
||||
│ └── _prepare.lua Script that is executed before each script launch
|
||||
└── Makefile
|
||||
│ ├── echo.lua
|
||||
│ ├── _globals.lua Declaring global variables and functions for all internal scripts (also required for luarc to work correctly)
|
||||
│ └── _prepare.lua Script that is executed before each script launch
|
||||
└── config.yaml
|
||||
|
||||
3 directories, 5 files
|
||||
|
||||
3 directories, 4 files
|
||||
```
|
||||
Launch by command
|
||||
```bash
|
||||
$ make run
|
||||
./bin/node run
|
||||
```
|
||||
or for structured logs
|
||||
```bash
|
||||
$ make run
|
||||
./bin/node run | jq
|
||||
```
|
||||
|
||||
Example of GET request to server
|
||||
@@ -36,9 +37,14 @@ curl -s http://localhost:8080/api/v1/com/echo?msg=Hello
|
||||
Then the response from the server
|
||||
```json
|
||||
{
|
||||
"answer": "Hello",
|
||||
"status": "ok"
|
||||
"ResponsibleAgentUUID": "4593a87000bbe088f4e79c477e9c90d3",
|
||||
"RequestedCommand": "echo",
|
||||
"Response": {
|
||||
"answer": "Hello",
|
||||
"status": "ok"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### How to install
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/net/netutil"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
gs "github.com/akyaiy/GoSally-mvp/core/general_server"
|
||||
"github.com/akyaiy/GoSally-mvp/core/logs"
|
||||
"github.com/akyaiy/GoSally-mvp/core/sv1"
|
||||
"github.com/akyaiy/GoSally-mvp/core/update"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
var log *slog.Logger
|
||||
var cfg *config.ConfigConf
|
||||
|
||||
func init() {
|
||||
cfg = config.MustLoadConfig()
|
||||
|
||||
log = logs.SetupLogger(cfg.Mode)
|
||||
log = log.With("mode", cfg.Mode)
|
||||
|
||||
log.Info("Initializing server", slog.String("address", cfg.HTTPServer.Address))
|
||||
log.Debug("Server running in debug mode")
|
||||
}
|
||||
|
||||
func main() {
|
||||
updater := update.NewUpdater(*log, cfg)
|
||||
versuion, versionType, _ := updater.GetCurrentVersion()
|
||||
fmt.Printf("Current version: %s (%s)\n", versuion, versionType)
|
||||
ver, vert, _ := updater.GetLatestVersion(versionType)
|
||||
fmt.Printf("Latest version: %s (%s)\n", ver, vert)
|
||||
|
||||
fmt.Println("Checking for updates...")
|
||||
isNewUpdate, _ := updater.CkeckUpdates()
|
||||
fmt.Println("Update check result:", isNewUpdate)
|
||||
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
|
||||
Log: *log,
|
||||
Config: cfg,
|
||||
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`),
|
||||
ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`),
|
||||
Ver: "v1",
|
||||
})
|
||||
s := gs.InitGeneral(&gs.GeneralServerInit{
|
||||
Log: *log,
|
||||
Config: cfg,
|
||||
}, serverv1)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Route(config.GetServerConsts().GetApiRoute()+config.GetServerConsts().GetComDirRoute(), func(r chi.Router) {
|
||||
r.Get("/", s.HandleList)
|
||||
r.Get("/{cmd}", s.Handle)
|
||||
})
|
||||
r.Route("/favicon.ico", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
r.NotFound(serverv1.ErrNotFound)
|
||||
|
||||
address := cfg.Address
|
||||
if cfg.TlsEnabled {
|
||||
log.Info("HTTPS server started with TLS", slog.String("address", address))
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Error("Failed to start TLS listener", slog.String("error", err.Error()))
|
||||
return
|
||||
}
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
err = http.ServeTLS(limitedListener, r, cfg.CertFile, cfg.KeyFile)
|
||||
if err != nil {
|
||||
log.Error("Failed to start HTTPS server", slog.String("error", err.Error()))
|
||||
}
|
||||
} else {
|
||||
log.Info("HTTP server started", slog.String("address", address))
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Error("Failed to start listener", slog.String("error", err.Error()))
|
||||
return
|
||||
}
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
err = http.Serve(limitedListener, r)
|
||||
if err != nil {
|
||||
log.Error("Failed to start HTTP server", slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
34
cmd/root.go
Normal file
34
cmd/root.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/logs"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var compositor *config.Compositor = config.NewCompositor()
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Go Sally node",
|
||||
Long: "Main node runner for Go Sally",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady)))
|
||||
log.SetFlags(log.Ldate | log.Ltime)
|
||||
compositor.LoadCMDLine(rootCmd)
|
||||
_ = rootCmd.Execute()
|
||||
// if err := rootCmd.Execute(); err != nil {
|
||||
// log.Fatalf("Unexpected error: %s", err.Error())
|
||||
// }
|
||||
}
|
||||
326
cmd/run.go
Normal file
326
cmd/run.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/app"
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
gs "github.com/akyaiy/GoSally-mvp/core/general_server"
|
||||
"github.com/akyaiy/GoSally-mvp/core/logs"
|
||||
"github.com/akyaiy/GoSally-mvp/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/core/sv1"
|
||||
"github.com/akyaiy/GoSally-mvp/core/update"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/netutil"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run node normally",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
nodeApp := app.New()
|
||||
|
||||
nodeApp.InitialHooks(
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Config = compositor
|
||||
x.Log.SetOutput(os.Stdout)
|
||||
x.Log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
x.Log.SetFlags(log.Ldate | log.Ltime)
|
||||
},
|
||||
|
||||
// First stage: pre-init
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
*cs = *corestate.NewCorestate(&corestate.CoreState{
|
||||
UUID32DirName: "uuid",
|
||||
NodeBinName: filepath.Base(os.Args[0]),
|
||||
NodeVersion: config.NodeVersion,
|
||||
MetaDir: "./.meta",
|
||||
Stage: corestate.StagePreInit,
|
||||
StartTimestampUnix: time.Now().Unix(),
|
||||
})
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Log.SetPrefix(logs.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
if err := x.Config.LoadEnv(); err != nil {
|
||||
x.Log.Fatalf("env load error: %s", err)
|
||||
}
|
||||
cs.NodePath = x.Config.Env.NodePath
|
||||
|
||||
if cfgPath := x.Config.CMDLine.Run.ConfigPath; cfgPath != "" {
|
||||
x.Config.Env.ConfigPath = cfgPath
|
||||
}
|
||||
if err := x.Config.LoadConf(x.Config.Env.ConfigPath); err != nil {
|
||||
x.Log.Fatalf("conf load error: %s", err)
|
||||
}
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
|
||||
x.Log.Fatalf("Cannod generate node uuid: %s", err.Error())
|
||||
}
|
||||
uuid32, err = corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
|
||||
if err != nil {
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
x.Log.Fatalf("uuid load error: %s", err)
|
||||
}
|
||||
cs.UUID32 = uuid32
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
if x.Config.Env.ParentStagePID != os.Getpid() {
|
||||
if os.TempDir() != "/tmp" {
|
||||
x.Log.Printf("%s: %s", logs.PrintWarn(), "Non-standard value specified for temporary directory")
|
||||
}
|
||||
// still pre-init stage
|
||||
runDir, err := run_manager.Create(cs.UUID32)
|
||||
if err != nil {
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
cs.RunDir = runDir
|
||||
input, err := os.Open(os.Args[0])
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if err := run_manager.Set(cs.NodeBinName); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
fmgr := run_manager.File(cs.NodeBinName)
|
||||
output, err := fmgr.Open()
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
fmgr.Close()
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
|
||||
fmgr.Close()
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
input.Close()
|
||||
fmgr.Close()
|
||||
runArgs := os.Args
|
||||
runArgs[0] = filepath.Join(cs.RunDir, cs.NodeBinName)
|
||||
|
||||
// prepare environ
|
||||
env := utils.SetEviron(os.Environ(), fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))
|
||||
|
||||
if err := syscall.Exec(runArgs[0], runArgs, env); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
x.Log.Printf("Node uuid is %s", cs.UUID32)
|
||||
},
|
||||
|
||||
// post-init stage
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
cs.Stage = corestate.StagePostInit
|
||||
x.Log.SetPrefix(logs.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
cs.RunDir = run_manager.Toggle()
|
||||
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if exist {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
|
||||
}
|
||||
|
||||
if err := run_manager.Set("run.lock"); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
lockPath, err := run_manager.Get("run.lock")
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
lockFile := ini.Empty()
|
||||
secRun, err := lockFile.NewSection("runtime")
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
secRun.Key("pid").SetValue(fmt.Sprintf("%d/%d", os.Getpid(), x.Config.Env.ParentStagePID))
|
||||
secRun.Key("version").SetValue(cs.NodeVersion)
|
||||
secRun.Key("uuid").SetValue(cs.UUID32)
|
||||
secRun.Key("timestamp").SetValue(time.Unix(cs.StartTimestampUnix, 0).Format("2006-01-02/15:04:05 MST"))
|
||||
secRun.Key("timestamp-unix").SetValue(fmt.Sprintf("%d", cs.StartTimestampUnix))
|
||||
|
||||
err = lockFile.SaveTo(lockPath)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
cs.Stage = corestate.StageReady
|
||||
x.Log.SetPrefix(logs.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
x.SLog = new(slog.Logger)
|
||||
newSlog, err := logs.SetupLogger(x.Config.Conf.Log)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
*x.SLog = *newSlog
|
||||
},
|
||||
)
|
||||
|
||||
nodeApp.Run(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
|
||||
ctxMain, cancelMain := context.WithCancel(ctx)
|
||||
runLockFile := run_manager.File("run.lock")
|
||||
_, err := runLockFile.Open()
|
||||
if err != nil {
|
||||
x.Log.Fatalf("cannot open run.lock: %s", err)
|
||||
}
|
||||
|
||||
_, err = runLockFile.Watch(ctxMain, func() {
|
||||
x.Log.Printf("run.lock was touched")
|
||||
_ = run_manager.Clean()
|
||||
cancelMain()
|
||||
})
|
||||
if err != nil {
|
||||
x.Log.Printf("watch error: %s", err)
|
||||
}
|
||||
|
||||
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
|
||||
Log: *x.SLog,
|
||||
Config: x.Config.Conf,
|
||||
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`),
|
||||
ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`),
|
||||
Ver: "v1",
|
||||
})
|
||||
|
||||
s := gs.InitGeneral(&gs.GeneralServerInit{
|
||||
Log: x.SLog,
|
||||
Config: x.Config.Conf,
|
||||
}, serverv1)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
r.HandleFunc(config.ComDirRoute, s.Handle)
|
||||
r.Route("/favicon.ico", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: x.Config.Conf.HTTPServer.Address,
|
||||
Handler: r,
|
||||
ErrorLog: log.New(&logs.SlogWriter{
|
||||
Logger: x.SLog,
|
||||
Level: logs.GlobalLevel,
|
||||
}, "", 0),
|
||||
}
|
||||
go func() {
|
||||
if x.Config.Conf.TLS.TlsEnabled {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
|
||||
if err != nil {
|
||||
x.Log.Printf("%s: Failed to start TLS listener: %s", logs.PrintError(), err.Error())
|
||||
return
|
||||
}
|
||||
x.Log.Printf("Serving on %s port %s with TLS... (https://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
if err := srv.ServeTLS(limitedListener, x.Config.Conf.TLS.CertFile, x.Config.Conf.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
x.Log.Printf("%s: Failed to start HTTPS server: %s", logs.PrintError(), err.Error())
|
||||
}
|
||||
} else {
|
||||
x.Log.Printf("Serving on %s port %s... (http://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
|
||||
if err != nil {
|
||||
x.Log.Printf("%s: Failed to start listener: %s", logs.PrintError(), err.Error())
|
||||
return
|
||||
}
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
if err := srv.Serve(limitedListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
x.Log.Printf("%s: Failed to start HTTP server: %s", logs.PrintError(), err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if x.Config.Conf.Updates.UpdatesEnabled {
|
||||
go func() {
|
||||
x.Updated = update.NewUpdater(ctxMain, x.Log, x.Config.Conf, x.Config.Env)
|
||||
x.Updated.Shutdownfunc(cancelMain)
|
||||
for {
|
||||
isNewUpdate, err := x.Updated.CkeckUpdates()
|
||||
if err != nil {
|
||||
x.Log.Printf("Failed to check for updates: %s", err.Error())
|
||||
}
|
||||
if isNewUpdate {
|
||||
if err := x.Updated.Update(); err != nil {
|
||||
x.Log.Printf("Failed to update: %s", err.Error())
|
||||
} else {
|
||||
x.Log.Printf("Update completed successfully")
|
||||
}
|
||||
}
|
||||
time.Sleep(x.Config.Conf.Updates.CheckInterval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-ctxMain.Done()
|
||||
if err := srv.Shutdown(ctxMain); err != nil {
|
||||
x.Log.Printf("%s: Failed to stop the server gracefully: %s", logs.PrintError(), err.Error())
|
||||
} else {
|
||||
x.Log.Printf("Server stopped gracefully")
|
||||
}
|
||||
|
||||
x.Log.Println("Cleaning up...")
|
||||
|
||||
if err := run_manager.Clean(); err != nil {
|
||||
x.Log.Printf("%s: Cleanup error: %s", logs.PrintError(), err.Error())
|
||||
}
|
||||
x.Log.Println("bye!")
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
}
|
||||
16
com/exec.lua
16
com/exec.lua
@@ -1,16 +0,0 @@
|
||||
if not Params.f then
|
||||
Result.status = "error"
|
||||
Result.error = "Missing parameter: f"
|
||||
return
|
||||
end
|
||||
|
||||
local code = os.execute("touch " .. Params.f)
|
||||
if code ~= 0 then
|
||||
Result.status = "error"
|
||||
Result.message = "Failed to execute command"
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
Result.status = "ok"
|
||||
Result.message = "Command executed successfully"
|
||||
@@ -1,9 +1,7 @@
|
||||
mode: "dev"
|
||||
mode: "prod"
|
||||
|
||||
http_server:
|
||||
address: "0.0.0.0:8080"
|
||||
timeout: 3s
|
||||
idle_timeout: 30s
|
||||
api:
|
||||
latest-version: v1
|
||||
layers:
|
||||
@@ -15,18 +13,9 @@ tls:
|
||||
cert_file: "./cert/fullchain.pem"
|
||||
key_file: "./cert/privkey.pem"
|
||||
|
||||
internal:
|
||||
meta-dir: "./.meta/"
|
||||
|
||||
com_dir: "com/"
|
||||
|
||||
|
||||
updates:
|
||||
enabled: true
|
||||
allow-auto-updates: true
|
||||
allow-updates: true
|
||||
allow-downgrades: false
|
||||
|
||||
check-interval: 1h
|
||||
repository_url: "https://repo.serve.lv/raw/go-sally"
|
||||
wanted-version: "latest-stable"
|
||||
repository_url: "https://repo.serve.lv/raw/go-sally"
|
||||
64
core/app/app.go
Normal file
64
core/app/app.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/update"
|
||||
)
|
||||
|
||||
type AppContract interface {
|
||||
InitialHooks(fn ...func(cs *corestate.CoreState, x *AppX))
|
||||
Run(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX) error)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
initHooks []func(cs *corestate.CoreState, x *AppX)
|
||||
runHook func(ctx context.Context, cs *corestate.CoreState, x *AppX) error
|
||||
|
||||
Corestate *corestate.CoreState
|
||||
AppX *AppX
|
||||
}
|
||||
|
||||
type AppX struct {
|
||||
Config *config.Compositor
|
||||
Log *log.Logger
|
||||
SLog *slog.Logger
|
||||
Updated *update.Updater
|
||||
}
|
||||
|
||||
func New() AppContract {
|
||||
return &App{
|
||||
AppX: &AppX{
|
||||
Log: log.Default(),
|
||||
},
|
||||
Corestate: &corestate.CoreState{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) InitialHooks(fn ...func(cs *corestate.CoreState, x *AppX)) {
|
||||
a.initHooks = append(a.initHooks, fn...)
|
||||
}
|
||||
|
||||
func (a *App) Run(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX) error) {
|
||||
a.runHook = fn
|
||||
|
||||
for _, hook := range a.initHooks {
|
||||
hook(a.Corestate, a.AppX)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer stop()
|
||||
|
||||
if a.runHook != nil {
|
||||
if err := a.runHook(ctx, a.Corestate, a.AppX); err != nil {
|
||||
log.Fatalf("fatal in Run: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
182
core/config/compositor.go
Normal file
182
core/config/compositor.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func NewCompositor() *Compositor {
|
||||
return &Compositor{}
|
||||
}
|
||||
|
||||
func (c *Compositor) LoadEnv() error {
|
||||
v := viper.New()
|
||||
|
||||
// defaults
|
||||
v.SetDefault("config_path", "./cfg/config.yaml")
|
||||
v.SetDefault("node_path", "./")
|
||||
v.SetDefault("parent_pid", -1)
|
||||
|
||||
// GS_*
|
||||
v.SetEnvPrefix("GS")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
|
||||
var env Env
|
||||
if err := v.Unmarshal(&env); err != nil {
|
||||
return fmt.Errorf("error unmarshaling env: %w", err)
|
||||
}
|
||||
|
||||
c.Env = &env
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compositor) LoadConf(path string) error {
|
||||
v := viper.New()
|
||||
|
||||
v.SetConfigFile(path)
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
// defaults
|
||||
v.SetDefault("mode", "dev")
|
||||
v.SetDefault("com_dir", "./com/")
|
||||
v.SetDefault("http_server.address", "0.0.0.0")
|
||||
v.SetDefault("http_server.port", "8080")
|
||||
v.SetDefault("http_server.timeout", "5s")
|
||||
v.SetDefault("http_server.idle_timeout", "60s")
|
||||
v.SetDefault("tls.enabled", false)
|
||||
v.SetDefault("tls.cert_file", "./cert/server.crt")
|
||||
v.SetDefault("tls.key_file", "./cert/server.key")
|
||||
v.SetDefault("updates.enabled", false)
|
||||
v.SetDefault("updates.check_interval", "2h")
|
||||
v.SetDefault("updates.wanted_version", "latest-stable")
|
||||
v.SetDefault("log.level", "info")
|
||||
v.SetDefault("log.out_path", "")
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("error reading config: %w", err)
|
||||
}
|
||||
|
||||
var cfg Conf
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
return fmt.Errorf("error unmarshaling config: %w", err)
|
||||
}
|
||||
|
||||
c.Conf = &Conf{}
|
||||
c.Conf = &cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compositor) LoadCMDLine(root *cobra.Command) {
|
||||
cmdLine := &CMDLine{}
|
||||
c.CMDLine = cmdLine
|
||||
|
||||
t := reflect.TypeOf(cmdLine).Elem()
|
||||
v := reflect.ValueOf(cmdLine).Elem()
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldVal := v.Field(i)
|
||||
ptr := fieldVal.Addr().Interface()
|
||||
use := strings.ToLower(field.Name)
|
||||
|
||||
var cmd *cobra.Command
|
||||
for _, sub := range root.Commands() {
|
||||
|
||||
if sub.Use == use {
|
||||
cmd = sub
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if use == root.Use {
|
||||
cmd = root
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
Unmarshal(cmd, ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func Unmarshal(cmd *cobra.Command, target any) {
|
||||
t := reflect.TypeOf(target).Elem()
|
||||
v := reflect.ValueOf(target).Elem()
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
valPtr := v.Field(i).Addr().Interface()
|
||||
|
||||
full := field.Tag.Get("full")
|
||||
short := field.Tag.Get("short")
|
||||
def := field.Tag.Get("def")
|
||||
desc := field.Tag.Get("desc")
|
||||
isPersistent := field.Tag.Get("persistent") == "true"
|
||||
|
||||
flagSet := cmd.Flags()
|
||||
if isPersistent {
|
||||
flagSet = cmd.PersistentFlags()
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.String:
|
||||
flagSet.StringVarP(valPtr.(*string), full, short, def, desc)
|
||||
|
||||
case reflect.Bool:
|
||||
defVal, err := strconv.ParseBool(def)
|
||||
if err != nil && def != "" {
|
||||
fmt.Printf("warning: cannot parse default bool: %q\n", def)
|
||||
}
|
||||
flagSet.BoolVarP(valPtr.(*bool), full, short, defVal, desc)
|
||||
|
||||
case reflect.Int:
|
||||
defVal, err := strconv.Atoi(def)
|
||||
if err != nil && def != "" {
|
||||
fmt.Printf("warning: cannot parse default int: %q\n", def)
|
||||
}
|
||||
flagSet.IntVarP(valPtr.(*int), full, short, defVal, desc)
|
||||
|
||||
case reflect.Slice:
|
||||
elemKind := field.Type.Elem().Kind()
|
||||
switch elemKind {
|
||||
case reflect.String:
|
||||
defVals := []string{}
|
||||
if def != "" {
|
||||
defVals = strings.Split(def, ",")
|
||||
}
|
||||
flagSet.StringSliceVarP(valPtr.(*[]string), full, short, defVals, desc)
|
||||
|
||||
case reflect.Int:
|
||||
var intVals []int
|
||||
if def != "" {
|
||||
for _, s := range strings.Split(def, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
fmt.Printf("warning: cannot parse int in slice: %q\n", s)
|
||||
continue
|
||||
}
|
||||
intVals = append(intVals, n)
|
||||
}
|
||||
}
|
||||
flagSet.IntSliceVarP(valPtr.(*[]int), full, short, intVals, desc)
|
||||
|
||||
default:
|
||||
fmt.Printf("unsupported slice element type: %s\n", elemKind)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("unsupported field type: %s\n", field.Type.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,78 +3,77 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
)
|
||||
|
||||
// ConfigConf basic structure of configs
|
||||
type ConfigConf struct {
|
||||
Mode string `yaml:"mode" env-default:"dev"`
|
||||
ComDir string `yaml:"com_dir" env-default:"./com/"`
|
||||
HTTPServer `yaml:"http_server"`
|
||||
TLS `yaml:"tls"`
|
||||
Internal `yaml:"internal"`
|
||||
Updates `yaml:"updates"`
|
||||
type CompositorContract interface {
|
||||
LoadEnv() error
|
||||
LoadConf(path string) error
|
||||
}
|
||||
|
||||
type Compositor struct {
|
||||
CMDLine *CMDLine
|
||||
Conf *Conf
|
||||
Env *Env
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
Mode string `mapstructure:"mode"`
|
||||
ComDir string `mapstructure:"com_dir"`
|
||||
HTTPServer HTTPServer `mapstructure:"http_server"`
|
||||
TLS TLS `mapstructure:"tls"`
|
||||
Updates Updates `mapstructure:"updates"`
|
||||
Log Log `mapstructure:"log"`
|
||||
}
|
||||
|
||||
type HTTPServer struct {
|
||||
Address string `yaml:"address" env-default:"0.0.0.0:8080"`
|
||||
Timeout time.Duration `yaml:"timeout" env-default:"5s"`
|
||||
IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s"`
|
||||
HTTPServer_Api `yaml:"api"`
|
||||
Address string `mapstructure:"address"`
|
||||
Port string `mapstructure:"port"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
|
||||
HTTPServer_Api HTTPServer_Api `mapstructure:"api"`
|
||||
}
|
||||
|
||||
type HTTPServer_Api struct {
|
||||
LatestVer string `yaml:"latest-version" env-required:"true"`
|
||||
Layers []string `yaml:"layers"`
|
||||
LatestVer string `mapstructure:"latest-version"`
|
||||
Layers []string `mapstructure:"layers"`
|
||||
}
|
||||
|
||||
type TLS struct {
|
||||
TlsEnabled bool `yaml:"enabled" env-default:"false"`
|
||||
CertFile string `yaml:"cert_file" env-default:"./cert/server.crt"`
|
||||
KeyFile string `yaml:"key_file" env-default:"./cert/server.key"`
|
||||
}
|
||||
|
||||
type Internal struct {
|
||||
MetaDir string `yaml:"meta_dir" env-default:"./.meta/"`
|
||||
TlsEnabled bool `mapstructure:"enabled"`
|
||||
CertFile string `mapstructure:"cert_file"`
|
||||
KeyFile string `mapstructure:"key_file"`
|
||||
}
|
||||
|
||||
type Updates struct {
|
||||
UpdatesEnabled bool `yaml:"enabled" env-default:"false"`
|
||||
AllowAutoUpdates bool `yaml:"allow_auto_updates" env-default:"false"`
|
||||
AllowUpdates bool `yaml:"allow_updates" env-default:"false"`
|
||||
AllowDowngrades bool `yaml:"allow_downgrades" env-default:"false"`
|
||||
CheckInterval time.Duration `yaml:"check_interval" env-default:"2h"`
|
||||
RepositoryURL string `yaml:"repository_url" env-default:""`
|
||||
WantedVersion string `yaml:"wanted_version" env-default:"latest-stable"`
|
||||
UpdatesEnabled bool `mapstructure:"enabled"`
|
||||
CheckInterval time.Duration `mapstructure:"check_interval"`
|
||||
RepositoryURL string `mapstructure:"repository_url"`
|
||||
WantedVersion string `mapstructure:"wanted_version"`
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level"`
|
||||
OutPath string `mapstructure:"out_path"`
|
||||
}
|
||||
|
||||
// ConfigEnv structure for environment variables
|
||||
type ConfigEnv struct {
|
||||
ConfigPath string `env:"CONFIG_PATH" env-default:"./cfg/config.yaml"`
|
||||
type Env struct {
|
||||
ConfigPath string `mapstructure:"config_path"`
|
||||
NodePath string `mapstructure:"node_path"`
|
||||
ParentStagePID int `mapstructure:"parent_pid"`
|
||||
}
|
||||
|
||||
// MustLoadConfig loads the configuration from the specified path and environment variables.
|
||||
// Program will shutdown if any error occurs during loading.
|
||||
func MustLoadConfig() *ConfigConf {
|
||||
log.SetOutput(os.Stderr)
|
||||
var configEnv ConfigEnv
|
||||
if err := cleanenv.ReadEnv(&configEnv); err != nil {
|
||||
log.Fatalf("Failed to read environment variables: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := os.Stat(configEnv.ConfigPath); os.IsNotExist(err) {
|
||||
log.Fatalf("Config file does not exist: %s", configEnv.ConfigPath)
|
||||
os.Exit(2)
|
||||
}
|
||||
var config ConfigConf
|
||||
if err := cleanenv.ReadConfig(configEnv.ConfigPath, &config); err != nil {
|
||||
log.Fatalf("Failed to read config file: %v", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
log.Printf("Configuration loaded successfully from %s", configEnv.ConfigPath)
|
||||
return &config
|
||||
type CMDLine struct {
|
||||
Run Run
|
||||
Node Root
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Debug bool `persistent:"true" full:"debug" short:"d" def:"false" desc:"Set debug mode"`
|
||||
}
|
||||
|
||||
type Run struct {
|
||||
ConfigPath string `persistent:"true" full:"config" short:"c" def:"./config.yaml" desc:"Path to configuration file"`
|
||||
Test []int `persistent:"true" full:"test" short:"t" def:"" desc:"js test"`
|
||||
}
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
// UUIDLength is uuids length for sessions. By default it is 16 bytes.
|
||||
var UUIDLength byte = 4
|
||||
var UUIDLength int = 16
|
||||
|
||||
// ApiRoute setting for go-chi for main route for api requests
|
||||
var ApiRoute string = "/api/{ver}"
|
||||
|
||||
// ComDirRoute setting for go-chi for main route for commands
|
||||
var ComDirRoute string = "/com"
|
||||
|
||||
// NodeVersion is the version of the node. It can be set by the build system or manually.
|
||||
// If not set, it will return "version0.0.0-none" by default
|
||||
var NodeVersion string
|
||||
// ActualFileName is a feature of the GoSally update system.
|
||||
|
||||
// ActualFileName is a feature of the GoSally update system.
|
||||
// In the repository, the file specified in the variable contains the current information about updates
|
||||
var ActualFileName string = "actual.txt"
|
||||
|
||||
type _internalConsts struct{}
|
||||
type _serverConsts struct{}
|
||||
type _updateConsts struct{}
|
||||
// UpdateArchiveName is the name of the archive that will be used for updates.
|
||||
var UpdateArchiveName string = "gosally-node"
|
||||
|
||||
func GetUpdateConsts() _updateConsts { return _updateConsts{} }
|
||||
func (_ _updateConsts) GetNodeVersion() string {
|
||||
// UpdateInstallPath is the path where the update will be installed.
|
||||
var UpdateDownloadPath string = os.TempDir()
|
||||
|
||||
var MetaDir string = "./.meta"
|
||||
|
||||
func init() {
|
||||
if NodeVersion == "" {
|
||||
return "v0.0.0-none"
|
||||
NodeVersion = "v0.0.0-none"
|
||||
}
|
||||
return NodeVersion
|
||||
}
|
||||
func (_ _updateConsts) GetActualFileName() string { return ActualFileName }
|
||||
|
||||
func GetInternalConsts() _internalConsts { return _internalConsts{} }
|
||||
func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength }
|
||||
|
||||
func GetServerConsts() _serverConsts { return _serverConsts{} }
|
||||
func (_ _serverConsts) GetApiRoute() string { return ApiRoute }
|
||||
func (_ _serverConsts) GetComDirRoute() string { return ComDirRoute }
|
||||
|
||||
22
core/corestate/corestate.go
Normal file
22
core/corestate/corestate.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package corestate
|
||||
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
StageNotReady Stage = "init"
|
||||
StagePreInit Stage = "pre-init"
|
||||
StagePostInit Stage = "post-init"
|
||||
StageReady Stage = "event"
|
||||
)
|
||||
|
||||
const (
|
||||
StringsNone string = "none"
|
||||
)
|
||||
|
||||
func NewCorestate(o *CoreState) *CoreState {
|
||||
// TODO: create a convenient interface for creating a state
|
||||
// if !utils.IsFullyInitialized(o) {
|
||||
// return nil, fmt.Errorf("CoreState is not fully initialized")
|
||||
// }
|
||||
return o
|
||||
}
|
||||
80
core/corestate/node_uuid.go
Normal file
80
core/corestate/node_uuid.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package corestate
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
)
|
||||
|
||||
// GetNodeUUID outputs the correct uuid from the file at the path specified in the arguments.
|
||||
// If the uuid is not correct or is not exist, an empty string and an error will be returned.
|
||||
// The path to the identifier must contain the path to the "uuid" directory,
|
||||
// not the file with the identifier itself, for example: "uuid/data"
|
||||
func GetNodeUUID(metaInfPath string) (string, error) {
|
||||
uuid, err := readNodeUUIDRaw(filepath.Join(metaInfPath, "data"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(uuid[:]), nil
|
||||
}
|
||||
|
||||
func readNodeUUIDRaw(p string) ([]byte, error) {
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if len(data) != config.UUIDLength {
|
||||
return data, errors.New("decoded UUID length mismatch")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SetNodeUUID sets the identifier to the given path.
|
||||
// The function replaces the identifier's associated directory with all its contents.
|
||||
func SetNodeUUID(metaInfPath string) error {
|
||||
if !strings.HasSuffix(metaInfPath, "uuid") {
|
||||
return errors.New("invalid meta/uuid path")
|
||||
}
|
||||
info, err := os.Stat(metaInfPath)
|
||||
if err == nil && info.IsDir() {
|
||||
err = os.RemoveAll(metaInfPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.MkdirAll(metaInfPath, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dataPath := filepath.Join(metaInfPath, "data")
|
||||
|
||||
uuidStr, err := utils.NewUUID32Raw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(dataPath, uuidStr[:], 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readmePath := filepath.Join(metaInfPath, "README.txt")
|
||||
readmeContent := ` - - - - ! STRICTLY FORBIDDEN TO MODIFY THIS DIRECTORY ! - - - -
|
||||
This directory contains the unique node identifier stored in the file named data.
|
||||
This identifier is critical for correct node recognition both locally and across the network.
|
||||
Any modification, deletion, or tampering with this directory may lead to permanent loss of identity, data corruption, or network conflicts.
|
||||
Proceed at your own risk. You have been warned.`
|
||||
err = os.WriteFile(readmePath, []byte(readmeContent), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
24
core/corestate/types.go
Normal file
24
core/corestate/types.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package corestate
|
||||
|
||||
// CoreStateContract is interface for CoreState.
|
||||
// CoreState is a structure that contains the basic meta-information vital to the node.
|
||||
// The interface contains functionality for working with the Runtime directory and its files,
|
||||
// and access to low-level logging in stdout
|
||||
type CoreStateContract interface {
|
||||
}
|
||||
|
||||
type CoreState struct {
|
||||
UUID32 string
|
||||
UUID32DirName string
|
||||
|
||||
StartTimestampUnix int64
|
||||
|
||||
NodeBinName string
|
||||
NodeVersion string
|
||||
|
||||
Stage Stage
|
||||
|
||||
NodePath string
|
||||
MetaDir string
|
||||
RunDir string
|
||||
}
|
||||
190
core/general_server/handle_multi.go
Normal file
190
core/general_server/handle_multi.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Package general_server provides an API request router based on versioning and custom layers.
|
||||
//
|
||||
// The GeneralServer distributes incoming HTTP requests to specific registered servers
|
||||
// depending on the API version or defined logical layer. To operate properly, additional
|
||||
// servers must be registered using the InitGeneral function or AppendToArray method.
|
||||
//
|
||||
// All registered servers must implement the GeneralServerApiContract interface to ensure
|
||||
// correct interaction. The GeneralServer itself implements this interface and can be
|
||||
// passed as an HTTP handler.
|
||||
//
|
||||
// If the requested version is not explicitly registered but matches a configured logical
|
||||
// layer, the server will fallback to the latest registered version for that layer.
|
||||
// Otherwise, an HTTP 400 error is returned.
|
||||
package general_server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// serversApiVer is a type alias for string, used to represent API version strings in the GeneralServer.
|
||||
type serversApiVer string
|
||||
|
||||
// GeneralServerApiContract defines the interface for servers that can be registered
|
||||
type GeneralServerApiContract interface {
|
||||
// GetVersion returns the API version of the server.
|
||||
GetVersion() string
|
||||
|
||||
// Handle and HandleList methods are used to forward requests.
|
||||
Handle(w http.ResponseWriter, r *http.Request)
|
||||
HandleList(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// GeneralServerContarct extends the GeneralServerApiContract with a method to append new servers.
|
||||
// This interface is only for general server initialization and does not need to be implemented by individual servers.
|
||||
type GeneralServerContarct interface {
|
||||
GeneralServerApiContract
|
||||
// AppendToArray adds a new server to the GeneralServer's internal map.
|
||||
AppendToArray(GeneralServerApiContract) error
|
||||
}
|
||||
|
||||
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
|
||||
type GeneralServer struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
|
||||
// servers holds the registered servers by their API version.
|
||||
// The key is the version string, and the value is the server implementing GeneralServerApi
|
||||
servers map[serversApiVer]GeneralServerApiContract
|
||||
|
||||
log slog.Logger
|
||||
cfg *config.Conf
|
||||
}
|
||||
|
||||
// GeneralServerInit structure only for initialization general server.
|
||||
type GeneralServerInit struct {
|
||||
Log slog.Logger
|
||||
Config *config.Conf
|
||||
}
|
||||
|
||||
// InitGeneral initializes a new GeneralServer with the provided configuration and registered servers.
|
||||
func InitGeneral(o *GeneralServerInit, servers ...GeneralServerApiContract) *GeneralServer {
|
||||
general := &GeneralServer{
|
||||
servers: make(map[serversApiVer]GeneralServerApiContract),
|
||||
cfg: o.Config,
|
||||
log: o.Log,
|
||||
}
|
||||
|
||||
// register the provided servers
|
||||
// s is each server implementing GeneralServerApiContract, this is not a general server
|
||||
for _, s := range servers {
|
||||
general.servers[serversApiVer(s.GetVersion())] = s
|
||||
}
|
||||
return general
|
||||
}
|
||||
|
||||
// GetVersion returns the API version of the GeneralServer, which is "general".
|
||||
func (s *GeneralServer) GetVersion() string {
|
||||
return "general"
|
||||
}
|
||||
|
||||
// AppendToArray adds a new server to the GeneralServer's internal map.
|
||||
func (s *GeneralServer) AppendToArray(server GeneralServerApiContract) error {
|
||||
if _, exist := s.servers[serversApiVer(server.GetVersion())]; !exist {
|
||||
s.servers[serversApiVer(server.GetVersion())] = server
|
||||
return nil
|
||||
}
|
||||
return errors.New("server with this version is already exist")
|
||||
}
|
||||
|
||||
// Handle processes incoming HTTP requests, routing them to the appropriate server based on the API version.
|
||||
// It checks if the requested version is registered and handles the request accordingly.
|
||||
func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
s.w = w
|
||||
s.r = r
|
||||
serverReqApiVer := chi.URLParam(r, "ver")
|
||||
log := s.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", serverReqApiVer),
|
||||
slog.String("url", s.r.URL.String()),
|
||||
slog.String("method", s.r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("remote", s.r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
|
||||
s.log.Debug("Received request")
|
||||
|
||||
// transfer control to the server
|
||||
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
|
||||
srv.Handle(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// if the requested version is not registered, check if it matches a logical layer
|
||||
// and use the latest version for that layer if available
|
||||
// this allows for custom layers to be defined in the configuration
|
||||
// and used as a fallback for unsupported versions
|
||||
// this is useful for cases where the API version is not explicitly registered
|
||||
// but the logical layer is defined in the configuration
|
||||
if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
|
||||
if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
|
||||
s.log.Debug("Using latest version under custom layer",
|
||||
slog.String("layer", serverReqApiVer),
|
||||
slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
|
||||
)
|
||||
// transfer control to the latest version server under the custom layer
|
||||
srv.Handle(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("HTTP request error: unsupported API version",
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
if err := utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version"); err != nil {
|
||||
s.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleList processes incoming HTTP requests for listing commands, routing them to the appropriate server based on the API version.
|
||||
func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
s.w = w
|
||||
s.r = r
|
||||
serverReqApiVer := chi.URLParam(r, "ver")
|
||||
|
||||
log := s.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", serverReqApiVer),
|
||||
slog.String("url", s.r.URL.String()),
|
||||
slog.String("method", s.r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("remote", s.r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
|
||||
log.Debug("Received request")
|
||||
|
||||
// transfer control to the server
|
||||
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
|
||||
srv.HandleList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
|
||||
if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
|
||||
log.Debug("Using latest version under custom layer",
|
||||
slog.String("layer", serverReqApiVer),
|
||||
slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
|
||||
)
|
||||
// transfer control to the latest version server under the custom layer
|
||||
srv.HandleList(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("HTTP request error: unsupported API version",
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
if err := utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version"); err != nil {
|
||||
s.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package general_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type serversApiVer string
|
||||
|
||||
type GeneralServerApiContract interface {
|
||||
GetVersion() string
|
||||
|
||||
Handle(w http.ResponseWriter, r *http.Request)
|
||||
HandleList(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type GeneralServerContarct interface {
|
||||
GeneralServerApiContract
|
||||
AppendToArray(GeneralServerApiContract) error
|
||||
}
|
||||
|
||||
type GeneralServer struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
|
||||
servers map[serversApiVer]GeneralServerApiContract
|
||||
|
||||
log slog.Logger
|
||||
cfg *config.ConfigConf
|
||||
}
|
||||
|
||||
// structure only for initialization
|
||||
type GeneralServerInit struct {
|
||||
Log slog.Logger
|
||||
Config *config.ConfigConf
|
||||
}
|
||||
|
||||
func InitGeneral(o *GeneralServerInit, servers ...GeneralServerApiContract) *GeneralServer {
|
||||
general := &GeneralServer{
|
||||
servers: make(map[serversApiVer]GeneralServerApiContract),
|
||||
cfg: o.Config,
|
||||
log: o.Log,
|
||||
}
|
||||
for _, s := range servers {
|
||||
general.servers[serversApiVer(s.GetVersion())] = s
|
||||
}
|
||||
return general
|
||||
}
|
||||
|
||||
func (s *GeneralServer) GetVersion() string {
|
||||
return "general"
|
||||
}
|
||||
|
||||
func (s *GeneralServer) AppendToArray(server GeneralServerApiContract) error {
|
||||
if _, exist := s.servers[serversApiVer(server.GetVersion())]; !exist {
|
||||
s.servers[serversApiVer(server.GetVersion())] = server
|
||||
return nil
|
||||
}
|
||||
return errors.New("server with this version is already exist")
|
||||
}
|
||||
|
||||
func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
s.w = w
|
||||
s.r = r
|
||||
serverReqApiVer := chi.URLParam(r, "ver")
|
||||
log := s.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", serverReqApiVer),
|
||||
slog.String("url", s.r.URL.String()),
|
||||
slog.String("method", s.r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("remote", s.r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
|
||||
s.log.Debug("Received request")
|
||||
|
||||
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
|
||||
srv.Handle(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(s.cfg.Layers, serverReqApiVer) {
|
||||
if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok {
|
||||
s.log.Debug("Using latest version under custom layer",
|
||||
slog.String("layer", serverReqApiVer),
|
||||
slog.String("fallback-version", s.cfg.LatestVer),
|
||||
)
|
||||
srv.Handle(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("HTTP request error: unsupported API version",
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
s.writeJSONError(http.StatusBadRequest, "unsupported API version")
|
||||
}
|
||||
|
||||
func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
s.w = w
|
||||
s.r = r
|
||||
serverReqApiVer := chi.URLParam(r, "ver")
|
||||
|
||||
log := s.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", serverReqApiVer),
|
||||
slog.String("url", s.r.URL.String()),
|
||||
slog.String("method", s.r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("remote", s.r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
|
||||
log.Debug("Received request")
|
||||
|
||||
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
|
||||
srv.HandleList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(s.cfg.Layers, serverReqApiVer) {
|
||||
if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok {
|
||||
log.Debug("Using latest version under custom layer",
|
||||
slog.String("layer", serverReqApiVer),
|
||||
slog.String("fallback-version", s.cfg.LatestVer),
|
||||
)
|
||||
srv.HandleList(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("HTTP request error: unsupported API version",
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
s.writeJSONError(http.StatusBadRequest, "unsupported API version")
|
||||
}
|
||||
|
||||
// func (s *GeneralServer) _errNotFound() {
|
||||
// s.writeJSONError(http.StatusBadRequest, "invalid request")
|
||||
// s.log.Error("HTTP request error",
|
||||
// slog.String("remote", s.r.RemoteAddr),
|
||||
// slog.String("method", s.r.Method),
|
||||
// slog.String("url", s.r.URL.String()),
|
||||
// slog.Int("status", http.StatusBadRequest))
|
||||
// }
|
||||
|
||||
func (s *GeneralServer) writeJSONError(status int, msg string) {
|
||||
s.w.Header().Set("Content-Type", "application/json")
|
||||
s.w.WriteHeader(status)
|
||||
resp := map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": msg,
|
||||
"code": status,
|
||||
}
|
||||
if err := json.NewEncoder(s.w).Encode(resp); err != nil {
|
||||
s.log.Error("Failed to write JSON error response",
|
||||
slog.String("error", err.Error()),
|
||||
slog.Int("status", status))
|
||||
return
|
||||
}
|
||||
}
|
||||
24
core/logs/color.go
Normal file
24
core/logs/color.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package logs
|
||||
|
||||
import "fmt"
|
||||
|
||||
func SetBlack(s string) string { return fmt.Sprintf("\033[30m%s\033[0m", s) }
|
||||
func SetRed(s string) string { return fmt.Sprintf("\033[31m%s\033[0m", s) }
|
||||
func SetGreen(s string) string { return fmt.Sprintf("\033[32m%s\033[0m", s) }
|
||||
func SetYellow(s string) string { return fmt.Sprintf("\033[33m%s\033[0m", s) }
|
||||
func SetBlue(s string) string { return fmt.Sprintf("\033[34m%s\033[0m", s) }
|
||||
func SetMagenta(s string) string { return fmt.Sprintf("\033[35m%s\033[0m", s) }
|
||||
func SetCyan(s string) string { return fmt.Sprintf("\033[36m%s\033[0m", s) }
|
||||
func SetWhite(s string) string { return fmt.Sprintf("\033[37m%s\033[0m", s) }
|
||||
|
||||
func SetBrightBlack(s string) string { return fmt.Sprintf("\033[90m%s\033[0m", s) }
|
||||
func SetBrightRed(s string) string { return fmt.Sprintf("\033[91m%s\033[0m", s) }
|
||||
func SetBrightGreen(s string) string { return fmt.Sprintf("\033[92m%s\033[0m", s) }
|
||||
func SetBrightYellow(s string) string { return fmt.Sprintf("\033[93m%s\033[0m", s) }
|
||||
func SetBrightBlue(s string) string { return fmt.Sprintf("\033[94m%s\033[0m", s) }
|
||||
func SetBrightMagenta(s string) string { return fmt.Sprintf("\033[95m%s\033[0m", s) }
|
||||
func SetBrightCyan(s string) string { return fmt.Sprintf("\033[96m%s\033[0m", s) }
|
||||
func SetBrightWhite(s string) string { return fmt.Sprintf("\033[97m%s\033[0m", s) }
|
||||
|
||||
func PrintError() string { return SetRed("Error") }
|
||||
func PrintWarn() string { return SetYellow("Warning") }
|
||||
@@ -1,25 +1,87 @@
|
||||
// Package logs provides a logger setup function that configures the logger based on the environment.
|
||||
// It supports different logging levels for development and production environments.
|
||||
// It uses the standard library's slog package for structured logging.
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/run_manager"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envDev = "dev"
|
||||
envProd = "prod"
|
||||
)
|
||||
var GlobalLevel slog.Level
|
||||
|
||||
func SetupLogger(env string) *slog.Logger {
|
||||
var log *slog.Logger
|
||||
switch env {
|
||||
case envDev:
|
||||
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
case envProd:
|
||||
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
type SlogWriter struct {
|
||||
Logger *slog.Logger
|
||||
Level slog.Level
|
||||
}
|
||||
|
||||
func (w *SlogWriter) Write(p []byte) (n int, err error) {
|
||||
msg := string(bytes.TrimSpace(p))
|
||||
w.Logger.Log(context.TODO(), w.Level, msg)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// SetupLogger initializes and returns a logger based on the provided environment.
|
||||
func SetupLogger(o config.Log) (*slog.Logger, error) {
|
||||
var handlerOpts = slog.HandlerOptions{}
|
||||
var writer io.Writer = os.Stdout
|
||||
|
||||
switch o.Level {
|
||||
case "debug":
|
||||
GlobalLevel = slog.LevelDebug
|
||||
handlerOpts.Level = slog.LevelDebug
|
||||
case "info":
|
||||
GlobalLevel = slog.LevelInfo
|
||||
handlerOpts.Level = slog.LevelInfo
|
||||
default:
|
||||
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
GlobalLevel = slog.LevelInfo
|
||||
handlerOpts.Level = slog.LevelInfo
|
||||
}
|
||||
|
||||
return log
|
||||
if o.OutPath != "" {
|
||||
repl := map[string]string{
|
||||
"tmp": filepath.Clean(run_manager.RuntimeDir()),
|
||||
}
|
||||
re := regexp.MustCompile(`%(\w+)%`)
|
||||
result := re.ReplaceAllStringFunc(o.OutPath, func(match string) string {
|
||||
sub := re.FindStringSubmatch(match)
|
||||
if len(sub) < 2 {
|
||||
return match
|
||||
}
|
||||
key := sub[1]
|
||||
if val, ok := repl[key]; ok {
|
||||
return val
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
if strings.Contains(o.OutPath, "%tmp%") {
|
||||
relPath := strings.TrimPrefix(result, filepath.Clean(run_manager.RuntimeDir()))
|
||||
if err := run_manager.SetDir(relPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
logFile := &lumberjack.Logger{
|
||||
Filename: filepath.Join(result, "event.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 5,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
}
|
||||
writer = logFile
|
||||
}
|
||||
|
||||
log := slog.New(slog.NewJSONHandler(writer, &handlerOpts))
|
||||
return log, nil
|
||||
}
|
||||
|
||||
@@ -6,30 +6,20 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MockHandler is a mock implementation of slog.Handler for testing purposes.
|
||||
type MockHandler struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// Logs stores the log records captured by the handler.
|
||||
Logs []slog.Record
|
||||
}
|
||||
|
||||
func NewMockHandler() *MockHandler {
|
||||
return &MockHandler{}
|
||||
}
|
||||
|
||||
func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func NewMockHandler() *MockHandler { return &MockHandler{} }
|
||||
func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
|
||||
func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
|
||||
func (h *MockHandler) WithGroup(_ string) slog.Handler { return h }
|
||||
func (h *MockHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.Logs = append(h.Logs, r.Clone())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *MockHandler) WithGroup(_ string) slog.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
95
core/run_manager/run_file_manager.go
Normal file
95
core/run_manager/run_file_manager.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package run_manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func File(index string) RunFileManagerContract {
|
||||
value, ok := indexedPaths[index]
|
||||
if !ok {
|
||||
err := indexPaths()
|
||||
if err != nil {
|
||||
return &RunFileManager{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
value, ok = indexedPaths[index]
|
||||
if !ok {
|
||||
return &RunFileManager{
|
||||
err: fmt.Errorf("cannot detect file under index %s", index),
|
||||
}
|
||||
}
|
||||
}
|
||||
return &RunFileManager{
|
||||
indexedPath: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RunFileManager) Open() (*os.File, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
file, err := os.OpenFile(r.indexedPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.file = file
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (r *RunFileManager) Close() error {
|
||||
return r.file.Close()
|
||||
}
|
||||
|
||||
func (r *RunFileManager) Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.file == nil {
|
||||
return nil, fmt.Errorf("file is not opened")
|
||||
}
|
||||
|
||||
info, err := r.file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origStat := info.Sys().(*syscall.Stat_t)
|
||||
origIno := origStat.Ino
|
||||
origModTime := info.ModTime()
|
||||
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
newInfo, err := os.Stat(r.indexedPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
newStat := newInfo.Sys().(*syscall.Stat_t)
|
||||
if newStat.Ino != origIno {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if !newInfo.ModTime().Equal(origModTime) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return cancel, nil
|
||||
}
|
||||
158
core/run_manager/run_manager.go
Normal file
158
core/run_manager/run_manager.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package run_manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
)
|
||||
|
||||
type RunManagerContract interface {
|
||||
Get(index string) (string, error)
|
||||
|
||||
// Set recursively creates a file in runDir
|
||||
Set(index string) error
|
||||
|
||||
File(index string) RunFileManagerContract
|
||||
|
||||
indexPaths() error
|
||||
}
|
||||
|
||||
var (
|
||||
created bool
|
||||
runDir string
|
||||
indexedPaths = make(map[string]string)
|
||||
)
|
||||
|
||||
type RunFileManagerContract interface {
|
||||
Open() (*os.File, error)
|
||||
Close() error
|
||||
Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error)
|
||||
}
|
||||
|
||||
type RunFileManager struct {
|
||||
err error
|
||||
indexedPath string
|
||||
file *os.File
|
||||
}
|
||||
|
||||
// func (c *CoreState) RuntimeDir() RunManagerContract {
|
||||
// return c.RM
|
||||
// }
|
||||
|
||||
// Create creates a temp directory
|
||||
func Create(uuid32 string) (string, error) {
|
||||
if created {
|
||||
return runDir, fmt.Errorf("runtime directory is already created")
|
||||
}
|
||||
path, err := os.MkdirTemp("", fmt.Sprintf("*-%s-%s", uuid32, "gosally-runtime"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
runDir = path
|
||||
created = true
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func Clean() error {
|
||||
created = false
|
||||
indexedPaths = nil
|
||||
return utils.CleanTempRuntimes(runDir)
|
||||
}
|
||||
|
||||
// Quite dangerous and goofy.
|
||||
// TODO: implement a better variant of runDir indexing on the second stage of initialization
|
||||
func Toggle() string {
|
||||
runDir = filepath.Dir(os.Args[0])
|
||||
created = true
|
||||
return runDir
|
||||
}
|
||||
|
||||
func Get(index string) (string, error) {
|
||||
if !created {
|
||||
return "", fmt.Errorf("runtime directory is not created")
|
||||
}
|
||||
if indexedPaths == nil {
|
||||
err := indexPaths()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
if indexedPaths == nil {
|
||||
return "", fmt.Errorf("indexedPaths is nil")
|
||||
}
|
||||
value, ok := indexedPaths[index]
|
||||
if !ok {
|
||||
err := indexPaths()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value, ok = indexedPaths[index]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cannot detect file under index %s", index)
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func Set(index string) error {
|
||||
if !created {
|
||||
return fmt.Errorf("runtime directory is not created")
|
||||
}
|
||||
fullPath := filepath.Join(runDir, index)
|
||||
|
||||
dir := filepath.Dir(fullPath)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if indexedPaths == nil {
|
||||
err = indexPaths()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
indexedPaths[index] = fullPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetDir(index string) error {
|
||||
if !created {
|
||||
return fmt.Errorf("runtime directory is not created")
|
||||
}
|
||||
fullPath := filepath.Join(runDir, index)
|
||||
|
||||
err := os.MkdirAll(fullPath, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexPaths() error {
|
||||
if !created {
|
||||
return fmt.Errorf("runtime directory is not created")
|
||||
}
|
||||
i, err := utils.IndexPaths(runDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexedPaths = i
|
||||
return nil
|
||||
}
|
||||
|
||||
func RuntimeDir() string {
|
||||
return runDir
|
||||
}
|
||||
@@ -7,42 +7,63 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func (h *HandlerV1) _handle() {
|
||||
uuid16 := h.newUUID()
|
||||
// HandlerV1 is the main handler for version 1 of the API.
|
||||
// The function processes the HTTP request and runs Lua scripts,
|
||||
// preparing the environment and subsequently transmitting the execution result
|
||||
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
uuid16, err := utils.NewUUID(int(config.UUIDLength))
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID",
|
||||
slog.String("error", err.Error()))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
log := h.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", h.GetVersion()),
|
||||
slog.String("url", h.r.URL.String()),
|
||||
slog.String("method", h.r.Method),
|
||||
slog.String("url", r.URL.String()),
|
||||
slog.String("method", r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("connection-uuid", uuid16),
|
||||
slog.String("remote", h.r.RemoteAddr),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
log.Info("Received request")
|
||||
|
||||
cmd := chi.URLParam(h.r, "cmd")
|
||||
cmd := chi.URLParam(r, "cmd")
|
||||
if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) || !h.listAllowedCmd.MatchString(cmd) {
|
||||
log.Error("HTTP request error",
|
||||
slog.String("error", "invalid command"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
h.writeJSONError(http.StatusBadRequest, "invalid command")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusBadRequest, "invalid command"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scriptPath := h.comMatch(chi.URLParam(h.r, "ver"), cmd)
|
||||
scriptPath := h.comMatch(chi.URLParam(r, "ver"), cmd)
|
||||
if scriptPath == "" {
|
||||
log.Error("HTTP request error",
|
||||
slog.String("error", "command not found"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusNotFound))
|
||||
h.writeJSONError(http.StatusNotFound, "command not found")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusNotFound, "command not found"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,17 +73,18 @@ func (h *HandlerV1) _handle() {
|
||||
slog.String("error", "command not found"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusNotFound))
|
||||
h.writeJSONError(http.StatusNotFound, "command not found")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusNotFound, "command not found"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
// Создаем таблицу Params
|
||||
// Создаем таблицу In с Params
|
||||
paramsTable := L.NewTable()
|
||||
qt := h.r.URL.Query()
|
||||
qt := r.URL.Query()
|
||||
for k, v := range qt {
|
||||
if len(v) > 0 {
|
||||
L.SetField(paramsTable, k, lua.LString(v[0]))
|
||||
@@ -78,53 +100,65 @@ func (h *HandlerV1) _handle() {
|
||||
L.SetField(outTable, "Result", resultTable)
|
||||
L.SetGlobal("Out", outTable)
|
||||
|
||||
// Скрипт подготовки окружения
|
||||
prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua")
|
||||
if _, err := os.Stat(prepareLuaEnv); err == nil {
|
||||
if err := L.DoFile(prepareLuaEnv); err != nil {
|
||||
log.Error("Failed to prepare lua environment",
|
||||
slog.String("error", err.Error()))
|
||||
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Warn("No environment preparation script found, skipping preparation")
|
||||
}
|
||||
|
||||
// Основной Lua скрипт
|
||||
if err := L.DoFile(scriptPath); err != nil {
|
||||
log.Error("Failed to execute lua script",
|
||||
slog.String("error", err.Error()))
|
||||
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем Out
|
||||
lv := L.GetGlobal("Out")
|
||||
tbl, ok := lv.(*lua.LTable)
|
||||
if !ok {
|
||||
log.Error("Lua global 'Out' is not a table")
|
||||
h.writeJSONError(http.StatusInternalServerError, "'Out' is not a table")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "'Out' is not a table"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем Result из Out
|
||||
resultVal := tbl.RawGetString("Result")
|
||||
resultTbl, ok := resultVal.(*lua.LTable)
|
||||
if !ok {
|
||||
log.Error("Lua global 'Result' is not a table")
|
||||
h.writeJSONError(http.StatusInternalServerError, "'Result' is not a table")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "'Result' is not a table"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Перебираем таблицу Result
|
||||
out := make(map[string]interface{})
|
||||
out := make(map[string]any)
|
||||
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
out[key.String()] = convertTypes(value)
|
||||
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
|
||||
})
|
||||
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
|
||||
response := ResponseFormat{
|
||||
ResponsibleAgentUUID: uuid32,
|
||||
RequestedCommand: cmd,
|
||||
Response: out,
|
||||
}
|
||||
|
||||
h.w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(h.w).Encode(out); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
log.Error("Failed to encode JSON response",
|
||||
slog.String("error", err.Error()))
|
||||
}
|
||||
@@ -147,26 +181,3 @@ func (h *HandlerV1) _handle() {
|
||||
|
||||
log.Info("Session completed")
|
||||
}
|
||||
|
||||
func convertTypes(value lua.LValue) any {
|
||||
switch value.Type() {
|
||||
case lua.LTString:
|
||||
return value.String()
|
||||
case lua.LTNumber:
|
||||
return float64(value.(lua.LNumber))
|
||||
case lua.LTBool:
|
||||
return bool(value.(lua.LBool))
|
||||
case lua.LTTable:
|
||||
result := make(map[string]interface{})
|
||||
if tbl, ok := value.(*lua.LTable); ok {
|
||||
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
result[key.String()] = convertTypes(value)
|
||||
})
|
||||
}
|
||||
return result
|
||||
case lua.LTNil:
|
||||
return nil
|
||||
default:
|
||||
return value.String()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,33 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func (h *HandlerV1) _handleList() {
|
||||
uuid16 := h.newUUID()
|
||||
// The function processes the HTTP request and returns a list of available commands.
|
||||
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
uuid16, err := utils.NewUUID(int(config.UUIDLength))
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID",
|
||||
slog.String("error", err.Error()))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
log := h.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", h.GetVersion()),
|
||||
slog.String("url", h.r.URL.String()),
|
||||
slog.String("method", h.r.Method),
|
||||
slog.String("url", r.URL.String()),
|
||||
slog.String("method", r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("connection-uuid", uuid16),
|
||||
slog.String("remote", h.r.RemoteAddr),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
log.Info("Received request")
|
||||
@@ -31,7 +44,6 @@ func (h *HandlerV1) _handleList() {
|
||||
}
|
||||
var (
|
||||
files []os.DirEntry
|
||||
err error
|
||||
commands = make(map[string]ComMeta)
|
||||
cmdsProcessed = make(map[string]bool)
|
||||
)
|
||||
@@ -39,11 +51,14 @@ func (h *HandlerV1) _handleList() {
|
||||
if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
|
||||
log.Error("Failed to read commands directory",
|
||||
slog.String("error", err.Error()))
|
||||
h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
apiVer := chi.URLParam(h.r, "ver")
|
||||
apiVer := chi.URLParam(r, "ver")
|
||||
|
||||
// Сначала ищем версионные
|
||||
for _, file := range files {
|
||||
@@ -102,9 +117,14 @@ func (h *HandlerV1) _handleList() {
|
||||
log.Debug("Command list prepared")
|
||||
|
||||
log.Info("Session completed")
|
||||
|
||||
h.w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(h.w).Encode(commands); err != nil {
|
||||
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
|
||||
response := ResponseFormat{
|
||||
ResponsibleAgentUUID: uuid32,
|
||||
RequestedCommand: "list",
|
||||
Response: commands,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.log.Error("Failed to write JSON error response",
|
||||
slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
7
core/sv1/proto.go
Normal file
7
core/sv1/proto.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package sv1
|
||||
|
||||
type ResponseFormat struct {
|
||||
ResponsibleAgentUUID string
|
||||
RequestedCommand string
|
||||
Response any
|
||||
}
|
||||
@@ -1,48 +1,42 @@
|
||||
// Package sv1 provides the implementation of the Server V1 API handler.
|
||||
// It includes utilities for handling API requests, extracting descriptions, and managing UUIDs.
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
)
|
||||
|
||||
type ServerV1UtilsContract interface {
|
||||
extractDescriptionStatic(path string) (string, error)
|
||||
writeJSONError(status int, msg string)
|
||||
newUUID() string
|
||||
|
||||
_errNotFound()
|
||||
ErrNotFound(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// structure only for initialization
|
||||
// HandlerV1InitStruct structure is only for initialization
|
||||
type HandlerV1InitStruct struct {
|
||||
Ver string
|
||||
Log slog.Logger
|
||||
Config *config.ConfigConf
|
||||
Config *config.Conf
|
||||
AllowedCmd *regexp.Regexp
|
||||
ListAllowedCmd *regexp.Regexp
|
||||
}
|
||||
|
||||
// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests.
|
||||
type HandlerV1 struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
log *slog.Logger
|
||||
|
||||
log slog.Logger
|
||||
|
||||
cfg *config.ConfigConf
|
||||
cfg *config.Conf
|
||||
|
||||
// allowedCmd and listAllowedCmd are regular expressions used to validate command names.
|
||||
allowedCmd *regexp.Regexp
|
||||
listAllowedCmd *regexp.Regexp
|
||||
|
||||
ver string
|
||||
}
|
||||
|
||||
// InitV1Server initializes a new HandlerV1 with the provided configuration and returns it.
|
||||
// Should be carefull with giving to this function invalid parameters,
|
||||
// because there is no validation of parameters in this function.
|
||||
func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
|
||||
return &HandlerV1{
|
||||
log: o.Log,
|
||||
log: &o.Log,
|
||||
cfg: o.Config,
|
||||
allowedCmd: o.AllowedCmd,
|
||||
listAllowedCmd: o.ListAllowedCmd,
|
||||
@@ -50,18 +44,8 @@ func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
h.w = w
|
||||
h.r = r
|
||||
h._handle()
|
||||
}
|
||||
|
||||
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
h.w = w
|
||||
h.r = r
|
||||
h._handleList()
|
||||
}
|
||||
|
||||
// GetVersion returns the API version of the HandlerV1, which is set during initialization.
|
||||
// This version is used to identify the API version in the request routing.
|
||||
func (h *HandlerV1) GetVersion() string {
|
||||
return h.ver
|
||||
}
|
||||
|
||||
@@ -1,57 +1,19 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
)
|
||||
|
||||
func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
h.w = w
|
||||
h.r = r
|
||||
h._errNotFound()
|
||||
}
|
||||
|
||||
func (h *HandlerV1) newUUID() string {
|
||||
bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2))
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID", slog.String("error", err.Error()))
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func (h *HandlerV1) _errNotFound() {
|
||||
h.writeJSONError(http.StatusBadRequest, "invalid request")
|
||||
h.log.Error("HTTP request error",
|
||||
slog.String("remote", h.r.RemoteAddr),
|
||||
slog.String("method", h.r.Method),
|
||||
slog.String("url", h.r.URL.String()),
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
}
|
||||
|
||||
func (h *HandlerV1) writeJSONError(status int, msg string) {
|
||||
h.w.Header().Set("Content-Type", "application/json")
|
||||
h.w.WriteHeader(status)
|
||||
resp := map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": msg,
|
||||
"code": status,
|
||||
}
|
||||
if err := json.NewEncoder(h.w).Encode(resp); err != nil {
|
||||
h.log.Error("Failed to write JSON error response",
|
||||
slog.String("error", err.Error()),
|
||||
slog.Int("status", status))
|
||||
return
|
||||
}
|
||||
}
|
||||
// func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
// utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
|
||||
// h.log.Error("HTTP request error",
|
||||
// slog.String("remote", h.r.RemoteAddr),
|
||||
// slog.String("method", h.r.Method),
|
||||
// slog.String("url", h.r.URL.String()),
|
||||
// slog.Int("status", http.StatusBadRequest))
|
||||
// }
|
||||
|
||||
func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,14 +38,20 @@ type UpdaterContract interface {
|
||||
}
|
||||
|
||||
type Updater struct {
|
||||
Log slog.Logger
|
||||
Config *config.ConfigConf
|
||||
log *log.Logger
|
||||
config *config.Conf
|
||||
env *config.Env
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewUpdater(log slog.Logger, cfg *config.ConfigConf) *Updater {
|
||||
func NewUpdater(ctx context.Context, log *log.Logger, cfg *config.Conf, env *config.Env) *Updater {
|
||||
return &Updater{
|
||||
Log: log,
|
||||
Config: cfg,
|
||||
log: log,
|
||||
config: cfg,
|
||||
env: env,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +88,11 @@ func isVersionNewer(current, latest Version) bool {
|
||||
if i < len(currentParts) {
|
||||
cur, err := strconv.Atoi(currentParts[i])
|
||||
if err != nil {
|
||||
cur = 0 // или можно обработать ошибку иначе
|
||||
cur = 0
|
||||
}
|
||||
curPart = cur
|
||||
} else {
|
||||
curPart = 0 // Если части в current меньше, считаем недостающие нулями
|
||||
curPart = 0
|
||||
}
|
||||
|
||||
if i < len(latestParts) {
|
||||
@@ -96,31 +111,15 @@ func isVersionNewer(current, latest Version) bool {
|
||||
if curPart > latPart {
|
||||
return false
|
||||
}
|
||||
// если равны — идём дальше
|
||||
}
|
||||
return false // все части равны, значит не новее
|
||||
return false
|
||||
}
|
||||
|
||||
// if len(currentParts) >= 1 && len(latestParts) >= 1 {
|
||||
// if currentParts[0] < latestParts[0] {
|
||||
// if len(currentParts) < 2 || len(latestParts) < 2 {
|
||||
// if currentParts[1] < latestParts[1] {
|
||||
// return true
|
||||
// }
|
||||
// if currentParts[1] > latestParts[1] {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// if currentParts[0] > latestParts[0] {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
|
||||
// GetCurrentVersion reads the current version from the version file and returns it along with the branch.
|
||||
func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
|
||||
version, branch, err := splitVersionString(string(config.GetUpdateConsts().GetNodeVersion()))
|
||||
version, branch, err := splitVersionString(string(config.NodeVersion))
|
||||
if err != nil {
|
||||
u.Log.Error("Failed to parse version string", slog.String("version", string(config.GetUpdateConsts().GetNodeVersion())), slog.String("error", err.Error()))
|
||||
u.log.Printf("Failed to parse version string: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
switch branch {
|
||||
@@ -132,28 +131,28 @@ func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
|
||||
}
|
||||
|
||||
func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error) {
|
||||
repoURL := u.Config.Updates.RepositoryURL
|
||||
repoURL := u.config.Updates.RepositoryURL
|
||||
if repoURL == "" {
|
||||
u.Log.Error("RepositoryURL is empty in config")
|
||||
u.log.Printf("Failed to get latest version: %s", "RepositoryURL is empty in config")
|
||||
return "", "", errors.New("repository URL is empty")
|
||||
}
|
||||
if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") {
|
||||
u.Log.Error("RepositoryURL does not start with http:// or https://", slog.String("RepositoryURL", repoURL))
|
||||
u.log.Printf("Failed to get latest version: %s: %s", "RepositoryURL does not start with http:// or https:/", repoURL)
|
||||
return "", "", errors.New("repository URL must start with http:// or https://")
|
||||
}
|
||||
response, err := http.Get(repoURL + "/" + config.GetUpdateConsts().GetActualFileName())
|
||||
response, err := http.Get(repoURL + "/" + config.ActualFileName)
|
||||
if err != nil {
|
||||
u.Log.Error("Failed to fetch latest version", slog.String("error", err.Error()))
|
||||
u.log.Printf("Failed to fetch latest version: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
u.Log.Error("Failed to fetch latest version", slog.Int("status", response.StatusCode))
|
||||
u.log.Printf("Failed to fetch latest version: HTTP status %d", response.StatusCode)
|
||||
return "", "", errors.New("failed to fetch latest version, status code: " + http.StatusText(response.StatusCode))
|
||||
}
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
u.Log.Error("Failed to read latest version response", slog.String("error", err.Error()))
|
||||
u.log.Printf("Failed to read latest version response: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
@@ -164,14 +163,13 @@ func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error)
|
||||
}
|
||||
version, branch, err := splitVersionString(string(line))
|
||||
if err != nil {
|
||||
u.Log.Error("Failed to parse version string", slog.String("version", string(line)), slog.String("error", err.Error()))
|
||||
u.log.Printf("Failed to parse version string: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
if branch == updateBranch {
|
||||
return Version(version), Branch(branch), nil
|
||||
}
|
||||
}
|
||||
u.Log.Warn("No version found for branch", slog.String("branch", string(updateBranch)))
|
||||
return "", "", errors.New("no version found for branch: " + string(updateBranch))
|
||||
}
|
||||
|
||||
@@ -187,30 +185,151 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, error) {
|
||||
if currentVersion == latestVersion && currentBranch == latestBranch {
|
||||
return false, nil
|
||||
}
|
||||
u.Log.Info("New update available",
|
||||
slog.String("current_version", string(currentVersion)),
|
||||
slog.String("current_branch", string(currentBranch)),
|
||||
slog.String("latest_version", string(latestVersion)),
|
||||
slog.String("latest_branch", string(latestBranch)),
|
||||
)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// func (u *Updater) Update() error {
|
||||
// if !(u.Config.UpdatesEnabled && u.Config.Updates.AllowUpdates && u.Config.Updates.AllowDowngrades) {
|
||||
// u.Log.Info("Updates are disabled in config, skipping update")
|
||||
// return nil
|
||||
// }
|
||||
// wantedVersion := u.Config.Updates.WantedVersion
|
||||
// _, wantedBranch, _ := splitVersionString(wantedVersion)
|
||||
// newVersion, newBranch, err := u.GetLatestVersion(wantedBranch)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if wantedBranch != newBranch {
|
||||
// u.Log.Info("Wanted version branch does not match latest version branch: updating wanted branch",
|
||||
// slog.String("wanted_branch", string(wantedBranch)),
|
||||
// slog.String("latest_branch", string(newBranch)),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
func (u *Updater) Update() error {
|
||||
if !u.config.Updates.UpdatesEnabled {
|
||||
return errors.New("updates are disabled in config, skipping update")
|
||||
}
|
||||
|
||||
if err := run_manager.SetDir("update"); err != nil {
|
||||
return fmt.Errorf("failed to create update dir: %w", err)
|
||||
}
|
||||
|
||||
downloadPath := filepath.Join(run_manager.RuntimeDir(), "update")
|
||||
|
||||
_, currentBranch, err := u.GetCurrentVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current version: %w", err)
|
||||
}
|
||||
latestVersion, latestBranch, err := u.GetLatestVersion(currentBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest version: %w", err)
|
||||
}
|
||||
|
||||
updateArchiveName := fmt.Sprintf("%s.v%s-%s", config.UpdateArchiveName, latestVersion, latestBranch)
|
||||
updateDest := fmt.Sprintf("%s/%s.%s", u.config.Updates.RepositoryURL, updateArchiveName, "tar.gz")
|
||||
|
||||
resp, err := http.Get(updateDest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch archive: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("unexpected HTTP status: %s, body: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
gzReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gzip reader error: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("tar read error: %w", err)
|
||||
}
|
||||
|
||||
relativeParts := strings.SplitN(header.Name, string(os.PathSeparator), 2)
|
||||
if len(relativeParts) < 2 {
|
||||
// It's either a top level directory or garbage.
|
||||
continue
|
||||
}
|
||||
cleanName := relativeParts[1]
|
||||
targetPath := filepath.Join(downloadPath, cleanName)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
return fmt.Errorf("mkdir error: %w", err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err := run_manager.Set(filepath.Join("update", cleanName)); err != nil {
|
||||
return fmt.Errorf("set file error: %w", err)
|
||||
}
|
||||
f := run_manager.File(filepath.Join("update", cleanName))
|
||||
outFile, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file error: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
outFile.Close()
|
||||
return fmt.Errorf("copy file error: %w", err)
|
||||
}
|
||||
outFile.Close()
|
||||
default:
|
||||
return fmt.Errorf("unsupported tar type: %v", header.Typeflag)
|
||||
}
|
||||
}
|
||||
|
||||
return u.InstallAndRestart()
|
||||
}
|
||||
|
||||
func (u *Updater) InstallAndRestart() error {
|
||||
|
||||
nodePath := u.env.NodePath
|
||||
if nodePath == "" {
|
||||
return errors.New("GS_NODE_PATH environment variable is not set")
|
||||
}
|
||||
installDir := filepath.Join(nodePath, "bin")
|
||||
targetPath := filepath.Join(installDir, "node")
|
||||
|
||||
f := run_manager.File("update/node")
|
||||
input, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open new binary: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
output, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create target binary: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
output.Close()
|
||||
return fmt.Errorf("copy failed: %w", err)
|
||||
}
|
||||
output.Close()
|
||||
|
||||
if err := os.Chmod(targetPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to chmod: %w", err)
|
||||
}
|
||||
|
||||
u.log.Printf("Launching new version: path is %s", targetPath)
|
||||
// cmd := exec.Command(targetPath, os.Args[1:]...)
|
||||
// cmd.Env = os.Environ()
|
||||
// cmd.Stdout = os.Stdout
|
||||
// cmd.Stderr = os.Stderr
|
||||
// cmd.Stdin = os.Stdin
|
||||
args := os.Args
|
||||
args[0] = targetPath
|
||||
env := utils.SetEviron(os.Environ(), "GS_PARENT_PID=-1")
|
||||
|
||||
if err := run_manager.Clean(); err != nil {
|
||||
return err
|
||||
}
|
||||
return syscall.Exec(targetPath, args, env)
|
||||
//u.cancel()
|
||||
|
||||
// TODO: fix this crap and find a better way to update without errors
|
||||
// for {
|
||||
// _, err := run_manager.Get("run.lock")
|
||||
// if err != nil {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// return cmd.Start()
|
||||
}
|
||||
|
||||
func (u *Updater) Shutdownfunc(f context.CancelFunc) {
|
||||
u.cancel = f
|
||||
}
|
||||
|
||||
22
core/utils/http_errors.go
Normal file
22
core/utils/http_errors.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// writeJSONError writes a JSON error response to the HTTP response writer.
|
||||
// It sets the Content-Type to application/json, writes the specified HTTP status code
|
||||
func WriteJSONError(w http.ResponseWriter, status int, msg string) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
resp := map[string]any{
|
||||
"status": "error",
|
||||
"error": msg,
|
||||
"code": status,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
26
core/utils/internal_lua.go
Normal file
26
core/utils/internal_lua.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
func ConvertLuaTypesToGolang(value lua.LValue) any {
|
||||
switch value.Type() {
|
||||
case lua.LTString:
|
||||
return value.String()
|
||||
case lua.LTNumber:
|
||||
return float64(value.(lua.LNumber))
|
||||
case lua.LTBool:
|
||||
return bool(value.(lua.LBool))
|
||||
case lua.LTTable:
|
||||
result := make(map[string]interface{})
|
||||
if tbl, ok := value.(*lua.LTable); ok {
|
||||
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
result[key.String()] = ConvertLuaTypesToGolang(value)
|
||||
})
|
||||
}
|
||||
return result
|
||||
case lua.LTNil:
|
||||
return nil
|
||||
default:
|
||||
return value.String()
|
||||
}
|
||||
}
|
||||
122
core/utils/routines.go
Normal file
122
core/utils/routines.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SetEviron(eviron []string, envs ...string) []string {
|
||||
envMap := make(map[string]string)
|
||||
for _, e := range eviron {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
envMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
for _, e := range envs {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
envMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
newEviron := make([]string, 0, len(envMap))
|
||||
for k, v := range envMap {
|
||||
newEviron = append(newEviron, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
return newEviron
|
||||
}
|
||||
func CleanTempRuntimes(pattern string) error {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, path := range matches {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
os.RemoveAll(path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExistsMatchingDirs(pattern, exclude string) (bool, error) {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, path := range matches {
|
||||
if filepath.Clean(path) == filepath.Clean(exclude) {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err == nil && info.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func IndexPaths(runDir string) (map[string]string, error) {
|
||||
indexed := make(map[string]string)
|
||||
|
||||
err := filepath.Walk(runDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(runDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexed[relPath] = path
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return indexed, nil
|
||||
}
|
||||
|
||||
func IsFullyInitialized(i any) bool {
|
||||
v := reflect.ValueOf(i).Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
|
||||
if field.IsNil() {
|
||||
return false
|
||||
}
|
||||
case reflect.String:
|
||||
if field.String() == "" {
|
||||
return false
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if field.Int() == 0 {
|
||||
return false
|
||||
}
|
||||
case reflect.Bool:
|
||||
if !field.Bool() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
48
core/utils/utils_test.go
Normal file
48
core/utils/utils_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFunc_SetEviron(t *testing.T) {
|
||||
tests := []struct {
|
||||
eviron []string
|
||||
envs []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
|
||||
[]string{"ENV3=3"},
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
|
||||
},
|
||||
{
|
||||
[]string{"ENV1=1", "ENV2=5", "ENV3=4"},
|
||||
[]string{"ENV2=2", "ENV3=3"},
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
|
||||
},
|
||||
{
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
|
||||
[]string{"ENV4=4"},
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=3", "ENV4=4"},
|
||||
},
|
||||
{
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
|
||||
[]string{"ENV3=2", "ENV3=3"},
|
||||
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("in %q set new %q", tt.eviron, tt.envs), func(t *testing.T) {
|
||||
got := SetEviron(tt.eviron, tt.envs...)
|
||||
sort.Strings(got)
|
||||
sort.Strings(tt.want)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SetEviron(%q, %q) = got %v; want %v", tt.eviron, tt.envs, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
core/utils/uuid.go
Normal file
41
core/utils/uuid.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
)
|
||||
|
||||
func NewUUIDRaw(length int) ([]byte, error) {
|
||||
bytes := make([]byte, int(length))
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return bytes, errors.New("failed to generate UUID: " + err.Error())
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func NewUUID(length int) (string, error) {
|
||||
data, err := NewUUIDRaw(length)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
func NewUUID32() (string, error) {
|
||||
return NewUUID(config.UUIDLength)
|
||||
}
|
||||
|
||||
func NewUUID32Raw() ([]byte, error) {
|
||||
data, err := NewUUIDRaw(config.UUIDLength)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if len(data) != config.UUIDLength {
|
||||
return data, errors.New("unexpected UUID length")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
21
go.mod
21
go.mod
@@ -9,8 +9,29 @@ require (
|
||||
golang.org/x/net v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||
|
||||
43
go.sum
43
go.sum
@@ -1,18 +1,61 @@
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
|
||||
Reference in New Issue
Block a user