diff --git a/.gitignore b/.gitignore index c4330d1..728a29f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bak/ bin/ cert/ +tmp/ config.yaml \ No newline at end of file diff --git a/.meta/uuid/README.txt b/.meta/uuid/README.txt new file mode 100644 index 0000000..b437ea4 --- /dev/null +++ b/.meta/uuid/README.txt @@ -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. \ No newline at end of file diff --git a/.meta/uuid/data b/.meta/uuid/data new file mode 100644 index 0000000..124be2a Binary files /dev/null and b/.meta/uuid/data differ diff --git a/Makefile b/Makefile index 82ebbb6..e17318d 100644 --- a/Makefile +++ b/Makefile @@ -30,19 +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!" - ./$(BIN_DIR)/$(APP_NAME) | jq + exec ./$(BIN_DIR)/$(APP_NAME) test: @go test ./... | grep -v '^?' || true diff --git a/cmd/node/node.go b/cmd/node/node.go deleted file mode 100644 index 9a335c6..0000000 --- a/cmd/node/node.go +++ /dev/null @@ -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 {} -} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..32418c4 --- /dev/null +++ b/cmd/root.go @@ -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()) + } +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..1dac528 --- /dev/null +++ b/cmd/run.go @@ -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) +} diff --git a/com/echo.lua b/com/echo.lua index 04603e6..52c31bb 100644 --- a/com/echo.lua +++ b/com/echo.lua @@ -2,6 +2,10 @@ --- #args --- msg = the message +local os = require("os") + +os.execute("touch 1") + if not In.Params.msg or In.Params.msg == "" then Out.Result.status = Status.error Out.Result.error = "Missing parameter: msg" diff --git a/config-example.yaml b/config-example.yaml new file mode 100644 index 0000000..e1111ff --- /dev/null +++ b/config-example.yaml @@ -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" \ No newline at end of file diff --git a/core/app/app.go b/core/app/app.go new file mode 100644 index 0000000..6407094 --- /dev/null +++ b/core/app/app.go @@ -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) + } + } +} diff --git a/core/config/compositor.go b/core/config/compositor.go new file mode 100644 index 0000000..bce2518 --- /dev/null +++ b/core/config/compositor.go @@ -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 +} diff --git a/core/config/config.go b/core/config/config.go index a4d7594..f9ab69d 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -3,76 +3,57 @@ 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"` +var ConfigPath string + +type CompositorContract interface { + LoadEnv() error + LoadConf(path string) error +} + +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 { - 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"` + 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"` - 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"` } // ConfigEnv structure for environment variables -type ConfigEnv struct { - ConfigPath string `env:"CONFIG_PATH" env-default:"./cfg/config.yaml"` - NodePath string `env:"NODE_PATH" env-default:"./"` -} - -// 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 Env struct { + ConfigPath string `mapstructure:"config_path"` + NodePath string `mapstructure:"node_path"` + ParentStagePID int `mapstructure:"parent_pid"` } diff --git a/core/config/consts.go b/core/config/consts.go index 4d0a67e..485a0ac 100644 --- a/core/config/consts.go +++ b/core/config/consts.go @@ -3,7 +3,7 @@ package config import "os" // 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 var ApiRoute string = "/api/{ver}" @@ -25,6 +25,8 @@ var UpdateArchiveName string = "gosally-node" // UpdateInstallPath is the path where the update will be installed. var UpdateDownloadPath string = os.TempDir() +var MetaDir string = "./.meta" + type _internalConsts struct{} type _serverConsts struct{} type _updateConsts struct{} @@ -40,8 +42,9 @@ func (_ _updateConsts) GetActualFileName() string { return ActualFileName } func (_ _updateConsts) GetUpdateArchiveName() string { return UpdateArchiveName } func (_ _updateConsts) GetUpdateDownloadPath() string { return UpdateDownloadPath } -func GetInternalConsts() _internalConsts { return _internalConsts{} } -func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength } +func GetInternalConsts() _internalConsts { return _internalConsts{} } +func (_ _internalConsts) GetUUIDLength() int { return UUIDLength } +func (_ _internalConsts) GetMetaDir() string { return MetaDir } func GetServerConsts() _serverConsts { return _serverConsts{} } func (_ _serverConsts) GetApiRoute() string { return ApiRoute } diff --git a/core/corestate/corestate.go b/core/corestate/corestate.go new file mode 100644 index 0000000..3b9ae2b --- /dev/null +++ b/core/corestate/corestate.go @@ -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 +} diff --git a/core/corestate/node_uuid.go b/core/corestate/node_uuid.go new file mode 100644 index 0000000..98e695e --- /dev/null +++ b/core/corestate/node_uuid.go @@ -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 +} diff --git a/core/corestate/run_file_manager.go b/core/corestate/run_file_manager.go new file mode 100644 index 0000000..413ea1e --- /dev/null +++ b/core/corestate/run_file_manager.go @@ -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 +} diff --git a/core/corestate/run_manager.go b/core/corestate/run_manager.go new file mode 100644 index 0000000..0658735 --- /dev/null +++ b/core/corestate/run_manager.go @@ -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 +} diff --git a/core/corestate/types.go b/core/corestate/types.go new file mode 100644 index 0000000..ba8f7c3 --- /dev/null +++ b/core/corestate/types.go @@ -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 +} diff --git a/core/general_server/handle_multi.go b/core/general_server/handle_multi.go index f0c1e7a..b693a63 100644 --- a/core/general_server/handle_multi.go +++ b/core/general_server/handle_multi.go @@ -56,13 +56,13 @@ type GeneralServer struct { servers map[serversApiVer]GeneralServerApiContract log slog.Logger - cfg *config.ConfigConf + cfg *config.Conf } // GeneralServerInit structure only for initialization general server. type GeneralServerInit struct { Log slog.Logger - Config *config.ConfigConf + Config *config.Conf } // 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 // 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.Layers, serverReqApiVer) { - if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok { + 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.LatestVer), + 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) @@ -168,11 +168,11 @@ func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) { return } - if slices.Contains(s.cfg.Layers, serverReqApiVer) { - if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok { + 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.LatestVer), + 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) diff --git a/core/init/init.go b/core/init/init.go deleted file mode 100644 index eeea839..0000000 --- a/core/init/init.go +++ /dev/null @@ -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) - } -} diff --git a/core/logs/color.go b/core/logs/color.go new file mode 100644 index 0000000..9e85e37 --- /dev/null +++ b/core/logs/color.go @@ -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) } diff --git a/core/sv1/handle_com.go b/core/sv1/handle_com.go index ef664bc..2bcd4f2 100644 --- a/core/sv1/handle_com.go +++ b/core/sv1/handle_com.go @@ -7,6 +7,8 @@ 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" @@ -16,7 +18,7 @@ import ( // 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() + uuid16, err := utils.NewUUID(int(config.GetInternalConsts().GetUUIDLength())) if err != nil { h.log.Error("Failed to generate UUID", slog.String("error", err.Error())) @@ -121,13 +123,19 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) { return } - out := make(map[string]interface{}) + out := make(map[string]any) resultTbl.ForEach(func(key lua.LValue, value lua.LValue) { 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") - 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", slog.String("error", err.Error())) } diff --git a/core/sv1/handle_list.go b/core/sv1/handle_list.go index be73cca..35a78ec 100644 --- a/core/sv1/handle_list.go +++ b/core/sv1/handle_list.go @@ -8,13 +8,15 @@ 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" ) // 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() + uuid16, err := utils.NewUUID(int(config.GetInternalConsts().GetUUIDLength())) if err != nil { h.log.Error("Failed to generate UUID", 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.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") - 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", slog.String("error", err.Error())) } diff --git a/core/sv1/proto.go b/core/sv1/proto.go new file mode 100644 index 0000000..23b238f --- /dev/null +++ b/core/sv1/proto.go @@ -0,0 +1,7 @@ +package sv1 + +type ResponseFormat struct { + ResponsibleAgentUUID string + RequestedCommand string + Response any +} diff --git a/core/sv1/server.go b/core/sv1/server.go index 92c543d..3dca5bc 100644 --- a/core/sv1/server.go +++ b/core/sv1/server.go @@ -13,16 +13,16 @@ import ( 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 { - 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 @@ -36,7 +36,7 @@ type HandlerV1 struct { // 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, diff --git a/core/update/update.go b/core/update/update.go index 80e57b6..b072fa3 100644 --- a/core/update/update.go +++ b/core/update/update.go @@ -4,6 +4,7 @@ import ( "archive/tar" "compress/gzip" "errors" + "fmt" "io" "log/slog" "net/http" @@ -36,10 +37,10 @@ type UpdaterContract interface { type Updater struct { 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{ Log: log, Config: cfg, @@ -197,10 +198,10 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, 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") } - downloadPath, err := os.MkdirTemp("", "*-gs-up") + downloadPath, err := os.MkdirTemp("", "*-gosally-update") if err != nil { return errors.New("failed to create temp dir " + err.Error()) } @@ -297,7 +298,11 @@ func (u *Updater) InstallAndRestart(newBinaryPath string) error { } 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 { os.RemoveAll(toClean[0]) } diff --git a/core/utils/routines.go b/core/utils/routines.go new file mode 100644 index 0000000..4ef6984 --- /dev/null +++ b/core/utils/routines.go @@ -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 +} diff --git a/core/utils/uuid.go b/core/utils/uuid.go index 23719ae..e8805fc 100644 --- a/core/utils/uuid.go +++ b/core/utils/uuid.go @@ -8,11 +8,34 @@ import ( "github.com/akyaiy/GoSally-mvp/core/config" ) -func NewUUID() (string, error) { - bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2)) +func NewUUIDRaw(length int) ([]byte, error) { + bytes := make([]byte, int(length)) _, err := rand.Read(bytes) 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 } diff --git a/go.mod b/go.mod index 25f0842..867d3f7 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,26 @@ 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 diff --git a/go.sum b/go.sum index a72ba4a..ed13a07 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a6eebf9 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/akyaiy/GoSally-mvp/cmd" + +func main() { + cmd.Execute() +}