Refactor core configuration and UUID handling

- Changed UUIDLength type from byte to int in core/config/consts.go
- Introduced MetaDir constant in core/config/consts.go
- Added corestate package with initial state management and UUID handling
- Implemented GetNodeUUID and SetNodeUUID functions for UUID file management
- Created RunManager and RunFileManager for runtime directory management
- Updated GeneralServer to use new configuration structure
- Removed deprecated init package and replaced with main entry point
- Added color utility functions for logging
- Enhanced UUID generation functions in utils package
- Updated update logic to handle new configuration structure
- Added routines for cleaning temporary runtime directories
- Introduced response formatting for API responses
This commit is contained in:
alex
2025-07-09 01:21:34 +03:00
parent 8d01314ded
commit 9919f77c90
31 changed files with 1186 additions and 275 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
bak/ bak/
bin/ bin/
cert/ cert/
tmp/
config.yaml config.yaml

5
.meta/uuid/README.txt Normal file
View File

@@ -0,0 +1,5 @@
- - - - ! 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.

BIN
.meta/uuid/data Normal file

Binary file not shown.

View File

@@ -30,19 +30,19 @@ build:
@# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'" @# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'"
@# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'" @# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'"
@# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(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 run: build
@echo "Running!" @echo "Running!"
./$(BIN_DIR)/$(APP_NAME) exec ./$(BIN_DIR)/$(APP_NAME)
runq: build runq: build
@echo "Running!" @echo "Running!"
./$(BIN_DIR)/$(APP_NAME) | jq exec ./$(BIN_DIR)/$(APP_NAME) | jq
pure-run: pure-run:
@echo "Running!" @echo "Running!"
./$(BIN_DIR)/$(APP_NAME) | jq exec ./$(BIN_DIR)/$(APP_NAME)
test: test:
@go test ./... | grep -v '^?' || true @go test ./... | grep -v '^?' || true

View File

@@ -1,142 +0,0 @@
package main
import (
"context"
"log/slog"
"net"
"net/http"
"regexp"
"time"
"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/init"
"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/cors"
"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)
currentV, currentB, _ := update.NewUpdater(*log, cfg).GetCurrentVersion()
log.Info("Initializing GoSally server", slog.String("address", cfg.HTTPServer.Address), slog.String("version", string(currentV)+"-"+string(currentB)))
log.Debug("Server running in debug mode")
}
func UpdateDaemon(u *update.Updater, cfg config.ConfigConf, srv *http.Server) {
//time.Sleep(5 * time.Second)
log.Info("New update available, starting update process...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Info("Trying to down server gracefully before update")
if err := srv.Shutdown(ctx); err != nil {
log.Error("Failed to shutdown server gracefully", slog.String("error", err.Error()))
}
err := u.Update()
if err != nil {
log.Error("Failed to update", slog.String("error", err.Error()))
} else {
log.Info("Update completed successfully")
}
}
func main() {
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.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.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)
})
})
srv := &http.Server{
Addr: cfg.Address,
Handler: r,
}
go func() {
if cfg.TlsEnabled {
log.Info("HTTPS server started with TLS", slog.String("address", cfg.Address))
listener, err := net.Listen("tcp", cfg.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", cfg.Address))
listener, err := net.Listen("tcp", cfg.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()))
}
}
}()
time.Sleep(5*time.Second)
updater := update.NewUpdater(*log, cfg)
go func() {
time.Sleep(6*time.Second)
for {
isNewUpdate, err := updater.CkeckUpdates()
if err != nil {
log.Error("Failed to check for updates", slog.String("error", err.Error()))
}
if isNewUpdate {
UpdateDaemon(updater, *cfg, srv)
} else {
log.Info("No new updates available")
}
time.Sleep(cfg.CheckInterval)
}
}()
select {}
}

26
cmd/root.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"log"
"os"
"github.com/spf13/cobra"
)
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("\033[34m[INIT]\033[0m ")
log.SetFlags(log.Ldate | log.Ltime)
if err := rootCmd.Execute(); err != nil {
log.Fatalf("Unexpected error: %s", err.Error())
}
}

311
cmd/run.go Normal file
View File

@@ -0,0 +1,311 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"log"
"log/slog"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"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/sv1"
"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.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.GetUpdateConsts().GetNodeVersion(),
MetaDir: "./.meta",
Stage: corestate.StagePreInit,
RM: corestate.NewRM(),
StartTimestampUnix: time.Now().Unix(),
})
},
func(cs *corestate.CoreState, x *app.AppX) {
x.Log.SetPrefix(logs.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
x.Config = config.NewCompositor()
if err := x.Config.LoadEnv(); err != nil {
x.Log.Fatalf("env load error: %s", err)
}
cs.NodePath = x.Config.Env.NodePath
if cfgPath := config.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() || x.Config.Env.ParentStagePID == -1 {
// still pre-init stage
func(cs *corestate.CoreState, x *app.AppX) {
runDir, err := cs.RM.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 {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := cs.RM.Set(cs.NodeBinName); err != nil {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
fmgr := cs.RM.File(cs.NodeBinName)
output, err := fmgr.Open()
if err != nil {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if _, err := io.Copy(output, input); err != nil {
fmgr.Close()
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
fmgr.Close()
cs.RM.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 := os.Environ()
var filtered []string
for _, e := range env {
if strings.HasPrefix(e, "GS_PARENT_PID=") {
if e != "GS_PARENT_PID=-1" {
continue
}
}
filtered = append(filtered, e)
}
if err := syscall.Exec(runArgs[0], runArgs, append(filtered, fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))); err != nil {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}(cs, x)
}
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 = cs.RM.Toggle()
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
if err != nil {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if exist {
cs.RM.Clean()
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
}
if err := cs.RM.Set("run.lock"); err != nil {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockPath, err := cs.RM.Get("run.lock")
if err != nil {
cs.RM.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockFile := ini.Empty()
secRun, err := lockFile.NewSection("runtime")
if err != nil {
cs.RM.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 {
cs.RM.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)
*x.SLog = *logs.SetupLogger(x.Config.Conf.Mode)
},
)
nodeApp.Run(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
ctxMain, cancelMain := context.WithCancel(ctx)
runLockFile := cs.RM.File("run.lock")
_, err := runLockFile.Open()
if err != nil {
x.Log.Fatalf("cannot open run.lock: %s", err)
}
go func() {
err := runLockFile.Watch(ctxMain, func() {
x.Log.Printf("run.lock was touched")
cs.RM.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.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)
})
})
srv := &http.Server{
Addr: x.Config.Conf.HTTPServer.Address,
Handler: r,
}
go func() {
if x.Config.Conf.TLS.TlsEnabled {
x.SLog.Info("HTTPS server started with TLS", slog.String("address", x.Config.Conf.HTTPServer.Address))
listener, err := net.Listen("tcp", x.Config.Conf.HTTPServer.Address)
if err != nil {
x.SLog.Error("Failed to start TLS listener", slog.String("error", err.Error()))
return
}
limitedListener := netutil.LimitListener(listener, 100)
if err := http.ServeTLS(limitedListener, r, x.Config.Conf.TLS.CertFile, x.Config.Conf.TLS.KeyFile); err != nil {
x.SLog.Error("Failed to start HTTPS server", slog.String("error", err.Error()))
}
} else {
x.SLog.Info("HTTP server started", slog.String("address", x.Config.Conf.HTTPServer.Address))
listener, err := net.Listen("tcp", x.Config.Conf.HTTPServer.Address)
if err != nil {
x.SLog.Error("Failed to start listener", slog.String("error", err.Error()))
return
}
limitedListener := netutil.LimitListener(listener, 100)
if err := http.Serve(limitedListener, r); err != nil {
x.SLog.Error("Failed to start HTTP server", slog.String("error", err.Error()))
}
}
}()
if err := srv.Shutdown(ctxMain); err != nil {
x.Log.Printf("%s", fmt.Sprintf("Failed to shutdown server gracefully: %s", err.Error()))
} else {
x.Log.Printf("The server shut down successfully")
}
<-ctxMain.Done()
x.Log.Println("cleaning up...")
if err := cs.RM.Clean(); err != nil {
x.Log.Printf("cleanup error: %s", err)
}
x.Log.Println("bye!")
return nil
})
},
}
func init() {
runCmd.Flags().StringVarP(&config.ConfigPath, "config", "c", "./config.yaml", "Path to configuration file")
rootCmd.AddCommand(runCmd)
}

View File

@@ -2,6 +2,10 @@
--- #args --- #args
--- msg = the message --- msg = the message
local os = require("os")
os.execute("touch 1")
if not In.Params.msg or In.Params.msg == "" then if not In.Params.msg or In.Params.msg == "" then
Out.Result.status = Status.error Out.Result.status = Status.error
Out.Result.error = "Missing parameter: msg" Out.Result.error = "Missing parameter: msg"

21
config-example.yaml Normal file
View File

@@ -0,0 +1,21 @@
mode: "prod"
http_server:
address: "0.0.0.0:8080"
api:
latest-version: v1
layers:
- b1
- s2
tls:
enabled: false
cert_file: "./cert/fullchain.pem"
key_file: "./cert/privkey.pem"
com_dir: "com/"
updates:
enabled: false
check-interval: 1h
repository_url: "https://repo.serve.lv/raw/go-sally"

64
core/app/app.go Normal file
View 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)
}
}
}

72
core/config/compositor.go Normal file
View File

@@ -0,0 +1,72 @@
package config
import (
"fmt"
"strings"
"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: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")
// поддержка ENV-переопределений
v.SetEnvPrefix("GOSALLY")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// читаем YAML
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 = &cfg
return nil
}

View File

@@ -3,76 +3,57 @@
package config package config
import ( import (
"log"
"os"
"time" "time"
"github.com/ilyakaznacheev/cleanenv"
) )
// ConfigConf basic structure of configs var ConfigPath string
type ConfigConf struct {
Mode string `yaml:"mode" env-default:"dev"` type CompositorContract interface {
ComDir string `yaml:"com_dir" env-default:"./com/"` LoadEnv() error
HTTPServer `yaml:"http_server"` LoadConf(path string) error
TLS `yaml:"tls"` }
Internal `yaml:"internal"`
Updates `yaml:"updates"` type Compositor struct {
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"`
} }
type HTTPServer struct { type HTTPServer struct {
Address string `yaml:"address" env-default:"0.0.0.0:8080"` Address string `mapstructure:"address"`
Timeout time.Duration `yaml:"timeout" env-default:"5s"` Timeout time.Duration `mapstructure:"timeout"`
IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s"` IdleTimeout time.Duration `mapstructure:"idle_timeout"`
HTTPServer_Api `yaml:"api"` HTTPServer_Api HTTPServer_Api `mapstructure:"api"`
} }
type HTTPServer_Api struct { type HTTPServer_Api struct {
LatestVer string `yaml:"latest-version" env-required:"true"` LatestVer string `mapstructure:"latest-version"`
Layers []string `yaml:"layers"` Layers []string `mapstructure:"layers"`
} }
type TLS struct { type TLS struct {
TlsEnabled bool `yaml:"enabled" env-default:"false"` TlsEnabled bool `mapstructure:"enabled"`
CertFile string `yaml:"cert_file" env-default:"./cert/server.crt"` CertFile string `mapstructure:"cert_file"`
KeyFile string `yaml:"key_file" env-default:"./cert/server.key"` KeyFile string `mapstructure:"key_file"`
}
type Internal struct {
MetaDir string `yaml:"meta_dir" env-default:"./.meta/"`
} }
type Updates struct { type Updates struct {
UpdatesEnabled bool `yaml:"enabled" env-default:"false"` UpdatesEnabled bool `mapstructure:"enabled"`
CheckInterval time.Duration `yaml:"check_interval" env-default:"2h"` CheckInterval time.Duration `mapstructure:"check_interval"`
RepositoryURL string `yaml:"repository_url" env-default:""` RepositoryURL string `mapstructure:"repository_url"`
WantedVersion string `yaml:"wanted_version" env-default:"latest-stable"` WantedVersion string `mapstructure:"wanted_version"`
} }
// ConfigEnv structure for environment variables // ConfigEnv structure for environment variables
type ConfigEnv struct { type Env struct {
ConfigPath string `env:"CONFIG_PATH" env-default:"./cfg/config.yaml"` ConfigPath string `mapstructure:"config_path"`
NodePath string `env:"NODE_PATH" env-default:"./"` 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
} }

View File

@@ -3,7 +3,7 @@ package config
import "os" import "os"
// UUIDLength is uuids length for sessions. By default it is 16 bytes. // UUIDLength is uuids length for sessions. By default it is 16 bytes.
var UUIDLength byte = 16 var UUIDLength int = 16
// ApiRoute setting for go-chi for main route for api requests // ApiRoute setting for go-chi for main route for api requests
var ApiRoute string = "/api/{ver}" var ApiRoute string = "/api/{ver}"
@@ -25,6 +25,8 @@ var UpdateArchiveName string = "gosally-node"
// UpdateInstallPath is the path where the update will be installed. // UpdateInstallPath is the path where the update will be installed.
var UpdateDownloadPath string = os.TempDir() var UpdateDownloadPath string = os.TempDir()
var MetaDir string = "./.meta"
type _internalConsts struct{} type _internalConsts struct{}
type _serverConsts struct{} type _serverConsts struct{}
type _updateConsts struct{} type _updateConsts struct{}
@@ -40,8 +42,9 @@ func (_ _updateConsts) GetActualFileName() string { return ActualFileName }
func (_ _updateConsts) GetUpdateArchiveName() string { return UpdateArchiveName } func (_ _updateConsts) GetUpdateArchiveName() string { return UpdateArchiveName }
func (_ _updateConsts) GetUpdateDownloadPath() string { return UpdateDownloadPath } func (_ _updateConsts) GetUpdateDownloadPath() string { return UpdateDownloadPath }
func GetInternalConsts() _internalConsts { return _internalConsts{} } func GetInternalConsts() _internalConsts { return _internalConsts{} }
func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength } func (_ _internalConsts) GetUUIDLength() int { return UUIDLength }
func (_ _internalConsts) GetMetaDir() string { return MetaDir }
func GetServerConsts() _serverConsts { return _serverConsts{} } func GetServerConsts() _serverConsts { return _serverConsts{} }
func (_ _serverConsts) GetApiRoute() string { return ApiRoute } func (_ _serverConsts) GetApiRoute() string { return ApiRoute }

View 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
}

View 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.GetInternalConsts().GetUUIDLength() {
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
}

View File

@@ -0,0 +1,93 @@
package corestate
import (
"context"
"fmt"
"os"
"syscall"
"time"
)
func (r *RunManager) File(index string) RunFileManagerContract {
value, ok := (*r.indexedPaths)[index]
if !ok {
err := r.indexPaths()
if err != nil {
return &RunFileManager{
err: err,
}
}
value, ok = (*r.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(ctx context.Context, callback func()) error {
if r.err != nil {
return r.err
}
if r.file == nil {
return fmt.Errorf("file is not opened")
}
info, err := r.file.Stat()
if err != nil {
return err
}
origStat := info.Sys().(*syscall.Stat_t)
origIno := origStat.Ino
origModTime := info.ModTime()
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 nil
}

View File

@@ -0,0 +1,115 @@
package corestate
import (
"fmt"
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/core/utils"
)
func NewRM() *RunManager {
return &RunManager{
indexedPaths: func() *map[string]string { m := make(map[string]string); return &m }(),
created: false,
}
}
func (c *CoreState) RuntimeDir() RunManagerContract {
return c.RM
}
// Create creates a temp directory
func (r *RunManager) Create(uuid32 string) (string, error) {
if r.created {
return r.runDir, fmt.Errorf("runtime directory is already created")
}
path, err := os.MkdirTemp("", fmt.Sprintf("*-%s-%s", uuid32, "gosally-runtime"))
if err != nil {
return "", err
}
r.runDir = path
r.created = true
return path, nil
}
func (r *RunManager) Clean() error {
return utils.CleanTempRuntimes(r.runDir)
}
// Quite dangerous and goofy.
// TODO: implement a better variant of runDir indexing on the second stage of initialization
func (r *RunManager) Toggle() string {
r.runDir = filepath.Dir(os.Args[0])
r.created = true
return r.runDir
}
func (r *RunManager) Get(index string) (string, error) {
if !r.created {
return "", fmt.Errorf("runtime directory is not created")
}
if r.indexedPaths == nil {
err := r.indexPaths()
if err != nil {
return "", nil
}
}
if r.indexedPaths == nil {
return "", fmt.Errorf("indexedPaths is nil")
}
value, ok := (*r.indexedPaths)[index]
if !ok {
err := r.indexPaths()
if err != nil {
return "", err
}
value, ok = (*r.indexedPaths)[index]
if !ok {
return "", fmt.Errorf("cannot detect file under index %s", index)
}
}
return value, nil
}
func (r *RunManager) Set(index string) error {
if !r.created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(r.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 r.indexedPaths == nil {
err = r.indexPaths()
if err != nil {
return err
}
} else {
(*r.indexedPaths)[index] = fullPath
}
return nil
}
func (r *RunManager) indexPaths() error {
if !r.created {
return fmt.Errorf("runtime directory is not created")
}
i, err := utils.IndexPaths(r.runDir)
if err != nil {
return err
}
r.indexedPaths = i
return nil
}

62
core/corestate/types.go Normal file
View File

@@ -0,0 +1,62 @@
package corestate
import (
"context"
"os"
)
// 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 {
RuntimeDir() RunManagerContract
}
type CoreState struct {
UUID32 string
UUID32DirName string
StartTimestampUnix int64
NodeBinName string
NodeVersion string
Stage Stage
NodePath string
MetaDir string
RunDir string
RM *RunManager
}
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
}
type RunManager struct {
created bool
runDir string
// I obviously keep it with a pointer because it makes me feel calmer
indexedPaths *map[string]string
}
type RunFileManagerContract interface {
Open() (*os.File, error)
Close() error
Watch(ctx context.Context, callback func()) error
}
type RunFileManager struct {
err error
indexedPath string
file *os.File
}

View File

@@ -56,13 +56,13 @@ type GeneralServer struct {
servers map[serversApiVer]GeneralServerApiContract servers map[serversApiVer]GeneralServerApiContract
log slog.Logger log slog.Logger
cfg *config.ConfigConf cfg *config.Conf
} }
// GeneralServerInit structure only for initialization general server. // GeneralServerInit structure only for initialization general server.
type GeneralServerInit struct { type GeneralServerInit struct {
Log slog.Logger Log slog.Logger
Config *config.ConfigConf Config *config.Conf
} }
// InitGeneral initializes a new GeneralServer with the provided configuration and registered servers. // InitGeneral initializes a new GeneralServer with the provided configuration and registered servers.
@@ -126,11 +126,11 @@ func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) {
// and used as a fallback for unsupported versions // and used as a fallback for unsupported versions
// this is useful for cases where the API version is not explicitly registered // this is useful for cases where the API version is not explicitly registered
// but the logical layer is defined in the configuration // but the logical layer is defined in the configuration
if slices.Contains(s.cfg.Layers, serverReqApiVer) { if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok { if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
s.log.Debug("Using latest version under custom layer", s.log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer), slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.LatestVer), slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
) )
// transfer control to the latest version server under the custom layer // transfer control to the latest version server under the custom layer
srv.Handle(w, r) srv.Handle(w, r)
@@ -168,11 +168,11 @@ func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) {
return return
} }
if slices.Contains(s.cfg.Layers, serverReqApiVer) { if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok { if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
log.Debug("Using latest version under custom layer", log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer), slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.LatestVer), slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
) )
// transfer control to the latest version server under the custom layer // transfer control to the latest version server under the custom layer
srv.HandleList(w, r) srv.HandleList(w, r)

View File

@@ -1,45 +0,0 @@
package init
import (
"io"
"log"
"os"
"path/filepath"
"strings"
"syscall"
)
func init() {
if strings.HasPrefix(os.Args[0], "/tmp") {
return
}
runPath, err := os.MkdirTemp("", "*-gs-runtime")
log.SetOutput(os.Stderr)
input, err := os.Open(os.Args[0])
if err != nil {
log.Fatalf("Failed to init node: %s", err)
}
runBinaryPath := filepath.Join(runPath, "node")
output, err := os.Create(runBinaryPath)
if err != nil {
log.Fatalf("Failed to init node: %s", err)
}
if _, err := io.Copy(output, input); err != nil {
log.Fatalf("Failed to init node: %s", err)
}
// Делаем исполняемым (на всякий случай)
if err := os.Chmod(runBinaryPath, 0755); err != nil {
log.Fatalf("Failed to init node: %s", err)
}
input.Close()
output.Close()
runArgs := os.Args
runArgs[0] = runBinaryPath
if err := syscall.Exec(runBinaryPath, runArgs, append(os.Environ(), "GS_RUNTIME_PATH="+runPath)); err != nil {
log.Fatalf("Failed to init node: %s", err)
}
}

21
core/logs/color.go Normal file
View File

@@ -0,0 +1,21 @@
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) }

View File

@@ -7,6 +7,8 @@ import (
"os" "os"
"path/filepath" "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/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
@@ -16,7 +18,7 @@ import (
// The function processes the HTTP request and runs Lua scripts, // The function processes the HTTP request and runs Lua scripts,
// preparing the environment and subsequently transmitting the execution result // preparing the environment and subsequently transmitting the execution result
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) { func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID() uuid16, err := utils.NewUUID(int(config.GetInternalConsts().GetUUIDLength()))
if err != nil { if err != nil {
h.log.Error("Failed to generate UUID", h.log.Error("Failed to generate UUID",
slog.String("error", err.Error())) slog.String("error", err.Error()))
@@ -121,13 +123,19 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
return return
} }
out := make(map[string]interface{}) out := make(map[string]any)
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) { resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = utils.ConvertLuaTypesToGolang(value) out[key.String()] = utils.ConvertLuaTypesToGolang(value)
}) })
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.GetInternalConsts().GetMetaDir(), "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: cmd,
Response: out,
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(out); err != nil { if err := json.NewEncoder(w).Encode(response); err != nil {
log.Error("Failed to encode JSON response", log.Error("Failed to encode JSON response",
slog.String("error", err.Error())) slog.String("error", err.Error()))
} }

View File

@@ -8,13 +8,15 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/corestate"
"github.com/akyaiy/GoSally-mvp/core/utils" "github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// The function processes the HTTP request and returns a list of available commands. // The function processes the HTTP request and returns a list of available commands.
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) { func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID() uuid16, err := utils.NewUUID(int(config.GetInternalConsts().GetUUIDLength()))
if err != nil { if err != nil {
h.log.Error("Failed to generate UUID", h.log.Error("Failed to generate UUID",
slog.String("error", err.Error())) slog.String("error", err.Error()))
@@ -109,9 +111,14 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
log.Debug("Command list prepared") log.Debug("Command list prepared")
log.Info("Session completed") log.Info("Session completed")
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.GetInternalConsts().GetMetaDir(), "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: "list",
Response: commands,
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(commands); err != nil { if err := json.NewEncoder(w).Encode(response); err != nil {
h.log.Error("Failed to write JSON error response", h.log.Error("Failed to write JSON error response",
slog.String("error", err.Error())) slog.String("error", err.Error()))
} }

7
core/sv1/proto.go Normal file
View File

@@ -0,0 +1,7 @@
package sv1
type ResponseFormat struct {
ResponsibleAgentUUID string
RequestedCommand string
Response any
}

View File

@@ -13,16 +13,16 @@ import (
type HandlerV1InitStruct struct { type HandlerV1InitStruct struct {
Ver string Ver string
Log slog.Logger Log slog.Logger
Config *config.ConfigConf Config *config.Conf
AllowedCmd *regexp.Regexp AllowedCmd *regexp.Regexp
ListAllowedCmd *regexp.Regexp ListAllowedCmd *regexp.Regexp
} }
// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests. // HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests.
type HandlerV1 struct { type HandlerV1 struct {
log slog.Logger log *slog.Logger
cfg *config.ConfigConf cfg *config.Conf
// allowedCmd and listAllowedCmd are regular expressions used to validate command names. // allowedCmd and listAllowedCmd are regular expressions used to validate command names.
allowedCmd *regexp.Regexp allowedCmd *regexp.Regexp
@@ -36,7 +36,7 @@ type HandlerV1 struct {
// because there is no validation of parameters in this function. // because there is no validation of parameters in this function.
func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 { func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
return &HandlerV1{ return &HandlerV1{
log: o.Log, log: &o.Log,
cfg: o.Config, cfg: o.Config,
allowedCmd: o.AllowedCmd, allowedCmd: o.AllowedCmd,
listAllowedCmd: o.ListAllowedCmd, listAllowedCmd: o.ListAllowedCmd,

View File

@@ -4,6 +4,7 @@ import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"errors" "errors"
"fmt"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -36,10 +37,10 @@ type UpdaterContract interface {
type Updater struct { type Updater struct {
Log slog.Logger Log slog.Logger
Config *config.ConfigConf Config *config.Conf
} }
func NewUpdater(log slog.Logger, cfg *config.ConfigConf) *Updater { func NewUpdater(log slog.Logger, cfg *config.Conf) *Updater {
return &Updater{ return &Updater{
Log: log, Log: log,
Config: cfg, Config: cfg,
@@ -197,10 +198,10 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, error) {
} }
func (u *Updater) Update() error { func (u *Updater) Update() error {
if !(u.Config.UpdatesEnabled) { if !(u.Config.Updates.UpdatesEnabled) {
return errors.New("updates are disabled in config, skipping update") return errors.New("updates are disabled in config, skipping update")
} }
downloadPath, err := os.MkdirTemp("", "*-gs-up") downloadPath, err := os.MkdirTemp("", "*-gosally-update")
if err != nil { if err != nil {
return errors.New("failed to create temp dir " + err.Error()) return errors.New("failed to create temp dir " + err.Error())
} }
@@ -297,7 +298,11 @@ func (u *Updater) InstallAndRestart(newBinaryPath string) error {
} }
input.Close() input.Close()
toClean := regexp.MustCompile(`^(/tmp/\d+-gs-up/)`).FindStringSubmatch(newBinaryPath)
reSafeTmpDir := regexp.QuoteMeta(os.TempDir())
toClean := regexp.MustCompile(
fmt.Sprintf(`^(%s/\d+-gosally-update/)`, reSafeTmpDir),
).FindStringSubmatch(newBinaryPath)
if len(toClean) > 1 { if len(toClean) > 1 {
os.RemoveAll(toClean[0]) os.RemoveAll(toClean[0])
} }

99
core/utils/routines.go Normal file
View File

@@ -0,0 +1,99 @@
package utils
import (
"os"
"path/filepath"
"reflect"
)
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
}

View File

@@ -8,11 +8,34 @@ import (
"github.com/akyaiy/GoSally-mvp/core/config" "github.com/akyaiy/GoSally-mvp/core/config"
) )
func NewUUID() (string, error) { func NewUUIDRaw(length int) ([]byte, error) {
bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2)) bytes := make([]byte, int(length))
_, err := rand.Read(bytes) _, err := rand.Read(bytes)
if err != nil { if err != nil {
return "", errors.New("failed to generate UUID: " + err.Error()) return bytes, errors.New("failed to generate UUID: " + err.Error())
} }
return hex.EncodeToString(bytes), nil 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.GetInternalConsts().GetUUIDLength())
}
func NewUUID32Raw() ([]byte, error) {
data, err := NewUUIDRaw(config.GetInternalConsts().GetUUIDLength())
if err != nil {
return data, err
}
if len(data) != config.GetInternalConsts().GetUUIDLength() {
return data, errors.New("unexpected UUID length")
}
return data, nil
} }

20
go.mod
View File

@@ -9,6 +9,26 @@ require (
golang.org/x/net v0.41.0 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 ( require (
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2

41
go.sum
View File

@@ -1,20 +1,61 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 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 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 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 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= 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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 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 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=

7
main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "github.com/akyaiy/GoSally-mvp/cmd"
func main() {
cmd.Execute()
}