package invoke import ( "fmt" "io" "log/slog" "net/http" "os" "path/filepath" "strings" "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"` } type TerminalLogger struct { fc *worker.FuncConfig } func (l *TerminalLogger) Write(line string) { slog.Warn("function stderr", slog.String("line", line), slog.String("n:v", fmt.Sprintf("%s:%s", l.fc.Name, l.fc.Version))) } type JSONFileLogger struct { fc *worker.FuncConfig logger *slog.Logger } func NewJSONFileLogger(fc *worker.FuncConfig, path string) (*JSONFileLogger, error) { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } handler := slog.NewJSONHandler(file, &slog.HandlerOptions{}) logger := slog.New(handler) return &JSONFileLogger{ fc: fc, logger: logger, }, nil } func (l *JSONFileLogger) Write(line string) { l.logger.Warn("function stderr", slog.String("function", l.fc.Name), slog.String("version", l.fc.Version), slog.String("line", line), ) } 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 } var logger worker.Logger switch fc.Log.Output { case "stdout": logger = &TerminalLogger{ fc: fc, } case "file": fileLogger, err := NewJSONFileLogger(fc, filepath.Join(treeCfg.Log.Path, fmt.Sprintf("%s:%s", fc.Name, f.Path), "event.log.json")) if err != nil { slog.Error("Failed to create file logger", slog.String("err", err.Error())) w.WriteHeader(http.StatusInternalServerError) return } logger = fileLogger } var frmt = func(s1 string, s2 string) string { return fmt.Sprintf("FAAS_%s=%s", s1, s2) } var env = []string{ frmt("PROTOCOL", r.Proto), frmt("METHOD", r.Method), frmt("PATH", r.URL.Path), frmt("QUERY", r.URL.RawQuery), } for k, v := range r.Header { key := "FAAS_HEADER_" + strings.ReplaceAll(k, "-", "_") env = append(env, key+"="+v[0]) } input, _ := io.ReadAll(r.Body) path := filepath.Join(root, f.FunctionName, f.Path, fc.Entry) output, err := worker.RunFunction(&worker.RunOps{ Path: path, FuncConfig: fc, Log: logger, Env: env, }, 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) } }