4 Commits

Author SHA1 Message Date
zero@thinky 0660117c07 docs(backend): add swag to jobs
ci-agent / build (push) Failing after 2m56s
2026-04-04 16:51:18 +03:00
zero@thinky 9ede6257f8 feat(backend): use interpreters service for jobs handlers 2026-04-04 16:51:18 +03:00
zero@thinky f5b9b32a9f feat(backend): add script interpreters 2026-04-04 16:51:18 +03:00
zero@thinky e721cff3f8 refactor(agent): error handling 2026-04-04 16:51:18 +03:00
7 changed files with 615 additions and 108 deletions
+78 -86
View File
@@ -97,108 +97,100 @@ func main() {
wg := &errgroup.Group{} wg := &errgroup.Group{}
grpcAddr := cfg.GRPCURL
if grpcAddr == "" {
grpcAddr = cfg.BackendURL
}
grpcAddr = strings.TrimPrefix(grpcAddr, "http://")
grpcAddr = strings.TrimPrefix(grpcAddr, "https://")
// Start command executor // Start command executor
wg.Go(func() error { wg.Go(func() error {
cmdexe := new(commander.CommandExecutor) cmdexe := new(commander.CommandExecutor)
ccli := client.New(cmdexe, cfg.Label, cfg.Label) ccli := client.New(cmdexe, cfg.Label, cfg.Label)
grpcAddr := cfg.GRPCURL
if grpcAddr == "" {
grpcAddr = cfg.BackendURL
}
grpcAddr = strings.TrimPrefix(grpcAddr, "http://")
grpcAddr = strings.TrimPrefix(grpcAddr, "https://")
return ccli.HandleCommands(ctx, grpcAddr, creds) return ccli.HandleCommands(ctx, grpcAddr, creds)
}) })
// Start log collectors // Start log collectors
if len(cfg.Services) > 0 { if len(cfg.Services) > 0 {
grpcAddr := cfg.GRPCURL wg.Go(func() error {
if grpcAddr == "" { conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(creds))
grpcAddr = cfg.BackendURL if err != nil {
} return fmt.Errorf("failed to connect to gRPC: %w", err)
grpcAddr = strings.TrimPrefix(grpcAddr, "http://")
grpcAddr = strings.TrimPrefix(grpcAddr, "https://")
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(creds))
if err != nil {
lgr.Error("Failed to connect to gRPC", "err", err)
os.Exit(1)
}
defer func() { _ = conn.Close() }()
ccli := proto.NewCollectorClient(conn)
for _, svc := range cfg.Services {
svc := svc
var src logsource.LogSource
switch svc.Type {
case "journald":
src, err = journald.New(svc, os.Getenv("JOURNALD_LOGDIR"))
if err != nil {
lgr.Error("Failed to create journald source", "service", svc.Name, "err", err)
os.Exit(1)
}
case "file":
if svc.Path == nil {
lgr.Error("Path is required for file log source", "service", svc.Name)
os.Exit(1)
}
src, err = file.New(*svc.Path)
if err != nil {
lgr.Error("Failed to create file source", "service", svc.Name, "err", err)
os.Exit(1)
}
default:
lgr.Error("Unknown log source type", "type", svc.Type, "service", svc.Name)
os.Exit(1)
} }
defer func() { _ = conn.Close() }()
wg.Go(func() error { ccli := proto.NewCollectorClient(conn)
lgr.Info("Starting log stream", "service", svc.Name)
// First, flush any buffered logs from offline period svcWg := new(errgroup.Group)
if err := flushBufferedLogs(ctx, ccli, logBuf, svc.Name, cfg.Label, cfg.RegistrationToken, lgr); err != nil { for _, svc := range cfg.Services {
lgr.Error("Failed to flush buffered logs", "service", svc.Name, "err", err) svc := svc
} var src logsource.LogSource
switch svc.Type {
scli, err := ccli.Stream( case "journald":
metadata.NewOutgoingContext(ctx, metadata.MD{ src, err = journald.New(svc, os.Getenv("JOURNALD_LOGDIR"))
"whoami": []string{cfg.Label},
"service": []string{svc.Name},
"token": []string{cfg.RegistrationToken},
"services": lo.Map(cfg.Services, func(item config.ServiceConfig, _ int) string {
return item.Name
}),
}),
)
if err != nil {
return fmt.Errorf("failed to create stream: %w", err)
}
for {
line, err := src.ReadLine()
if err != nil { if err != nil {
lgr.Error("ReadLine error", "service", svc.Name, "err", err) return fmt.Errorf("failed to create journald source %q: %w", svc.Name, err)
return err }
case "file":
if svc.Path == nil {
return fmt.Errorf("path is required for file log source %q", svc.Name)
}
src, err = file.New(*svc.Path)
if err != nil {
return fmt.Errorf("failed to create file source %q: %w", svc.Name, err)
}
default:
return fmt.Errorf("unknown log source type %q for service %q", svc.Type, svc.Name)
}
svcWg.Go(func() error {
lgr.Info("Starting log stream", "service", svc.Name)
// First, flush any buffered logs from offline period
if err := flushBufferedLogs(ctx, ccli, logBuf, svc.Name, cfg.Label, cfg.RegistrationToken, lgr); err != nil {
lgr.Error("Failed to flush buffered logs", "service", svc.Name, "err", err)
} }
if err := scli.Send(&proto.CollectorRequest{ scli, err := ccli.Stream(
Message: line, metadata.NewOutgoingContext(ctx, metadata.MD{
}); err != nil { "whoami": []string{cfg.Label},
// Connection failed, buffer the log "service": []string{svc.Name},
lgr.Warn("Send failed, buffering log", "service", svc.Name, "err", err) "token": []string{cfg.RegistrationToken},
if storeErr := logBuf.Store(svc.Name, line); storeErr != nil { "services": lo.Map(cfg.Services, func(item config.ServiceConfig, _ int) string {
lgr.Error("Failed to buffer log", "service", svc.Name, "err", storeErr) return item.Name
} }),
// Try to reconnect }),
if reconnectErr := reconnectStream(ctx, &scli, ccli, svc.Name, cfg.Label, cfg.RegistrationToken, logBuf, lgr); reconnectErr != nil { )
return reconnectErr if err != nil {
} return fmt.Errorf("failed to create stream: %w", err)
continue
} }
}
}) for {
} line, err := src.ReadLine()
if err != nil {
lgr.Error("ReadLine error", "service", svc.Name, "err", err)
return err
}
if err := scli.Send(&proto.CollectorRequest{
Message: line,
}); err != nil {
// Connection failed, buffer the log
lgr.Warn("Send failed, buffering log", "service", svc.Name, "err", err)
if storeErr := logBuf.Store(svc.Name, line); storeErr != nil {
lgr.Error("Failed to buffer log", "service", svc.Name, "err", storeErr)
}
// Try to reconnect
if reconnectErr := reconnectStream(ctx, &scli, ccli, svc.Name, cfg.Label, cfg.RegistrationToken, logBuf, lgr); reconnectErr != nil {
return reconnectErr
}
continue
}
}
})
}
return svcWg.Wait()
})
} }
if err := wg.Wait(); err != nil { if err := wg.Wait(); err != nil {
+30 -1
View File
@@ -15,6 +15,7 @@ import (
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -86,6 +87,15 @@ func main() {
cmdr := commander.New(jobRepo) cmdr := commander.New(jobRepo)
// Initialize script interpreter repository and service
scriptRepo := repository.NewScriptInterpreterRepo(db)
if err := scriptRepo.Init(context.Background()); err != nil {
log.Printf("Warning: failed to initialize script interpreters table: %v", err)
}
scriptSvc := service.NewScriptService(scriptRepo)
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdr)
jobsHandlers := handlers.NewJobsHandlers(cmdr, scriptSvc)
agents := handlers.NewAgentsGroup(h, coll) agents := handlers.NewAgentsGroup(h, coll)
auth := handlers.AuthGroup{Handlers: h} auth := handlers.AuthGroup{Handlers: h}
agentReg := handlers.NewAgentRegistrationGroup(h) agentReg := handlers.NewAgentRegistrationGroup(h)
@@ -164,6 +174,13 @@ func main() {
agentsGroup.GET("", agents.List) agentsGroup.GET("", agents.List)
} }
// Jobs (requires admin permission)
jobsGroup := v1.Group("/jobs")
jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
{
jobsGroup.POST("", jobsHandlers.AddJob)
}
// Agent registration // Agent registration
agentRegGroup := v1.Group("/agents") agentRegGroup := v1.Group("/agents")
{ {
@@ -193,6 +210,18 @@ func main() {
logsGroup.GET("/agents", logHandlers.GetAgents) logsGroup.GET("/agents", logHandlers.GetAgents)
logsGroup.GET("/levels", logHandlers.GetLevels) logsGroup.GET("/levels", logHandlers.GetLevels)
} }
// Scripts (requires admin permission)
scriptsGroup := v1.Group("/scripts")
scriptsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
{
scriptsGroup.POST("/run", scriptHandlers.RunScript)
scriptsGroup.GET("/interpreters", scriptHandlers.ListInterpreters)
scriptsGroup.POST("/interpreters", scriptHandlers.CreateInterpreter)
scriptsGroup.GET("/interpreters/:id", scriptHandlers.GetInterpreter)
scriptsGroup.PUT("/interpreters/:id", scriptHandlers.UpdateInterpreter)
scriptsGroup.DELETE("/interpreters/:id", scriptHandlers.DeleteInterpreter)
}
} }
// Start gRPC server with mTLS in background // Start gRPC server with mTLS in background
@@ -270,4 +299,4 @@ func main() {
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
log.Fatalf("Server error: %v", err) log.Fatalf("Server error: %v", err)
} }
} }
+47 -21
View File
@@ -2,38 +2,72 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type JobsHandlers struct { type JobsHandlers struct {
cmder *commander.Commander cmder *commander.Commander
svc *service.ScriptService
} }
func NewJobsHandlers(cmder *commander.Commander) JobsHandlers { func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers {
return JobsHandlers{cmder} return JobsHandlers{cmder: cmder, svc: svc}
} }
type AddJobIn struct {
Command string `json:"command" binding:"required"`
InterpreterID int64 `json:"interpreter_id"`
Stdin *string `json:"stdin"`
AgentID string `json:"agent_id" binding:"required"`
}
type AddJobOut struct {
ID int64 `json:"id"`
Command []string `json:"command"`
Stdin *string `json:"stdin"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Status int32 `json:"status"`
}
// AddJob creates and executes a job on a target agent.
// @Summary Create and run a job on an agent
// @Description Sends a command to the specified agent, waits for execution, and returns the result
// @Tags jobs
// @Accept json
// @Produce json
// @Param body body AddJobIn true "Job request"
// @Success 201 {object} AddJobOut
// @Router /jobs [post]
func (self *JobsHandlers) AddJob(c *gin.Context) { func (self *JobsHandlers) AddJob(c *gin.Context) {
err := func() error { err := func() error {
type In struct { var in AddJobIn
Command []string `json:"command"`
Stdin *string `json:"stdin"`
AID string `json:"agent_id"`
}
var in In
if err := c.Bind(&in); err != nil { if err := c.Bind(&in); err != nil {
return err return err
} }
agent, ok := self.cmder.GetAgent(in.AID) agent, ok := self.cmder.GetAgent(in.AgentID)
if !ok { if !ok {
c.Status(404) c.Status(http.StatusNotFound)
return fmt.Errorf("Agent not found") return fmt.Errorf("agent not found")
} }
var command []string
if in.InterpreterID == 0 {
command = []string{"sh", "-c", in.Command}
} else {
var err error
command, err = self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.Command)
if err != nil {
return err
}
}
jid, err := agent.AddJob(models.JobForInsert{ jid, err := agent.AddJob(models.JobForInsert{
Command: in.Command, Command: command,
Stdin: in.Stdin, Stdin: in.Stdin,
}) })
if err != nil { if err != nil {
@@ -43,15 +77,7 @@ func (self *JobsHandlers) AddJob(c *gin.Context) {
if err != nil { if err != nil {
return err return err
} }
type Out struct { c.JSON(http.StatusCreated, AddJobOut{
ID int64 `json:"id"`
Command []string `json:"command"`
Stdin *string `json:"stdin"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Status int32 `json:"status"`
}
c.JSON(201, Out{
ID: job.ID, ID: job.ID,
Command: job.Command, Command: job.Command,
Stdin: job.Stdin, Stdin: job.Stdin,
+206
View File
@@ -0,0 +1,206 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin"
)
type ScriptHandlers struct {
svc *service.ScriptService
cmder *commander.Commander
}
func NewScriptHandlers(svc *service.ScriptService, cmder *commander.Commander) ScriptHandlers {
return ScriptHandlers{svc: svc, cmder: cmder}
}
// RunScript executes a script on a target agent.
// @Summary Run a script on an agent
// @Description Resolves interpreter argv[] and sends the full command to the agent
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body RunScriptIn true "Script request"
// @Success 201 {object} RunScriptOut
// @Router /scripts/run [post]
func (self *ScriptHandlers) RunScript(c *gin.Context) {
err := func() error {
type RunScriptIn struct {
AgentID string `json:"agent_id" binding:"required"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
ScriptText string `json:"script_text" binding:"required"`
Stdin *string `json:"stdin"`
}
var in RunScriptIn
if err := c.Bind(&in); err != nil {
return err
}
command, err := self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.ScriptText)
if err != nil {
return err
}
agent, ok := self.cmder.GetAgent(in.AgentID)
if !ok {
c.Status(http.StatusNotFound)
return fmt.Errorf("agent not found")
}
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
return err
}
job, err := agent.WaitJob(jid)
if err != nil {
return err
}
type RunScriptOut struct {
ID int64 `json:"id"`
Command []string `json:"command"`
Stdin *string `json:"stdin"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Status int32 `json:"status"`
}
c.JSON(http.StatusCreated, RunScriptOut{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
return nil
}()
if err != nil {
c.Error(err)
}
}
// ListInterpreters returns all registered script interpreters.
// @Summary List interpreters
// @Description Returns all script interpreters available in the system
// @Tags scripts
// @Produce json
// @Success 200 {array} repository.ScriptInterpreter
// @Router /scripts/interpreters [get]
func (self *ScriptHandlers) ListInterpreters(c *gin.Context) {
interpreters, err := self.svc.List(c.Request.Context())
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, interpreters)
}
// CreateInterpreter registers a new script interpreter.
// @Summary Create interpreter
// @Description Registers a new script interpreter with name, label, and argv
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body repository.ScriptInterpreterCreate true "Interpreter definition"
// @Success 201 {object} repository.ScriptInterpreter
// @Router /scripts/interpreters [post]
func (self *ScriptHandlers) CreateInterpreter(c *gin.Context) {
var in repository.ScriptInterpreterCreate
if err := c.BindJSON(&in); err != nil {
c.Error(err)
return
}
si, err := self.svc.Create(c.Request.Context(), in)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusCreated, si)
}
// GetInterpreter returns a single interpreter by ID.
// @Summary Get interpreter
// @Description Returns a script interpreter by ID
// @Tags scripts
// @Produce json
// @Param id path int true "Interpreter ID"
// @Success 200 {object} repository.ScriptInterpreter
// @Router /scripts/interpreters/:id [get]
func (self *ScriptHandlers) GetInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.Error(err)
return
}
si, err := self.svc.GetByID(c.Request.Context(), id)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, si)
}
// UpdateInterpreter updates an interpreter.
// @Summary Update interpreter
// @Description Updates fields of a script interpreter
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Interpreter ID"
// @Param body body repository.ScriptInterpreterUpdate true "Interpreter fields"
// @Success 200 {object} repository.ScriptInterpreter
// @Router /scripts/interpreters/:id [put]
func (self *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.Error(err)
return
}
var in repository.ScriptInterpreterUpdate
if err := c.BindJSON(&in); err != nil {
c.Error(err)
return
}
si, err := self.svc.Update(c.Request.Context(), id, in)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, si)
}
// DeleteInterpreter removes an interpreter.
// @Summary Delete interpreter
// @Description Removes a script interpreter by ID
// @Tags scripts
// @Param id path int true "Interpreter ID"
// @Success 204
// @Router /scripts/interpreters/:id [delete]
func (self *ScriptHandlers) DeleteInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.Error(err)
return
}
if err := self.svc.Delete(c.Request.Context(), id); err != nil {
c.Error(err)
return
}
c.Status(http.StatusNoContent)
}
@@ -0,0 +1,189 @@
package repository
import (
"context"
"database/sql"
"encoding/json"
"errors"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
)
type ScriptInterpreter struct {
ID int64 `json:"id"`
Name string `json:"name"`
Label string `json:"label"`
Argv []string `json:"argv"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ScriptInterpreterCreate struct {
Name string `json:"name" binding:"required"`
Label string `json:"label" binding:"required"`
Argv []string `json:"argv" binding:"required"`
}
type ScriptInterpreterUpdate struct {
Name *string `json:"name"`
Label *string `json:"label"`
Argv []string `json:"argv"`
}
type ScriptInterpreterRepo struct {
DB *sql.DB
}
func NewScriptInterpreterRepo(db *sql.DB) *ScriptInterpreterRepo {
return &ScriptInterpreterRepo{DB: db}
}
func (r *ScriptInterpreterRepo) Init(ctx context.Context) error {
_, err := r.DB.ExecContext(ctx, storage.CreateScriptInterpretersTable)
return err
}
func (r *ScriptInterpreterRepo) Create(ctx context.Context, in ScriptInterpreterCreate) (*ScriptInterpreter, error) {
argvJSON, err := json.Marshal(in.Argv)
if err != nil {
return nil, err
}
result, err := r.DB.ExecContext(ctx,
`INSERT INTO script_interpreters (name, label, argv) VALUES (?, ?, ?)`,
in.Name, in.Label, string(argvJSON),
)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
return r.GetByID(ctx, id)
}
func (r *ScriptInterpreterRepo) GetByID(ctx context.Context, id int64) (*ScriptInterpreter, error) {
var si ScriptInterpreter
var argvJSON string
var createdAt, updatedAt string
err := r.DB.QueryRowContext(ctx,
`SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters WHERE id = ?`,
id,
).Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil {
return nil, err
}
si.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
si.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
return &si, nil
}
func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter, error) {
rows, err := r.DB.QueryContext(ctx,
`SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var interpreters []ScriptInterpreter
for rows.Next() {
var si ScriptInterpreter
var argvJSON, createdAt, updatedAt string
if err := rows.Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil {
return nil, err
}
si.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
si.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
interpreters = append(interpreters, si)
}
return interpreters, rows.Err()
}
func (r *ScriptInterpreterRepo) Update(ctx context.Context, id int64, in ScriptInterpreterUpdate) (*ScriptInterpreter, error) {
si, err := r.GetByID(ctx, id)
if err != nil {
return nil, err
}
set := ""
args := make([]interface{}, 0)
idx := 1
if in.Name != nil {
set += "name = ?"
args = append(args, *in.Name)
idx++
}
if in.Label != nil {
if idx > 1 {
set += ", "
}
set += "label = ?"
args = append(args, *in.Label)
idx++
}
if in.Argv != nil {
if idx > 1 {
set += ", "
}
argvJSON, err := json.Marshal(in.Argv)
if err != nil {
return nil, err
}
set += "argv = ?"
args = append(args, string(argvJSON))
idx++
}
if idx == 1 {
return si, nil
}
set += ", updated_at = CURRENT_TIMESTAMP"
args = append(args, id)
_, err = r.DB.ExecContext(ctx,
`UPDATE script_interpreters SET `+set+` WHERE id = ?`,
args...,
)
if err != nil {
return nil, err
}
return r.GetByID(ctx, id)
}
func (r *ScriptInterpreterRepo) Delete(ctx context.Context, id int64) error {
result, err := r.DB.ExecContext(ctx,
`DELETE FROM script_interpreters WHERE id = ?`,
id,
)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
@@ -0,0 +1,54 @@
package service
import (
"context"
"fmt"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
)
type ScriptService struct {
repo *repository.ScriptInterpreterRepo
}
func NewScriptService(repo *repository.ScriptInterpreterRepo) *ScriptService {
return &ScriptService{repo: repo}
}
// ResolveCommand builds the full argv[] by prepending the interpreter's argv
// to the script text (as the last argument).
func (self *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
interpreter, err := self.repo.GetByID(ctx, interpreterID)
if err != nil {
return nil, err
}
if len(interpreter.Argv) == 0 {
return nil, fmt.Errorf("interpreter %q has empty argv", interpreter.Name)
}
argv := make([]string, len(interpreter.Argv)+1)
copy(argv, interpreter.Argv)
argv[len(argv)-1] = scriptText
return argv, nil
}
func (self *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
return self.repo.Create(ctx, in)
}
func (self *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
return self.repo.GetByID(ctx, id)
}
func (self *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
return self.repo.List(ctx)
}
func (self *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
return self.repo.Update(ctx, id, in)
}
func (self *ScriptService) Delete(ctx context.Context, id int64) error {
return self.repo.Delete(ctx, id)
}
+11
View File
@@ -46,6 +46,17 @@ CREATE TABLE IF NOT EXISTS jobs (
); );
` `
const CreateScriptInterpretersTable = `
CREATE TABLE IF NOT EXISTS script_interpreters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
label TEXT NOT NULL,
argv TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`
const CreateLogsTable = ` const CreateLogsTable = `
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs (
timestamp DateTime64(3) DEFAULT now(), timestamp DateTime64(3) DEFAULT now(),