Compare commits

18 Commits

Author SHA1 Message Date
alex
deef4a891b logs 2025-07-12 13:25:42 +03:00
alex
aa45a2e3e4 Some changes with http server 2025-07-12 13:20:25 +03:00
alex
29d9f0487b Eliminating lint static checker's whining 2025-07-10 18:22:53 +03:00
alex
ab8da05367 Fix README.md 2025-07-10 18:04:53 +03:00
alex
53572e4628 chore: remove tracked directory to apply .gitignore 2025-07-10 18:04:39 +03:00
alex
5bc334fd2c Fixing updates 2025-07-10 18:03:30 +03:00
alex
e71d69f3f1 chore: add .meta/ to .gitignore 2025-07-09 01:24:49 +03:00
alex
cbc9a554df chore: remove tracked directory to apply .gitignore 2025-07-09 01:24:39 +03:00
alex
3e03c39644 Update README to reflect command usage and response structure 2025-07-09 01:23:18 +03:00
alex
9919f77c90 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
2025-07-09 01:21:34 +03:00
alex
8d01314ded chore: remove tracked file to apply .gitignore 2025-07-08 23:55:14 +03:00
alex
d78510e2e4 Update .gitignore to include cert/ and config.yaml 2025-07-08 23:53:43 +03:00
alex
f90062efa7 Disable TLS in configuration 2025-07-06 09:13:57 +03:00
alex
90c7b1cd70 Enhance server update process with graceful shutdown; enable TLS in config and clean up temporary files during update 2025-07-06 09:13:37 +03:00
alex
66f3d12412 Implement automatic update functionality and improve server initialization; add NODE_PATH to Makefile, enhance logging, and update README 2025-07-05 22:13:16 +03:00
alex
2fdc32ce9f Refactor error handling and utility functions; remove deprecated code and improve logging 2025-07-05 16:05:03 +03:00
alex
b70819e976 Add GeneralServer implementation for API request routing based on versioning 2025-07-05 15:04:39 +03:00
alex
7093183140 Fix directory tree representation in README.md to accurately reflect file count 2025-07-05 13:50:41 +03:00
36 changed files with 2013 additions and 603 deletions

7
.gitignore vendored
View File

@@ -1,3 +1,8 @@
bak/
bin/
cert/
cert/
tmp/
.meta/
.vscode
config.yaml

View File

@@ -2,6 +2,8 @@ APP_NAME := node
BIN_DIR := bin
GOPATH := $(shell go env GOPATH)
export CONFIG_PATH := ./config.yaml
export NODE_PATH := $(shell pwd)
LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/core/config.NodeVersion=v0.0.1-dev'
CGO_CFLAGS := -I/usr/local/include
CGO_LDFLAGS := -L/usr/local/lib -llua5.1 -lm -ldl
@@ -28,15 +30,19 @@ build:
@# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'"
@# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'"
@# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)"
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./cmd/$(APP_NAME)
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./
run: build
@echo "Running!"
./$(BIN_DIR)/$(APP_NAME)
exec ./$(BIN_DIR)/$(APP_NAME)
runq: build
@echo "Running!"
./$(BIN_DIR)/$(APP_NAME) | jq
exec ./$(BIN_DIR)/$(APP_NAME) | jq
pure-run:
@echo "Running!"
exec ./$(BIN_DIR)/$(APP_NAME)
test:
@go test ./... | grep -v '^?' || true

View File

@@ -4,7 +4,7 @@
System that allows you to build your own infrastructure based on identical nodes and various scripts written using built-in Lua 5.1, shebang scripts (scripts that start with the `#!` symbols), compiled binaries.
### Features
Go Sally is not viable at the moment, but it already has the ability to run embedded scripts, log slog events to stdout, and handle RPC like requests.
Go Sally is not viable at the moment, but it already has the ability to run embedded scripts, log slog events to stdout, handle RPC like requests, and independent automatic update from the repository (my pride, to be honest).
### Example of use
The basic directory tree looks something like this
@@ -13,20 +13,21 @@ The basic directory tree looks something like this
├── bin
│   └── node Node core binary file
├── com
├── echo.lua
├── _globals.lua Declaring global variables and functions for all internal scripts (also required for luarc to work correctly)
└── _prepare.lua Script that is executed before each script launch
└── Makefile
   ├── echo.lua
   ├── _globals.lua Declaring global variables and functions for all internal scripts (also required for luarc to work correctly)
   └── _prepare.lua Script that is executed before each script launch
└── config.yaml
3 directories, 5 files
3 directories, 4 files
```
Launch by command
```bash
$ make run
./bin/node run
```
or for structured logs
```bash
$ make run
./bin/node run | jq
```
Example of GET request to server
@@ -36,9 +37,14 @@ curl -s http://localhost:8080/api/v1/com/echo?msg=Hello
Then the response from the server
```json
{
"answer": "Hello",
"status": "ok"
"ResponsibleAgentUUID": "4593a87000bbe088f4e79c477e9c90d3",
"RequestedCommand": "echo",
"Response": {
"answer": "Hello",
"status": "ok"
}
}
```
### How to install

View File

@@ -1,94 +0,0 @@
package main
import (
"fmt"
"log/slog"
"net"
"net/http"
"regexp"
"golang.org/x/net/netutil"
"github.com/akyaiy/GoSally-mvp/core/config"
gs "github.com/akyaiy/GoSally-mvp/core/general_server"
"github.com/akyaiy/GoSally-mvp/core/logs"
"github.com/akyaiy/GoSally-mvp/core/sv1"
"github.com/akyaiy/GoSally-mvp/core/update"
"github.com/go-chi/chi/v5"
)
var log *slog.Logger
var cfg *config.ConfigConf
func init() {
cfg = config.MustLoadConfig()
log = logs.SetupLogger(cfg.Mode)
log = log.With("mode", cfg.Mode)
log.Info("Initializing server", slog.String("address", cfg.HTTPServer.Address))
log.Debug("Server running in debug mode")
}
func main() {
updater := update.NewUpdater(*log, cfg)
versuion, versionType, _ := updater.GetCurrentVersion()
fmt.Printf("Current version: %s (%s)\n", versuion, versionType)
ver, vert, _ := updater.GetLatestVersion(versionType)
fmt.Printf("Latest version: %s (%s)\n", ver, vert)
fmt.Println("Checking for updates...")
isNewUpdate, _ := updater.CkeckUpdates()
fmt.Println("Update check result:", isNewUpdate)
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
Log: *log,
Config: cfg,
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`),
ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`),
Ver: "v1",
})
s := gs.InitGeneral(&gs.GeneralServerInit{
Log: *log,
Config: cfg,
}, serverv1)
r := chi.NewRouter()
r.Route(config.GetServerConsts().GetApiRoute()+config.GetServerConsts().GetComDirRoute(), func(r chi.Router) {
r.Get("/", s.HandleList)
r.Get("/{cmd}", s.Handle)
})
r.Route("/favicon.ico", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
})
r.NotFound(serverv1.ErrNotFound)
address := cfg.Address
if cfg.TlsEnabled {
log.Info("HTTPS server started with TLS", slog.String("address", address))
listener, err := net.Listen("tcp", address)
if err != nil {
log.Error("Failed to start TLS listener", slog.String("error", err.Error()))
return
}
limitedListener := netutil.LimitListener(listener, 100)
err = http.ServeTLS(limitedListener, r, cfg.CertFile, cfg.KeyFile)
if err != nil {
log.Error("Failed to start HTTPS server", slog.String("error", err.Error()))
}
} else {
log.Info("HTTP server started", slog.String("address", address))
listener, err := net.Listen("tcp", address)
if err != nil {
log.Error("Failed to start listener", slog.String("error", err.Error()))
return
}
limitedListener := netutil.LimitListener(listener, 100)
err = http.Serve(limitedListener, r)
if err != nil {
log.Error("Failed to start HTTP server", slog.String("error", err.Error()))
}
}
}

34
cmd/root.go Normal file
View File

@@ -0,0 +1,34 @@
package cmd
import (
"fmt"
"log"
"os"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/corestate"
"github.com/akyaiy/GoSally-mvp/core/logs"
"github.com/spf13/cobra"
)
var compositor *config.Compositor = config.NewCompositor()
var rootCmd = &cobra.Command{
Use: "node",
Short: "Go Sally node",
Long: "Main node runner for Go Sally",
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}
func Execute() {
log.SetOutput(os.Stdout)
log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady)))
log.SetFlags(log.Ldate | log.Ltime)
compositor.LoadCMDLine(rootCmd)
_ = rootCmd.Execute()
// if err := rootCmd.Execute(); err != nil {
// log.Fatalf("Unexpected error: %s", err.Error())
// }
}

326
cmd/run.go Normal file
View File

@@ -0,0 +1,326 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"log"
"log/slog"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"syscall"
"time"
"github.com/akyaiy/GoSally-mvp/core/app"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/corestate"
gs "github.com/akyaiy/GoSally-mvp/core/general_server"
"github.com/akyaiy/GoSally-mvp/core/logs"
"github.com/akyaiy/GoSally-mvp/core/run_manager"
"github.com/akyaiy/GoSally-mvp/core/sv1"
"github.com/akyaiy/GoSally-mvp/core/update"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/spf13/cobra"
"golang.org/x/net/netutil"
"gopkg.in/ini.v1"
)
var runCmd = &cobra.Command{
Use: "run",
Short: "Run node normally",
Run: func(cmd *cobra.Command, args []string) {
nodeApp := app.New()
nodeApp.InitialHooks(
func(cs *corestate.CoreState, x *app.AppX) {
x.Config = compositor
x.Log.SetOutput(os.Stdout)
x.Log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
x.Log.SetFlags(log.Ldate | log.Ltime)
},
// First stage: pre-init
func(cs *corestate.CoreState, x *app.AppX) {
*cs = *corestate.NewCorestate(&corestate.CoreState{
UUID32DirName: "uuid",
NodeBinName: filepath.Base(os.Args[0]),
NodeVersion: config.NodeVersion,
MetaDir: "./.meta",
Stage: corestate.StagePreInit,
StartTimestampUnix: time.Now().Unix(),
})
},
func(cs *corestate.CoreState, x *app.AppX) {
x.Log.SetPrefix(logs.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
if err := x.Config.LoadEnv(); err != nil {
x.Log.Fatalf("env load error: %s", err)
}
cs.NodePath = x.Config.Env.NodePath
if cfgPath := x.Config.CMDLine.Run.ConfigPath; cfgPath != "" {
x.Config.Env.ConfigPath = cfgPath
}
if err := x.Config.LoadConf(x.Config.Env.ConfigPath); err != nil {
x.Log.Fatalf("conf load error: %s", err)
}
},
func(cs *corestate.CoreState, x *app.AppX) {
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if errors.Is(err, fs.ErrNotExist) {
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
x.Log.Fatalf("Cannod generate node uuid: %s", err.Error())
}
uuid32, err = corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if err != nil {
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
if err != nil {
x.Log.Fatalf("uuid load error: %s", err)
}
cs.UUID32 = uuid32
},
func(cs *corestate.CoreState, x *app.AppX) {
if x.Config.Env.ParentStagePID != os.Getpid() {
if os.TempDir() != "/tmp" {
x.Log.Printf("%s: %s", logs.PrintWarn(), "Non-standard value specified for temporary directory")
}
// still pre-init stage
runDir, err := run_manager.Create(cs.UUID32)
if err != nil {
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
cs.RunDir = runDir
input, err := os.Open(os.Args[0])
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := run_manager.Set(cs.NodeBinName); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
fmgr := run_manager.File(cs.NodeBinName)
output, err := fmgr.Open()
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if _, err := io.Copy(output, input); err != nil {
fmgr.Close()
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
fmgr.Close()
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
input.Close()
fmgr.Close()
runArgs := os.Args
runArgs[0] = filepath.Join(cs.RunDir, cs.NodeBinName)
// prepare environ
env := utils.SetEviron(os.Environ(), fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))
if err := syscall.Exec(runArgs[0], runArgs, env); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
x.Log.Printf("Node uuid is %s", cs.UUID32)
},
// post-init stage
func(cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StagePostInit
x.Log.SetPrefix(logs.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
cs.RunDir = run_manager.Toggle()
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if exist {
_ = run_manager.Clean()
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
}
if err := run_manager.Set("run.lock"); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockPath, err := run_manager.Get("run.lock")
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockFile := ini.Empty()
secRun, err := lockFile.NewSection("runtime")
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
secRun.Key("pid").SetValue(fmt.Sprintf("%d/%d", os.Getpid(), x.Config.Env.ParentStagePID))
secRun.Key("version").SetValue(cs.NodeVersion)
secRun.Key("uuid").SetValue(cs.UUID32)
secRun.Key("timestamp").SetValue(time.Unix(cs.StartTimestampUnix, 0).Format("2006-01-02/15:04:05 MST"))
secRun.Key("timestamp-unix").SetValue(fmt.Sprintf("%d", cs.StartTimestampUnix))
err = lockFile.SaveTo(lockPath)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
},
func(cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StageReady
x.Log.SetPrefix(logs.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
x.SLog = new(slog.Logger)
newSlog, err := logs.SetupLogger(x.Config.Conf.Log)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
*x.SLog = *newSlog
},
)
nodeApp.Run(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
ctxMain, cancelMain := context.WithCancel(ctx)
runLockFile := run_manager.File("run.lock")
_, err := runLockFile.Open()
if err != nil {
x.Log.Fatalf("cannot open run.lock: %s", err)
}
_, err = runLockFile.Watch(ctxMain, func() {
x.Log.Printf("run.lock was touched")
_ = run_manager.Clean()
cancelMain()
})
if err != nil {
x.Log.Printf("watch error: %s", err)
}
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
Log: *x.SLog,
Config: x.Config.Conf,
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`),
ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`),
Ver: "v1",
})
s := gs.InitGeneral(&gs.GeneralServerInit{
Log: x.SLog,
Config: x.Config.Conf,
}, serverv1)
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300,
}))
r.HandleFunc(config.ComDirRoute, s.Handle)
r.Route("/favicon.ico", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
})
srv := &http.Server{
Addr: x.Config.Conf.HTTPServer.Address,
Handler: r,
ErrorLog: log.New(&logs.SlogWriter{
Logger: x.SLog,
Level: logs.GlobalLevel,
}, "", 0),
}
go func() {
if x.Config.Conf.TLS.TlsEnabled {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
if err != nil {
x.Log.Printf("%s: Failed to start TLS listener: %s", logs.PrintError(), err.Error())
return
}
x.Log.Printf("Serving on %s port %s with TLS... (https://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
limitedListener := netutil.LimitListener(listener, 100)
if err := srv.ServeTLS(limitedListener, x.Config.Conf.TLS.CertFile, x.Config.Conf.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
x.Log.Printf("%s: Failed to start HTTPS server: %s", logs.PrintError(), err.Error())
}
} else {
x.Log.Printf("Serving on %s port %s... (http://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
if err != nil {
x.Log.Printf("%s: Failed to start listener: %s", logs.PrintError(), err.Error())
return
}
limitedListener := netutil.LimitListener(listener, 100)
if err := srv.Serve(limitedListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
x.Log.Printf("%s: Failed to start HTTP server: %s", logs.PrintError(), err.Error())
}
}
}()
if x.Config.Conf.Updates.UpdatesEnabled {
go func() {
x.Updated = update.NewUpdater(ctxMain, x.Log, x.Config.Conf, x.Config.Env)
x.Updated.Shutdownfunc(cancelMain)
for {
isNewUpdate, err := x.Updated.CkeckUpdates()
if err != nil {
x.Log.Printf("Failed to check for updates: %s", err.Error())
}
if isNewUpdate {
if err := x.Updated.Update(); err != nil {
x.Log.Printf("Failed to update: %s", err.Error())
} else {
x.Log.Printf("Update completed successfully")
}
}
time.Sleep(x.Config.Conf.Updates.CheckInterval)
}
}()
}
<-ctxMain.Done()
if err := srv.Shutdown(ctxMain); err != nil {
x.Log.Printf("%s: Failed to stop the server gracefully: %s", logs.PrintError(), err.Error())
} else {
x.Log.Printf("Server stopped gracefully")
}
x.Log.Println("Cleaning up...")
if err := run_manager.Clean(); err != nil {
x.Log.Printf("%s: Cleanup error: %s", logs.PrintError(), err.Error())
}
x.Log.Println("bye!")
return nil
})
},
}
func init() {
rootCmd.AddCommand(runCmd)
}

View File

@@ -1,16 +0,0 @@
if not Params.f then
Result.status = "error"
Result.error = "Missing parameter: f"
return
end
local code = os.execute("touch " .. Params.f)
if code ~= 0 then
Result.status = "error"
Result.message = "Failed to execute command"
return
end
Result.status = "ok"
Result.message = "Command executed successfully"

View File

@@ -1,9 +1,7 @@
mode: "dev"
mode: "prod"
http_server:
address: "0.0.0.0:8080"
timeout: 3s
idle_timeout: 30s
api:
latest-version: v1
layers:
@@ -15,18 +13,9 @@ tls:
cert_file: "./cert/fullchain.pem"
key_file: "./cert/privkey.pem"
internal:
meta-dir: "./.meta/"
com_dir: "com/"
updates:
enabled: true
allow-auto-updates: true
allow-updates: true
allow-downgrades: false
check-interval: 1h
repository_url: "https://repo.serve.lv/raw/go-sally"
wanted-version: "latest-stable"
repository_url: "https://repo.serve.lv/raw/go-sally"

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

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

@@ -0,0 +1,182 @@
package config
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func NewCompositor() *Compositor {
return &Compositor{}
}
func (c *Compositor) LoadEnv() error {
v := viper.New()
// defaults
v.SetDefault("config_path", "./cfg/config.yaml")
v.SetDefault("node_path", "./")
v.SetDefault("parent_pid", -1)
// GS_*
v.SetEnvPrefix("GS")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
var env Env
if err := v.Unmarshal(&env); err != nil {
return fmt.Errorf("error unmarshaling env: %w", err)
}
c.Env = &env
return nil
}
func (c *Compositor) LoadConf(path string) error {
v := viper.New()
v.SetConfigFile(path)
v.SetConfigType("yaml")
// defaults
v.SetDefault("mode", "dev")
v.SetDefault("com_dir", "./com/")
v.SetDefault("http_server.address", "0.0.0.0")
v.SetDefault("http_server.port", "8080")
v.SetDefault("http_server.timeout", "5s")
v.SetDefault("http_server.idle_timeout", "60s")
v.SetDefault("tls.enabled", false)
v.SetDefault("tls.cert_file", "./cert/server.crt")
v.SetDefault("tls.key_file", "./cert/server.key")
v.SetDefault("updates.enabled", false)
v.SetDefault("updates.check_interval", "2h")
v.SetDefault("updates.wanted_version", "latest-stable")
v.SetDefault("log.level", "info")
v.SetDefault("log.out_path", "")
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("error reading config: %w", err)
}
var cfg Conf
if err := v.Unmarshal(&cfg); err != nil {
return fmt.Errorf("error unmarshaling config: %w", err)
}
c.Conf = &Conf{}
c.Conf = &cfg
return nil
}
func (c *Compositor) LoadCMDLine(root *cobra.Command) {
cmdLine := &CMDLine{}
c.CMDLine = cmdLine
t := reflect.TypeOf(cmdLine).Elem()
v := reflect.ValueOf(cmdLine).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldVal := v.Field(i)
ptr := fieldVal.Addr().Interface()
use := strings.ToLower(field.Name)
var cmd *cobra.Command
for _, sub := range root.Commands() {
if sub.Use == use {
cmd = sub
break
}
}
if use == root.Use {
cmd = root
}
if cmd == nil {
continue
}
Unmarshal(cmd, ptr)
}
}
func Unmarshal(cmd *cobra.Command, target any) {
t := reflect.TypeOf(target).Elem()
v := reflect.ValueOf(target).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
valPtr := v.Field(i).Addr().Interface()
full := field.Tag.Get("full")
short := field.Tag.Get("short")
def := field.Tag.Get("def")
desc := field.Tag.Get("desc")
isPersistent := field.Tag.Get("persistent") == "true"
flagSet := cmd.Flags()
if isPersistent {
flagSet = cmd.PersistentFlags()
}
switch field.Type.Kind() {
case reflect.String:
flagSet.StringVarP(valPtr.(*string), full, short, def, desc)
case reflect.Bool:
defVal, err := strconv.ParseBool(def)
if err != nil && def != "" {
fmt.Printf("warning: cannot parse default bool: %q\n", def)
}
flagSet.BoolVarP(valPtr.(*bool), full, short, defVal, desc)
case reflect.Int:
defVal, err := strconv.Atoi(def)
if err != nil && def != "" {
fmt.Printf("warning: cannot parse default int: %q\n", def)
}
flagSet.IntVarP(valPtr.(*int), full, short, defVal, desc)
case reflect.Slice:
elemKind := field.Type.Elem().Kind()
switch elemKind {
case reflect.String:
defVals := []string{}
if def != "" {
defVals = strings.Split(def, ",")
}
flagSet.StringSliceVarP(valPtr.(*[]string), full, short, defVals, desc)
case reflect.Int:
var intVals []int
if def != "" {
for _, s := range strings.Split(def, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
n, err := strconv.Atoi(s)
if err != nil {
fmt.Printf("warning: cannot parse int in slice: %q\n", s)
continue
}
intVals = append(intVals, n)
}
}
flagSet.IntSliceVarP(valPtr.(*[]int), full, short, intVals, desc)
default:
fmt.Printf("unsupported slice element type: %s\n", elemKind)
}
default:
fmt.Printf("unsupported field type: %s\n", field.Type.Kind())
}
}
}

View File

@@ -3,78 +3,77 @@
package config
import (
"log"
"os"
"time"
"github.com/ilyakaznacheev/cleanenv"
)
// ConfigConf basic structure of configs
type ConfigConf struct {
Mode string `yaml:"mode" env-default:"dev"`
ComDir string `yaml:"com_dir" env-default:"./com/"`
HTTPServer `yaml:"http_server"`
TLS `yaml:"tls"`
Internal `yaml:"internal"`
Updates `yaml:"updates"`
type CompositorContract interface {
LoadEnv() error
LoadConf(path string) error
}
type Compositor struct {
CMDLine *CMDLine
Conf *Conf
Env *Env
}
type Conf struct {
Mode string `mapstructure:"mode"`
ComDir string `mapstructure:"com_dir"`
HTTPServer HTTPServer `mapstructure:"http_server"`
TLS TLS `mapstructure:"tls"`
Updates Updates `mapstructure:"updates"`
Log Log `mapstructure:"log"`
}
type HTTPServer struct {
Address string `yaml:"address" env-default:"0.0.0.0:8080"`
Timeout time.Duration `yaml:"timeout" env-default:"5s"`
IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s"`
HTTPServer_Api `yaml:"api"`
Address string `mapstructure:"address"`
Port string `mapstructure:"port"`
Timeout time.Duration `mapstructure:"timeout"`
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
HTTPServer_Api HTTPServer_Api `mapstructure:"api"`
}
type HTTPServer_Api struct {
LatestVer string `yaml:"latest-version" env-required:"true"`
Layers []string `yaml:"layers"`
LatestVer string `mapstructure:"latest-version"`
Layers []string `mapstructure:"layers"`
}
type TLS struct {
TlsEnabled bool `yaml:"enabled" env-default:"false"`
CertFile string `yaml:"cert_file" env-default:"./cert/server.crt"`
KeyFile string `yaml:"key_file" env-default:"./cert/server.key"`
}
type Internal struct {
MetaDir string `yaml:"meta_dir" env-default:"./.meta/"`
TlsEnabled bool `mapstructure:"enabled"`
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
}
type Updates struct {
UpdatesEnabled bool `yaml:"enabled" env-default:"false"`
AllowAutoUpdates bool `yaml:"allow_auto_updates" env-default:"false"`
AllowUpdates bool `yaml:"allow_updates" env-default:"false"`
AllowDowngrades bool `yaml:"allow_downgrades" env-default:"false"`
CheckInterval time.Duration `yaml:"check_interval" env-default:"2h"`
RepositoryURL string `yaml:"repository_url" env-default:""`
WantedVersion string `yaml:"wanted_version" env-default:"latest-stable"`
UpdatesEnabled bool `mapstructure:"enabled"`
CheckInterval time.Duration `mapstructure:"check_interval"`
RepositoryURL string `mapstructure:"repository_url"`
WantedVersion string `mapstructure:"wanted_version"`
}
type Log struct {
Level string `mapstructure:"level"`
OutPath string `mapstructure:"out_path"`
}
// ConfigEnv structure for environment variables
type ConfigEnv struct {
ConfigPath string `env:"CONFIG_PATH" env-default:"./cfg/config.yaml"`
type Env struct {
ConfigPath string `mapstructure:"config_path"`
NodePath string `mapstructure:"node_path"`
ParentStagePID int `mapstructure:"parent_pid"`
}
// MustLoadConfig loads the configuration from the specified path and environment variables.
// Program will shutdown if any error occurs during loading.
func MustLoadConfig() *ConfigConf {
log.SetOutput(os.Stderr)
var configEnv ConfigEnv
if err := cleanenv.ReadEnv(&configEnv); err != nil {
log.Fatalf("Failed to read environment variables: %v", err)
os.Exit(1)
}
if _, err := os.Stat(configEnv.ConfigPath); os.IsNotExist(err) {
log.Fatalf("Config file does not exist: %s", configEnv.ConfigPath)
os.Exit(2)
}
var config ConfigConf
if err := cleanenv.ReadConfig(configEnv.ConfigPath, &config); err != nil {
log.Fatalf("Failed to read config file: %v", err)
os.Exit(3)
}
log.Printf("Configuration loaded successfully from %s", configEnv.ConfigPath)
return &config
type CMDLine struct {
Run Run
Node Root
}
type Root struct {
Debug bool `persistent:"true" full:"debug" short:"d" def:"false" desc:"Set debug mode"`
}
type Run struct {
ConfigPath string `persistent:"true" full:"config" short:"c" def:"./config.yaml" desc:"Path to configuration file"`
Test []int `persistent:"true" full:"test" short:"t" def:"" desc:"js test"`
}

View File

@@ -1,36 +1,34 @@
package config
import "os"
// UUIDLength is uuids length for sessions. By default it is 16 bytes.
var UUIDLength byte = 4
var UUIDLength int = 16
// ApiRoute setting for go-chi for main route for api requests
var ApiRoute string = "/api/{ver}"
// ComDirRoute setting for go-chi for main route for commands
var ComDirRoute string = "/com"
// NodeVersion is the version of the node. It can be set by the build system or manually.
// If not set, it will return "version0.0.0-none" by default
var NodeVersion string
// ActualFileName is a feature of the GoSally update system.
// ActualFileName is a feature of the GoSally update system.
// In the repository, the file specified in the variable contains the current information about updates
var ActualFileName string = "actual.txt"
type _internalConsts struct{}
type _serverConsts struct{}
type _updateConsts struct{}
// UpdateArchiveName is the name of the archive that will be used for updates.
var UpdateArchiveName string = "gosally-node"
func GetUpdateConsts() _updateConsts { return _updateConsts{} }
func (_ _updateConsts) GetNodeVersion() string {
// UpdateInstallPath is the path where the update will be installed.
var UpdateDownloadPath string = os.TempDir()
var MetaDir string = "./.meta"
func init() {
if NodeVersion == "" {
return "v0.0.0-none"
NodeVersion = "v0.0.0-none"
}
return NodeVersion
}
func (_ _updateConsts) GetActualFileName() string { return ActualFileName }
func GetInternalConsts() _internalConsts { return _internalConsts{} }
func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength }
func GetServerConsts() _serverConsts { return _serverConsts{} }
func (_ _serverConsts) GetApiRoute() string { return ApiRoute }
func (_ _serverConsts) GetComDirRoute() string { return ComDirRoute }

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.UUIDLength {
return data, errors.New("decoded UUID length mismatch")
}
return data, nil
}
// SetNodeUUID sets the identifier to the given path.
// The function replaces the identifier's associated directory with all its contents.
func SetNodeUUID(metaInfPath string) error {
if !strings.HasSuffix(metaInfPath, "uuid") {
return errors.New("invalid meta/uuid path")
}
info, err := os.Stat(metaInfPath)
if err == nil && info.IsDir() {
err = os.RemoveAll(metaInfPath)
if err != nil {
return err
}
} else if err != nil && !os.IsNotExist(err) {
return err
}
err = os.MkdirAll(metaInfPath, 0755)
if err != nil {
return err
}
dataPath := filepath.Join(metaInfPath, "data")
uuidStr, err := utils.NewUUID32Raw()
if err != nil {
return err
}
err = os.WriteFile(dataPath, uuidStr[:], 0644)
if err != nil {
return err
}
readmePath := filepath.Join(metaInfPath, "README.txt")
readmeContent := ` - - - - ! STRICTLY FORBIDDEN TO MODIFY THIS DIRECTORY ! - - - -
This directory contains the unique node identifier stored in the file named data.
This identifier is critical for correct node recognition both locally and across the network.
Any modification, deletion, or tampering with this directory may lead to permanent loss of identity, data corruption, or network conflicts.
Proceed at your own risk. You have been warned.`
err = os.WriteFile(readmePath, []byte(readmeContent), 0644)
if err != nil {
return err
}
return nil
}

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

@@ -0,0 +1,24 @@
package corestate
// CoreStateContract is interface for CoreState.
// CoreState is a structure that contains the basic meta-information vital to the node.
// The interface contains functionality for working with the Runtime directory and its files,
// and access to low-level logging in stdout
type CoreStateContract interface {
}
type CoreState struct {
UUID32 string
UUID32DirName string
StartTimestampUnix int64
NodeBinName string
NodeVersion string
Stage Stage
NodePath string
MetaDir string
RunDir string
}

View File

@@ -0,0 +1,190 @@
// Package general_server provides an API request router based on versioning and custom layers.
//
// The GeneralServer distributes incoming HTTP requests to specific registered servers
// depending on the API version or defined logical layer. To operate properly, additional
// servers must be registered using the InitGeneral function or AppendToArray method.
//
// All registered servers must implement the GeneralServerApiContract interface to ensure
// correct interaction. The GeneralServer itself implements this interface and can be
// passed as an HTTP handler.
//
// If the requested version is not explicitly registered but matches a configured logical
// layer, the server will fallback to the latest registered version for that layer.
// Otherwise, an HTTP 400 error is returned.
package general_server
import (
"errors"
"log/slog"
"net/http"
"slices"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5"
)
// serversApiVer is a type alias for string, used to represent API version strings in the GeneralServer.
type serversApiVer string
// GeneralServerApiContract defines the interface for servers that can be registered
type GeneralServerApiContract interface {
// GetVersion returns the API version of the server.
GetVersion() string
// Handle and HandleList methods are used to forward requests.
Handle(w http.ResponseWriter, r *http.Request)
HandleList(w http.ResponseWriter, r *http.Request)
}
// GeneralServerContarct extends the GeneralServerApiContract with a method to append new servers.
// This interface is only for general server initialization and does not need to be implemented by individual servers.
type GeneralServerContarct interface {
GeneralServerApiContract
// AppendToArray adds a new server to the GeneralServer's internal map.
AppendToArray(GeneralServerApiContract) error
}
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
type GeneralServer struct {
w http.ResponseWriter
r *http.Request
// servers holds the registered servers by their API version.
// The key is the version string, and the value is the server implementing GeneralServerApi
servers map[serversApiVer]GeneralServerApiContract
log slog.Logger
cfg *config.Conf
}
// GeneralServerInit structure only for initialization general server.
type GeneralServerInit struct {
Log slog.Logger
Config *config.Conf
}
// InitGeneral initializes a new GeneralServer with the provided configuration and registered servers.
func InitGeneral(o *GeneralServerInit, servers ...GeneralServerApiContract) *GeneralServer {
general := &GeneralServer{
servers: make(map[serversApiVer]GeneralServerApiContract),
cfg: o.Config,
log: o.Log,
}
// register the provided servers
// s is each server implementing GeneralServerApiContract, this is not a general server
for _, s := range servers {
general.servers[serversApiVer(s.GetVersion())] = s
}
return general
}
// GetVersion returns the API version of the GeneralServer, which is "general".
func (s *GeneralServer) GetVersion() string {
return "general"
}
// AppendToArray adds a new server to the GeneralServer's internal map.
func (s *GeneralServer) AppendToArray(server GeneralServerApiContract) error {
if _, exist := s.servers[serversApiVer(server.GetVersion())]; !exist {
s.servers[serversApiVer(server.GetVersion())] = server
return nil
}
return errors.New("server with this version is already exist")
}
// Handle processes incoming HTTP requests, routing them to the appropriate server based on the API version.
// It checks if the requested version is registered and handles the request accordingly.
func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) {
s.w = w
s.r = r
serverReqApiVer := chi.URLParam(r, "ver")
log := s.log.With(
slog.Group("request",
slog.String("version", serverReqApiVer),
slog.String("url", s.r.URL.String()),
slog.String("method", s.r.Method),
),
slog.Group("connection",
slog.String("remote", s.r.RemoteAddr),
),
)
s.log.Debug("Received request")
// transfer control to the server
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
srv.Handle(w, r)
return
}
// if the requested version is not registered, check if it matches a logical layer
// and use the latest version for that layer if available
// this allows for custom layers to be defined in the configuration
// and used as a fallback for unsupported versions
// this is useful for cases where the API version is not explicitly registered
// but the logical layer is defined in the configuration
if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
s.log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
)
// transfer control to the latest version server under the custom layer
srv.Handle(w, r)
return
}
}
log.Error("HTTP request error: unsupported API version",
slog.Int("status", http.StatusBadRequest))
if err := utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version"); err != nil {
s.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
}
// HandleList processes incoming HTTP requests for listing commands, routing them to the appropriate server based on the API version.
func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) {
s.w = w
s.r = r
serverReqApiVer := chi.URLParam(r, "ver")
log := s.log.With(
slog.Group("request",
slog.String("version", serverReqApiVer),
slog.String("url", s.r.URL.String()),
slog.String("method", s.r.Method),
),
slog.Group("connection",
slog.String("remote", s.r.RemoteAddr),
),
)
log.Debug("Received request")
// transfer control to the server
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
srv.HandleList(w, r)
return
}
if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
)
// transfer control to the latest version server under the custom layer
srv.HandleList(w, r)
return
}
}
log.Error("HTTP request error: unsupported API version",
slog.Int("status", http.StatusBadRequest))
if err := utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version"); err != nil {
s.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
}

View File

@@ -1,169 +0,0 @@
package general_server
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"slices"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/go-chi/chi/v5"
)
type serversApiVer string
type GeneralServerApiContract interface {
GetVersion() string
Handle(w http.ResponseWriter, r *http.Request)
HandleList(w http.ResponseWriter, r *http.Request)
}
type GeneralServerContarct interface {
GeneralServerApiContract
AppendToArray(GeneralServerApiContract) error
}
type GeneralServer struct {
w http.ResponseWriter
r *http.Request
servers map[serversApiVer]GeneralServerApiContract
log slog.Logger
cfg *config.ConfigConf
}
// structure only for initialization
type GeneralServerInit struct {
Log slog.Logger
Config *config.ConfigConf
}
func InitGeneral(o *GeneralServerInit, servers ...GeneralServerApiContract) *GeneralServer {
general := &GeneralServer{
servers: make(map[serversApiVer]GeneralServerApiContract),
cfg: o.Config,
log: o.Log,
}
for _, s := range servers {
general.servers[serversApiVer(s.GetVersion())] = s
}
return general
}
func (s *GeneralServer) GetVersion() string {
return "general"
}
func (s *GeneralServer) AppendToArray(server GeneralServerApiContract) error {
if _, exist := s.servers[serversApiVer(server.GetVersion())]; !exist {
s.servers[serversApiVer(server.GetVersion())] = server
return nil
}
return errors.New("server with this version is already exist")
}
func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) {
s.w = w
s.r = r
serverReqApiVer := chi.URLParam(r, "ver")
log := s.log.With(
slog.Group("request",
slog.String("version", serverReqApiVer),
slog.String("url", s.r.URL.String()),
slog.String("method", s.r.Method),
),
slog.Group("connection",
slog.String("remote", s.r.RemoteAddr),
),
)
s.log.Debug("Received request")
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
srv.Handle(w, r)
return
}
if slices.Contains(s.cfg.Layers, serverReqApiVer) {
if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok {
s.log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.LatestVer),
)
srv.Handle(w, r)
return
}
}
log.Error("HTTP request error: unsupported API version",
slog.Int("status", http.StatusBadRequest))
s.writeJSONError(http.StatusBadRequest, "unsupported API version")
}
func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) {
s.w = w
s.r = r
serverReqApiVer := chi.URLParam(r, "ver")
log := s.log.With(
slog.Group("request",
slog.String("version", serverReqApiVer),
slog.String("url", s.r.URL.String()),
slog.String("method", s.r.Method),
),
slog.Group("connection",
slog.String("remote", s.r.RemoteAddr),
),
)
log.Debug("Received request")
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
srv.HandleList(w, r)
return
}
if slices.Contains(s.cfg.Layers, serverReqApiVer) {
if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok {
log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.LatestVer),
)
srv.HandleList(w, r)
return
}
}
log.Error("HTTP request error: unsupported API version",
slog.Int("status", http.StatusBadRequest))
s.writeJSONError(http.StatusBadRequest, "unsupported API version")
}
// func (s *GeneralServer) _errNotFound() {
// s.writeJSONError(http.StatusBadRequest, "invalid request")
// s.log.Error("HTTP request error",
// slog.String("remote", s.r.RemoteAddr),
// slog.String("method", s.r.Method),
// slog.String("url", s.r.URL.String()),
// slog.Int("status", http.StatusBadRequest))
// }
func (s *GeneralServer) writeJSONError(status int, msg string) {
s.w.Header().Set("Content-Type", "application/json")
s.w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(s.w).Encode(resp); err != nil {
s.log.Error("Failed to write JSON error response",
slog.String("error", err.Error()),
slog.Int("status", status))
return
}
}

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

@@ -0,0 +1,24 @@
package logs
import "fmt"
func SetBlack(s string) string { return fmt.Sprintf("\033[30m%s\033[0m", s) }
func SetRed(s string) string { return fmt.Sprintf("\033[31m%s\033[0m", s) }
func SetGreen(s string) string { return fmt.Sprintf("\033[32m%s\033[0m", s) }
func SetYellow(s string) string { return fmt.Sprintf("\033[33m%s\033[0m", s) }
func SetBlue(s string) string { return fmt.Sprintf("\033[34m%s\033[0m", s) }
func SetMagenta(s string) string { return fmt.Sprintf("\033[35m%s\033[0m", s) }
func SetCyan(s string) string { return fmt.Sprintf("\033[36m%s\033[0m", s) }
func SetWhite(s string) string { return fmt.Sprintf("\033[37m%s\033[0m", s) }
func SetBrightBlack(s string) string { return fmt.Sprintf("\033[90m%s\033[0m", s) }
func SetBrightRed(s string) string { return fmt.Sprintf("\033[91m%s\033[0m", s) }
func SetBrightGreen(s string) string { return fmt.Sprintf("\033[92m%s\033[0m", s) }
func SetBrightYellow(s string) string { return fmt.Sprintf("\033[93m%s\033[0m", s) }
func SetBrightBlue(s string) string { return fmt.Sprintf("\033[94m%s\033[0m", s) }
func SetBrightMagenta(s string) string { return fmt.Sprintf("\033[95m%s\033[0m", s) }
func SetBrightCyan(s string) string { return fmt.Sprintf("\033[96m%s\033[0m", s) }
func SetBrightWhite(s string) string { return fmt.Sprintf("\033[97m%s\033[0m", s) }
func PrintError() string { return SetRed("Error") }
func PrintWarn() string { return SetYellow("Warning") }

View File

@@ -1,25 +1,87 @@
// Package logs provides a logger setup function that configures the logger based on the environment.
// It supports different logging levels for development and production environments.
// It uses the standard library's slog package for structured logging.
package logs
import (
"bytes"
"context"
"io"
"log/slog"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/run_manager"
"gopkg.in/natefinch/lumberjack.v2"
)
const (
envDev = "dev"
envProd = "prod"
)
var GlobalLevel slog.Level
func SetupLogger(env string) *slog.Logger {
var log *slog.Logger
switch env {
case envDev:
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
case envProd:
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
type SlogWriter struct {
Logger *slog.Logger
Level slog.Level
}
func (w *SlogWriter) Write(p []byte) (n int, err error) {
msg := string(bytes.TrimSpace(p))
w.Logger.Log(context.TODO(), w.Level, msg)
return len(p), nil
}
// SetupLogger initializes and returns a logger based on the provided environment.
func SetupLogger(o config.Log) (*slog.Logger, error) {
var handlerOpts = slog.HandlerOptions{}
var writer io.Writer = os.Stdout
switch o.Level {
case "debug":
GlobalLevel = slog.LevelDebug
handlerOpts.Level = slog.LevelDebug
case "info":
GlobalLevel = slog.LevelInfo
handlerOpts.Level = slog.LevelInfo
default:
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
GlobalLevel = slog.LevelInfo
handlerOpts.Level = slog.LevelInfo
}
return log
if o.OutPath != "" {
repl := map[string]string{
"tmp": filepath.Clean(run_manager.RuntimeDir()),
}
re := regexp.MustCompile(`%(\w+)%`)
result := re.ReplaceAllStringFunc(o.OutPath, func(match string) string {
sub := re.FindStringSubmatch(match)
if len(sub) < 2 {
return match
}
key := sub[1]
if val, ok := repl[key]; ok {
return val
}
return match
})
if strings.Contains(o.OutPath, "%tmp%") {
relPath := strings.TrimPrefix(result, filepath.Clean(run_manager.RuntimeDir()))
if err := run_manager.SetDir(relPath); err != nil {
return nil, err
}
}
logFile := &lumberjack.Logger{
Filename: filepath.Join(result, "event.log"),
MaxSize: 10,
MaxBackups: 5,
MaxAge: 28,
Compress: true,
}
writer = logFile
}
log := slog.New(slog.NewJSONHandler(writer, &handlerOpts))
return log, nil
}

View File

@@ -6,30 +6,20 @@ import (
"sync"
)
// MockHandler is a mock implementation of slog.Handler for testing purposes.
type MockHandler struct {
mu sync.Mutex
mu sync.Mutex
// Logs stores the log records captured by the handler.
Logs []slog.Record
}
func NewMockHandler() *MockHandler {
return &MockHandler{}
}
func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}
func NewMockHandler() *MockHandler { return &MockHandler{} }
func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
func (h *MockHandler) WithGroup(_ string) slog.Handler { return h }
func (h *MockHandler) Handle(_ context.Context, r slog.Record) error {
h.mu.Lock()
defer h.mu.Unlock()
h.Logs = append(h.Logs, r.Clone())
return nil
}
func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h
}
func (h *MockHandler) WithGroup(_ string) slog.Handler {
return h
}

View File

@@ -0,0 +1,95 @@
package run_manager
import (
"context"
"fmt"
"os"
"syscall"
"time"
)
func File(index string) RunFileManagerContract {
value, ok := indexedPaths[index]
if !ok {
err := indexPaths()
if err != nil {
return &RunFileManager{
err: err,
}
}
value, ok = indexedPaths[index]
if !ok {
return &RunFileManager{
err: fmt.Errorf("cannot detect file under index %s", index),
}
}
}
return &RunFileManager{
indexedPath: value,
}
}
func (r *RunFileManager) Open() (*os.File, error) {
if r.err != nil {
return nil, r.err
}
file, err := os.OpenFile(r.indexedPath, os.O_RDWR, 0)
if err != nil {
return nil, err
}
r.file = file
return file, nil
}
func (r *RunFileManager) Close() error {
return r.file.Close()
}
func (r *RunFileManager) Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error) {
if r.err != nil {
return nil, r.err
}
if r.file == nil {
return nil, fmt.Errorf("file is not opened")
}
info, err := r.file.Stat()
if err != nil {
return nil, err
}
origStat := info.Sys().(*syscall.Stat_t)
origIno := origStat.Ino
origModTime := info.ModTime()
ctx, cancel := context.WithCancel(parentCtx)
go func() {
for {
select {
case <-ctx.Done():
return
default:
newInfo, err := os.Stat(r.indexedPath)
if err != nil {
if os.IsNotExist(err) {
callback()
return
}
} else {
newStat := newInfo.Sys().(*syscall.Stat_t)
if newStat.Ino != origIno {
callback()
return
}
if !newInfo.ModTime().Equal(origModTime) {
callback()
return
}
}
time.Sleep(1 * time.Second)
}
}
}()
return cancel, nil
}

View File

@@ -0,0 +1,158 @@
package run_manager
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/core/utils"
)
type RunManagerContract interface {
Get(index string) (string, error)
// Set recursively creates a file in runDir
Set(index string) error
File(index string) RunFileManagerContract
indexPaths() error
}
var (
created bool
runDir string
indexedPaths = make(map[string]string)
)
type RunFileManagerContract interface {
Open() (*os.File, error)
Close() error
Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error)
}
type RunFileManager struct {
err error
indexedPath string
file *os.File
}
// func (c *CoreState) RuntimeDir() RunManagerContract {
// return c.RM
// }
// Create creates a temp directory
func Create(uuid32 string) (string, error) {
if created {
return runDir, fmt.Errorf("runtime directory is already created")
}
path, err := os.MkdirTemp("", fmt.Sprintf("*-%s-%s", uuid32, "gosally-runtime"))
if err != nil {
return "", err
}
runDir = path
created = true
return path, nil
}
func Clean() error {
created = false
indexedPaths = nil
return utils.CleanTempRuntimes(runDir)
}
// Quite dangerous and goofy.
// TODO: implement a better variant of runDir indexing on the second stage of initialization
func Toggle() string {
runDir = filepath.Dir(os.Args[0])
created = true
return runDir
}
func Get(index string) (string, error) {
if !created {
return "", fmt.Errorf("runtime directory is not created")
}
if indexedPaths == nil {
err := indexPaths()
if err != nil {
return "", nil
}
}
if indexedPaths == nil {
return "", fmt.Errorf("indexedPaths is nil")
}
value, ok := indexedPaths[index]
if !ok {
err := indexPaths()
if err != nil {
return "", err
}
value, ok = indexedPaths[index]
if !ok {
return "", fmt.Errorf("cannot detect file under index %s", index)
}
}
return value, nil
}
func Set(index string) error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(runDir, index)
dir := filepath.Dir(fullPath)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if indexedPaths == nil {
err = indexPaths()
if err != nil {
return err
}
} else {
indexedPaths[index] = fullPath
}
return nil
}
func SetDir(index string) error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(runDir, index)
err := os.MkdirAll(fullPath, 0755)
if err != nil {
return err
}
return nil
}
func indexPaths() error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
i, err := utils.IndexPaths(runDir)
if err != nil {
return err
}
indexedPaths = i
return nil
}
func RuntimeDir() string {
return runDir
}

View File

@@ -7,42 +7,63 @@ import (
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/corestate"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5"
lua "github.com/yuin/gopher-lua"
)
func (h *HandlerV1) _handle() {
uuid16 := h.newUUID()
// HandlerV1 is the main handler for version 1 of the API.
// The function processes the HTTP request and runs Lua scripts,
// preparing the environment and subsequently transmitting the execution result
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID(int(config.UUIDLength))
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
log := h.log.With(
slog.Group("request",
slog.String("version", h.GetVersion()),
slog.String("url", h.r.URL.String()),
slog.String("method", h.r.Method),
slog.String("url", r.URL.String()),
slog.String("method", r.Method),
),
slog.Group("connection",
slog.String("connection-uuid", uuid16),
slog.String("remote", h.r.RemoteAddr),
slog.String("remote", r.RemoteAddr),
),
)
log.Info("Received request")
cmd := chi.URLParam(h.r, "cmd")
cmd := chi.URLParam(r, "cmd")
if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) || !h.listAllowedCmd.MatchString(cmd) {
log.Error("HTTP request error",
slog.String("error", "invalid command"),
slog.String("cmd", cmd),
slog.Int("status", http.StatusBadRequest))
h.writeJSONError(http.StatusBadRequest, "invalid command")
if err := utils.WriteJSONError(w, http.StatusBadRequest, "invalid command"); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
scriptPath := h.comMatch(chi.URLParam(h.r, "ver"), cmd)
scriptPath := h.comMatch(chi.URLParam(r, "ver"), cmd)
if scriptPath == "" {
log.Error("HTTP request error",
slog.String("error", "command not found"),
slog.String("cmd", cmd),
slog.Int("status", http.StatusNotFound))
h.writeJSONError(http.StatusNotFound, "command not found")
if err := utils.WriteJSONError(w, http.StatusNotFound, "command not found"); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
@@ -52,17 +73,18 @@ func (h *HandlerV1) _handle() {
slog.String("error", "command not found"),
slog.String("cmd", cmd),
slog.Int("status", http.StatusNotFound))
h.writeJSONError(http.StatusNotFound, "command not found")
if err := utils.WriteJSONError(w, http.StatusNotFound, "command not found"); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
L := lua.NewState()
defer L.Close()
// Создаем таблицу Params
// Создаем таблицу In с Params
paramsTable := L.NewTable()
qt := h.r.URL.Query()
qt := r.URL.Query()
for k, v := range qt {
if len(v) > 0 {
L.SetField(paramsTable, k, lua.LString(v[0]))
@@ -78,53 +100,65 @@ func (h *HandlerV1) _handle() {
L.SetField(outTable, "Result", resultTable)
L.SetGlobal("Out", outTable)
// Скрипт подготовки окружения
prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua")
if _, err := os.Stat(prepareLuaEnv); err == nil {
if err := L.DoFile(prepareLuaEnv); err != nil {
log.Error("Failed to prepare lua environment",
slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
} else {
log.Warn("No environment preparation script found, skipping preparation")
}
// Основной Lua скрипт
if err := L.DoFile(scriptPath); err != nil {
log.Error("Failed to execute lua script",
slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
// Получаем Out
lv := L.GetGlobal("Out")
tbl, ok := lv.(*lua.LTable)
if !ok {
log.Error("Lua global 'Out' is not a table")
h.writeJSONError(http.StatusInternalServerError, "'Out' is not a table")
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "'Out' is not a table"); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
// Получаем Result из Out
resultVal := tbl.RawGetString("Result")
resultTbl, ok := resultVal.(*lua.LTable)
if !ok {
log.Error("Lua global 'Result' is not a table")
h.writeJSONError(http.StatusInternalServerError, "'Result' is not a table")
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "'Result' is not a table"); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
// Перебираем таблицу Result
out := make(map[string]interface{})
out := make(map[string]any)
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = convertTypes(value)
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
})
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: cmd,
Response: out,
}
h.w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(h.w).Encode(out); err != nil {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Error("Failed to encode JSON response",
slog.String("error", err.Error()))
}
@@ -147,26 +181,3 @@ func (h *HandlerV1) _handle() {
log.Info("Session completed")
}
func convertTypes(value lua.LValue) any {
switch value.Type() {
case lua.LTString:
return value.String()
case lua.LTNumber:
return float64(value.(lua.LNumber))
case lua.LTBool:
return bool(value.(lua.LBool))
case lua.LTTable:
result := make(map[string]interface{})
if tbl, ok := value.(*lua.LTable); ok {
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
result[key.String()] = convertTypes(value)
})
}
return result
case lua.LTNil:
return nil
default:
return value.String()
}
}

View File

@@ -8,20 +8,33 @@ import (
"path/filepath"
"strings"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/corestate"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5"
)
func (h *HandlerV1) _handleList() {
uuid16 := h.newUUID()
// The function processes the HTTP request and returns a list of available commands.
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID(int(config.UUIDLength))
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
log := h.log.With(
slog.Group("request",
slog.String("version", h.GetVersion()),
slog.String("url", h.r.URL.String()),
slog.String("method", h.r.Method),
slog.String("url", r.URL.String()),
slog.String("method", r.Method),
),
slog.Group("connection",
slog.String("connection-uuid", uuid16),
slog.String("remote", h.r.RemoteAddr),
slog.String("remote", r.RemoteAddr),
),
)
log.Info("Received request")
@@ -31,7 +44,6 @@ func (h *HandlerV1) _handleList() {
}
var (
files []os.DirEntry
err error
commands = make(map[string]ComMeta)
cmdsProcessed = make(map[string]bool)
)
@@ -39,11 +51,14 @@ func (h *HandlerV1) _handleList() {
if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
log.Error("Failed to read commands directory",
slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
apiVer := chi.URLParam(h.r, "ver")
apiVer := chi.URLParam(r, "ver")
// Сначала ищем версионные
for _, file := range files {
@@ -102,9 +117,14 @@ func (h *HandlerV1) _handleList() {
log.Debug("Command list prepared")
log.Info("Session completed")
h.w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(h.w).Encode(commands); err != nil {
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: "list",
Response: commands,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.log.Error("Failed to write JSON error response",
slog.String("error", err.Error()))
}

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

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

View File

@@ -1,48 +1,42 @@
// Package sv1 provides the implementation of the Server V1 API handler.
// It includes utilities for handling API requests, extracting descriptions, and managing UUIDs.
package sv1
import (
"log/slog"
"net/http"
"regexp"
"github.com/akyaiy/GoSally-mvp/core/config"
)
type ServerV1UtilsContract interface {
extractDescriptionStatic(path string) (string, error)
writeJSONError(status int, msg string)
newUUID() string
_errNotFound()
ErrNotFound(w http.ResponseWriter, r *http.Request)
}
// structure only for initialization
// HandlerV1InitStruct structure is only for initialization
type HandlerV1InitStruct struct {
Ver string
Log slog.Logger
Config *config.ConfigConf
Config *config.Conf
AllowedCmd *regexp.Regexp
ListAllowedCmd *regexp.Regexp
}
// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests.
type HandlerV1 struct {
w http.ResponseWriter
r *http.Request
log *slog.Logger
log slog.Logger
cfg *config.ConfigConf
cfg *config.Conf
// allowedCmd and listAllowedCmd are regular expressions used to validate command names.
allowedCmd *regexp.Regexp
listAllowedCmd *regexp.Regexp
ver string
}
// InitV1Server initializes a new HandlerV1 with the provided configuration and returns it.
// Should be carefull with giving to this function invalid parameters,
// because there is no validation of parameters in this function.
func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
return &HandlerV1{
log: o.Log,
log: &o.Log,
cfg: o.Config,
allowedCmd: o.AllowedCmd,
listAllowedCmd: o.ListAllowedCmd,
@@ -50,18 +44,8 @@ func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
}
}
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._handle()
}
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._handleList()
}
// GetVersion returns the API version of the HandlerV1, which is set during initialization.
// This version is used to identify the API version in the request routing.
func (h *HandlerV1) GetVersion() string {
return h.ver
}

View File

@@ -1,57 +1,19 @@
package sv1
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log/slog"
"net/http"
"os"
"regexp"
"github.com/akyaiy/GoSally-mvp/core/config"
)
func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._errNotFound()
}
func (h *HandlerV1) newUUID() string {
bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2))
_, err := rand.Read(bytes)
if err != nil {
h.log.Error("Failed to generate UUID", slog.String("error", err.Error()))
return ""
}
return hex.EncodeToString(bytes)
}
func (h *HandlerV1) _errNotFound() {
h.writeJSONError(http.StatusBadRequest, "invalid request")
h.log.Error("HTTP request error",
slog.String("remote", h.r.RemoteAddr),
slog.String("method", h.r.Method),
slog.String("url", h.r.URL.String()),
slog.Int("status", http.StatusBadRequest))
}
func (h *HandlerV1) writeJSONError(status int, msg string) {
h.w.Header().Set("Content-Type", "application/json")
h.w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(h.w).Encode(resp); err != nil {
h.log.Error("Failed to write JSON error response",
slog.String("error", err.Error()),
slog.Int("status", status))
return
}
}
// func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
// utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
// h.log.Error("HTTP request error",
// slog.String("remote", h.r.RemoteAddr),
// slog.String("method", h.r.Method),
// slog.String("url", h.r.URL.String()),
// slog.Int("status", http.StatusBadRequest))
// }
func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
data, err := os.ReadFile(path)

View File

@@ -1,14 +1,23 @@
package update
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"log/slog"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/run_manager"
"github.com/akyaiy/GoSally-mvp/core/utils"
"golang.org/x/net/context"
)
const (
@@ -29,14 +38,20 @@ type UpdaterContract interface {
}
type Updater struct {
Log slog.Logger
Config *config.ConfigConf
log *log.Logger
config *config.Conf
env *config.Env
ctx context.Context
cancel context.CancelFunc
}
func NewUpdater(log slog.Logger, cfg *config.ConfigConf) *Updater {
func NewUpdater(ctx context.Context, log *log.Logger, cfg *config.Conf, env *config.Env) *Updater {
return &Updater{
Log: log,
Config: cfg,
log: log,
config: cfg,
env: env,
ctx: ctx,
}
}
@@ -73,11 +88,11 @@ func isVersionNewer(current, latest Version) bool {
if i < len(currentParts) {
cur, err := strconv.Atoi(currentParts[i])
if err != nil {
cur = 0 // или можно обработать ошибку иначе
cur = 0
}
curPart = cur
} else {
curPart = 0 // Если части в current меньше, считаем недостающие нулями
curPart = 0
}
if i < len(latestParts) {
@@ -96,31 +111,15 @@ func isVersionNewer(current, latest Version) bool {
if curPart > latPart {
return false
}
// если равны — идём дальше
}
return false // все части равны, значит не новее
return false
}
// if len(currentParts) >= 1 && len(latestParts) >= 1 {
// if currentParts[0] < latestParts[0] {
// if len(currentParts) < 2 || len(latestParts) < 2 {
// if currentParts[1] < latestParts[1] {
// return true
// }
// if currentParts[1] > latestParts[1] {
// return false
// }
// }
// if currentParts[0] > latestParts[0] {
// return false
// }
// }
// GetCurrentVersion reads the current version from the version file and returns it along with the branch.
func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
version, branch, err := splitVersionString(string(config.GetUpdateConsts().GetNodeVersion()))
version, branch, err := splitVersionString(string(config.NodeVersion))
if err != nil {
u.Log.Error("Failed to parse version string", slog.String("version", string(config.GetUpdateConsts().GetNodeVersion())), slog.String("error", err.Error()))
u.log.Printf("Failed to parse version string: %s", err.Error())
return "", "", err
}
switch branch {
@@ -132,28 +131,28 @@ func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
}
func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error) {
repoURL := u.Config.Updates.RepositoryURL
repoURL := u.config.Updates.RepositoryURL
if repoURL == "" {
u.Log.Error("RepositoryURL is empty in config")
u.log.Printf("Failed to get latest version: %s", "RepositoryURL is empty in config")
return "", "", errors.New("repository URL is empty")
}
if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") {
u.Log.Error("RepositoryURL does not start with http:// or https://", slog.String("RepositoryURL", repoURL))
u.log.Printf("Failed to get latest version: %s: %s", "RepositoryURL does not start with http:// or https:/", repoURL)
return "", "", errors.New("repository URL must start with http:// or https://")
}
response, err := http.Get(repoURL + "/" + config.GetUpdateConsts().GetActualFileName())
response, err := http.Get(repoURL + "/" + config.ActualFileName)
if err != nil {
u.Log.Error("Failed to fetch latest version", slog.String("error", err.Error()))
u.log.Printf("Failed to fetch latest version: %s", err.Error())
return "", "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
u.Log.Error("Failed to fetch latest version", slog.Int("status", response.StatusCode))
u.log.Printf("Failed to fetch latest version: HTTP status %d", response.StatusCode)
return "", "", errors.New("failed to fetch latest version, status code: " + http.StatusText(response.StatusCode))
}
data, err := io.ReadAll(response.Body)
if err != nil {
u.Log.Error("Failed to read latest version response", slog.String("error", err.Error()))
u.log.Printf("Failed to read latest version response: %s", err.Error())
return "", "", err
}
lines := strings.Split(string(data), "\n")
@@ -164,14 +163,13 @@ func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error)
}
version, branch, err := splitVersionString(string(line))
if err != nil {
u.Log.Error("Failed to parse version string", slog.String("version", string(line)), slog.String("error", err.Error()))
u.log.Printf("Failed to parse version string: %s", err.Error())
return "", "", err
}
if branch == updateBranch {
return Version(version), Branch(branch), nil
}
}
u.Log.Warn("No version found for branch", slog.String("branch", string(updateBranch)))
return "", "", errors.New("no version found for branch: " + string(updateBranch))
}
@@ -187,30 +185,151 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, error) {
if currentVersion == latestVersion && currentBranch == latestBranch {
return false, nil
}
u.Log.Info("New update available",
slog.String("current_version", string(currentVersion)),
slog.String("current_branch", string(currentBranch)),
slog.String("latest_version", string(latestVersion)),
slog.String("latest_branch", string(latestBranch)),
)
return true, nil
}
// func (u *Updater) Update() error {
// if !(u.Config.UpdatesEnabled && u.Config.Updates.AllowUpdates && u.Config.Updates.AllowDowngrades) {
// u.Log.Info("Updates are disabled in config, skipping update")
// return nil
// }
// wantedVersion := u.Config.Updates.WantedVersion
// _, wantedBranch, _ := splitVersionString(wantedVersion)
// newVersion, newBranch, err := u.GetLatestVersion(wantedBranch)
// if err != nil {
// return err
// }
// if wantedBranch != newBranch {
// u.Log.Info("Wanted version branch does not match latest version branch: updating wanted branch",
// slog.String("wanted_branch", string(wantedBranch)),
// slog.String("latest_branch", string(newBranch)),
// )
// }
// }
func (u *Updater) Update() error {
if !u.config.Updates.UpdatesEnabled {
return errors.New("updates are disabled in config, skipping update")
}
if err := run_manager.SetDir("update"); err != nil {
return fmt.Errorf("failed to create update dir: %w", err)
}
downloadPath := filepath.Join(run_manager.RuntimeDir(), "update")
_, currentBranch, err := u.GetCurrentVersion()
if err != nil {
return fmt.Errorf("failed to get current version: %w", err)
}
latestVersion, latestBranch, err := u.GetLatestVersion(currentBranch)
if err != nil {
return fmt.Errorf("failed to get latest version: %w", err)
}
updateArchiveName := fmt.Sprintf("%s.v%s-%s", config.UpdateArchiveName, latestVersion, latestBranch)
updateDest := fmt.Sprintf("%s/%s.%s", u.config.Updates.RepositoryURL, updateArchiveName, "tar.gz")
resp, err := http.Get(updateDest)
if err != nil {
return fmt.Errorf("failed to fetch archive: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected HTTP status: %s, body: %s", resp.Status, body)
}
gzReader, err := gzip.NewReader(resp.Body)
if err != nil {
return fmt.Errorf("gzip reader error: %w", err)
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("tar read error: %w", err)
}
relativeParts := strings.SplitN(header.Name, string(os.PathSeparator), 2)
if len(relativeParts) < 2 {
// It's either a top level directory or garbage.
continue
}
cleanName := relativeParts[1]
targetPath := filepath.Join(downloadPath, cleanName)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("mkdir error: %w", err)
}
case tar.TypeReg:
if err := run_manager.Set(filepath.Join("update", cleanName)); err != nil {
return fmt.Errorf("set file error: %w", err)
}
f := run_manager.File(filepath.Join("update", cleanName))
outFile, err := f.Open()
if err != nil {
return fmt.Errorf("open file error: %w", err)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return fmt.Errorf("copy file error: %w", err)
}
outFile.Close()
default:
return fmt.Errorf("unsupported tar type: %v", header.Typeflag)
}
}
return u.InstallAndRestart()
}
func (u *Updater) InstallAndRestart() error {
nodePath := u.env.NodePath
if nodePath == "" {
return errors.New("GS_NODE_PATH environment variable is not set")
}
installDir := filepath.Join(nodePath, "bin")
targetPath := filepath.Join(installDir, "node")
f := run_manager.File("update/node")
input, err := f.Open()
if err != nil {
return fmt.Errorf("cannot open new binary: %w", err)
}
defer f.Close()
output, err := os.Create(targetPath)
if err != nil {
return fmt.Errorf("cannot create target binary: %w", err)
}
if _, err := io.Copy(output, input); err != nil {
output.Close()
return fmt.Errorf("copy failed: %w", err)
}
output.Close()
if err := os.Chmod(targetPath, 0755); err != nil {
return fmt.Errorf("failed to chmod: %w", err)
}
u.log.Printf("Launching new version: path is %s", targetPath)
// cmd := exec.Command(targetPath, os.Args[1:]...)
// cmd.Env = os.Environ()
// cmd.Stdout = os.Stdout
// cmd.Stderr = os.Stderr
// cmd.Stdin = os.Stdin
args := os.Args
args[0] = targetPath
env := utils.SetEviron(os.Environ(), "GS_PARENT_PID=-1")
if err := run_manager.Clean(); err != nil {
return err
}
return syscall.Exec(targetPath, args, env)
//u.cancel()
// TODO: fix this crap and find a better way to update without errors
// for {
// _, err := run_manager.Get("run.lock")
// if err != nil {
// break
// }
// }
// return cmd.Start()
}
func (u *Updater) Shutdownfunc(f context.CancelFunc) {
u.cancel = f
}

22
core/utils/http_errors.go Normal file
View File

@@ -0,0 +1,22 @@
package utils
import (
"encoding/json"
"net/http"
)
// writeJSONError writes a JSON error response to the HTTP response writer.
// It sets the Content-Type to application/json, writes the specified HTTP status code
func WriteJSONError(w http.ResponseWriter, status int, msg string) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := map[string]any{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,26 @@
package utils
import lua "github.com/yuin/gopher-lua"
func ConvertLuaTypesToGolang(value lua.LValue) any {
switch value.Type() {
case lua.LTString:
return value.String()
case lua.LTNumber:
return float64(value.(lua.LNumber))
case lua.LTBool:
return bool(value.(lua.LBool))
case lua.LTTable:
result := make(map[string]interface{})
if tbl, ok := value.(*lua.LTable); ok {
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
result[key.String()] = ConvertLuaTypesToGolang(value)
})
}
return result
case lua.LTNil:
return nil
default:
return value.String()
}
}

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

@@ -0,0 +1,122 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
)
func SetEviron(eviron []string, envs ...string) []string {
envMap := make(map[string]string)
for _, e := range eviron {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
for _, e := range envs {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
newEviron := make([]string, 0, len(envMap))
for k, v := range envMap {
newEviron = append(newEviron, fmt.Sprintf("%s=%s", k, v))
}
return newEviron
}
func CleanTempRuntimes(pattern string) error {
matches, err := filepath.Glob(pattern)
if err != nil {
return err
}
for _, path := range matches {
info, err := os.Stat(path)
if err != nil {
continue
}
if info.IsDir() {
os.RemoveAll(path)
}
}
return nil
}
func ExistsMatchingDirs(pattern, exclude string) (bool, error) {
matches, err := filepath.Glob(pattern)
if err != nil {
return false, err
}
for _, path := range matches {
if filepath.Clean(path) == filepath.Clean(exclude) {
continue
}
info, err := os.Stat(path)
if err == nil && info.IsDir() {
return true, nil
}
}
return false, nil
}
func IndexPaths(runDir string) (map[string]string, error) {
indexed := make(map[string]string)
err := filepath.Walk(runDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(runDir, path)
if err != nil {
return err
}
indexed[relPath] = path
return nil
})
if err != nil {
return nil, err
}
return indexed, nil
}
func IsFullyInitialized(i any) bool {
v := reflect.ValueOf(i).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
switch field.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
if field.IsNil() {
return false
}
case reflect.String:
if field.String() == "" {
return false
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if field.Int() == 0 {
return false
}
case reflect.Bool:
if !field.Bool() {
return false
}
}
}
return true
}

48
core/utils/utils_test.go Normal file
View File

@@ -0,0 +1,48 @@
package utils
import (
"fmt"
"reflect"
"sort"
"testing"
)
func TestFunc_SetEviron(t *testing.T) {
tests := []struct {
eviron []string
envs []string
want []string
}{
{
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
[]string{"ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
{
[]string{"ENV1=1", "ENV2=5", "ENV3=4"},
[]string{"ENV2=2", "ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
{
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
[]string{"ENV4=4"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3", "ENV4=4"},
},
{
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
[]string{"ENV3=2", "ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("in %q set new %q", tt.eviron, tt.envs), func(t *testing.T) {
got := SetEviron(tt.eviron, tt.envs...)
sort.Strings(got)
sort.Strings(tt.want)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SetEviron(%q, %q) = got %v; want %v", tt.eviron, tt.envs, got, tt.want)
}
})
}
}

41
core/utils/uuid.go Normal file
View File

@@ -0,0 +1,41 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"errors"
"github.com/akyaiy/GoSally-mvp/core/config"
)
func NewUUIDRaw(length int) ([]byte, error) {
bytes := make([]byte, int(length))
_, err := rand.Read(bytes)
if err != nil {
return bytes, errors.New("failed to generate UUID: " + err.Error())
}
return bytes, nil
}
func NewUUID(length int) (string, error) {
data, err := NewUUIDRaw(length)
if err != nil {
return "", err
}
return hex.EncodeToString(data), nil
}
func NewUUID32() (string, error) {
return NewUUID(config.UUIDLength)
}
func NewUUID32Raw() ([]byte, error) {
data, err := NewUUIDRaw(config.UUIDLength)
if err != nil {
return data, err
}
if len(data) != config.UUIDLength {
return data, errors.New("unexpected UUID length")
}
return data, nil
}

21
go.mod
View File

@@ -9,8 +9,29 @@ require (
golang.org/x/net v0.41.0
)
require (
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/go-chi/cors v1.2.2
github.com/joho/godotenv v1.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect

43
go.sum
View File

@@ -1,18 +1,61 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=

9
main.go Normal file
View File

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