From 6aae5f9fb03b6f03ffb4e8439f5eb568119e1aa1 Mon Sep 17 00:00:00 2001 From: Alexey Date: Sun, 30 Nov 2025 15:28:30 +0200 Subject: [PATCH] add function handling (basic) --- .gitignore | 3 +- api/invoke/worker.go | 62 ++++++++++++++++++++++ api/router.go | 15 ++++++ functions/config.json | 6 +++ functions/echo/1d965976/config.json | 6 +++ functions/echo/1d965976/echo.sh | 4 ++ internal/config/config.go | 9 +++- internal/vars/variables.go | 5 ++ internal/worker/handle.go | 82 +++++++++++++++++++++++++++++ internal/worker/run.go | 25 +++++++++ 10 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 api/invoke/worker.go create mode 100644 functions/config.json create mode 100644 functions/echo/1d965976/config.json create mode 100755 functions/echo/1d965976/echo.sh create mode 100644 internal/vars/variables.go create mode 100644 internal/worker/handle.go create mode 100644 internal/worker/run.go diff --git a/.gitignore b/.gitignore index 92b4304..4ca8948 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin/ -config.yaml \ No newline at end of file +config.yaml +*.sqlite3 \ No newline at end of file diff --git a/api/invoke/worker.go b/api/invoke/worker.go new file mode 100644 index 0000000..61b4f9d --- /dev/null +++ b/api/invoke/worker.go @@ -0,0 +1,62 @@ +package invoke + +import ( + "io" + "log/slog" + "net/http" + "path/filepath" + "time" + + "git.oblat.lv/alex/triggerssmith/internal/config" + "git.oblat.lv/alex/triggerssmith/internal/worker" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +type Function struct { + ID uint `gorm:"primaryKey;autoIncrement"` + FunctionName string `gorm:"not null"` + Version string `gorm:"not null"` + Path string `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func InvokeHandler(cfg *config.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "function_id") + version := chi.URLParam(r, "function_version") + slog.Debug("executing a function", slog.String("id", id), slog.String("version", version)) + root := cfg.Functions.FunctionDir + treeCfg, _ := worker.LoadTreeConfig(root) + db, err := worker.OpenDB(treeCfg, root) + if err != nil { + slog.Error("Failed to open db", slog.String("err", err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + //f, _ := worker.FindFunction(db, "echo", "0.0.1-00130112025") + f, err := worker.FindFunction(db, id, version) + if err != nil { + slog.Error("Failed to find function", slog.String("err", err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + fc, err := worker.LoadFunctionConfig(root, f.FunctionName, f.Path) + if err != nil { + slog.Error("Failed to load function config", slog.String("err", err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + + input, _ := io.ReadAll(r.Body) + output, err := worker.RunFunction(filepath.Join(root, f.FunctionName, f.Path, fc.Entry), fc, input) + if err != nil { + slog.Error("Failed to run function", slog.String("err", err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + slog.Debug("executing done", slog.Any("in", input), slog.Any("out", output)) + w.Write(output) + } +} diff --git a/api/router.go b/api/router.go index 15cf0b1..1993d40 100644 --- a/api/router.go +++ b/api/router.go @@ -1,10 +1,14 @@ package api import ( + "encoding/json" "net/http" "path/filepath" + "time" + "git.oblat.lv/alex/triggerssmith/api/invoke" "git.oblat.lv/alex/triggerssmith/internal/config" + "git.oblat.lv/alex/triggerssmith/internal/vars" "github.com/go-chi/chi/v5" ) @@ -28,5 +32,16 @@ func (r *Router) RouteHandler() chi.Router { }) fs := http.FileServer(http.Dir("static")) r.r.Handle("/static/*", http.StripPrefix("/static/", fs)) + r.r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + b, _ := json.Marshal(struct { + Status string `json:"status"` + Uptime string `json:"uptime"` + }{ + Status: "ok", + Uptime: time.Since(vars.START_TIME).String(), + }) + w.Write([]byte(b)) + }) + r.r.Handle("/i/invoke/function/{function_id}/{function_version}", invoke.InvokeHandler(r.cfg)) return r.r } diff --git a/functions/config.json b/functions/config.json new file mode 100644 index 0000000..2f8b5e7 --- /dev/null +++ b/functions/config.json @@ -0,0 +1,6 @@ +{ + "data": { + "driver": "sqlite", + "path": "data.sqlite3" + } +} \ No newline at end of file diff --git a/functions/echo/1d965976/config.json b/functions/echo/1d965976/config.json new file mode 100644 index 0000000..a7d215e --- /dev/null +++ b/functions/echo/1d965976/config.json @@ -0,0 +1,6 @@ +{ + "name": "echo", + "version": "0.0.1-00130112025", + "entry": "echo.sh", + "runtime": "exec" +} \ No newline at end of file diff --git a/functions/echo/1d965976/echo.sh b/functions/echo/1d965976/echo.sh new file mode 100755 index 0000000..2c05063 --- /dev/null +++ b/functions/echo/1d965976/echo.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +read text +echo "${text}" \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 21225a0..e77a0bd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,8 +13,13 @@ type ServerConfig struct { LogPath string `mapstructure:"log_path"` } +type FuncConfig struct { + FunctionDir string `mapstructure:"func_dir"` +} + type Config struct { - Server ServerConfig `mapstructure:"server"` + Server ServerConfig `mapstructure:"server"` + Functions FuncConfig `mapstructure:"functions"` } var configPath atomic.Value // string @@ -22,6 +27,8 @@ var defaults = map[string]any{ "server.port": 8080, "server.address": "127.0.0.0", "server.static_dir": "./static", + + "functions.func_dir": "./functions", } func read(cfg *Config) error { diff --git a/internal/vars/variables.go b/internal/vars/variables.go new file mode 100644 index 0000000..2a2d6db --- /dev/null +++ b/internal/vars/variables.go @@ -0,0 +1,5 @@ +package vars + +import "time" + +var START_TIME = time.Now() diff --git a/internal/worker/handle.go b/internal/worker/handle.go new file mode 100644 index 0000000..096ec2a --- /dev/null +++ b/internal/worker/handle.go @@ -0,0 +1,82 @@ +package worker + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type RootConfig struct { + Data struct { + Driver string `json:"driver"` + Path string `json:"path"` + } `json:"data"` +} + +type Function struct { + ID uint `gorm:"primaryKey"` + FunctionName string + Version string + Path string + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +type FuncConfig struct { + Name string `json:"name"` + Version string `json:"version"` + Entry string `json:"entry"` + Runtime string `json:"runtime"` +} + +func LoadTreeConfig(root string) (*RootConfig, error) { + cfgPath := filepath.Join(root, "config.json") + b, err := os.ReadFile(cfgPath) + if err != nil { + return nil, err + } + var cfg RootConfig + if err := json.Unmarshal(b, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func OpenDB(cfg *RootConfig, root string) (*gorm.DB, error) { + switch cfg.Data.Driver { + case "sqlite": + dbPath := filepath.Join(root, cfg.Data.Path) + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, err + } + return db, nil + default: + return nil, fmt.Errorf("unsupported db driver: %s", cfg.Data.Driver) + } +} + +func FindFunction(db *gorm.DB, name, version string) (*Function, error) { + var f Function + if err := db.Where("function_name = ? AND version = ? AND deleted_at IS NULL", name, version). + First(&f).Error; err != nil { + return nil, err + } + return &f, nil +} + +func LoadFunctionConfig(root, funcName, funcPath string) (*FuncConfig, error) { + cfgFile := filepath.Join(root, funcName, funcPath, "config.json") + b, err := os.ReadFile(cfgFile) + if err != nil { + return nil, err + } + var cfg FuncConfig + if err := json.Unmarshal(b, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/internal/worker/run.go b/internal/worker/run.go new file mode 100644 index 0000000..a40bb75 --- /dev/null +++ b/internal/worker/run.go @@ -0,0 +1,25 @@ +package worker + +import ( + "bytes" + "fmt" + "os/exec" +) + +func RunFunction(path string, fc *FuncConfig, input []byte) ([]byte, error) { + if fc.Runtime != "exec" { + return nil, fmt.Errorf("unsupported runtime: %s", fc.Runtime) + } + + cmd := exec.Command(path) + cmd.Stdin = bytes.NewReader(input) + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("failed: %w\noutput: %s", err, out.String()) + } + return out.Bytes(), nil +}