Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0660117c07 | |||
| 9ede6257f8 | |||
| f5b9b32a9f | |||
| e721cff3f8 |
+78
-86
@@ -97,108 +97,100 @@ func main() {
|
||||
|
||||
wg := &errgroup.Group{}
|
||||
|
||||
grpcAddr := cfg.GRPCURL
|
||||
if grpcAddr == "" {
|
||||
grpcAddr = cfg.BackendURL
|
||||
}
|
||||
grpcAddr = strings.TrimPrefix(grpcAddr, "http://")
|
||||
grpcAddr = strings.TrimPrefix(grpcAddr, "https://")
|
||||
// Start command executor
|
||||
wg.Go(func() error {
|
||||
cmdexe := new(commander.CommandExecutor)
|
||||
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)
|
||||
})
|
||||
|
||||
// Start log collectors
|
||||
if len(cfg.Services) > 0 {
|
||||
grpcAddr := cfg.GRPCURL
|
||||
if grpcAddr == "" {
|
||||
grpcAddr = cfg.BackendURL
|
||||
}
|
||||
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)
|
||||
wg.Go(func() error {
|
||||
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(creds))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to gRPC: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
wg.Go(func() error {
|
||||
lgr.Info("Starting log stream", "service", svc.Name)
|
||||
ccli := proto.NewCollectorClient(conn)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
scli, err := ccli.Stream(
|
||||
metadata.NewOutgoingContext(ctx, metadata.MD{
|
||||
"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()
|
||||
svcWg := new(errgroup.Group)
|
||||
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("ReadLine error", "service", svc.Name, "err", err)
|
||||
return err
|
||||
return fmt.Errorf("failed to create journald source %q: %w", svc.Name, 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{
|
||||
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
|
||||
scli, err := ccli.Stream(
|
||||
metadata.NewOutgoingContext(ctx, metadata.MD{
|
||||
"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 {
|
||||
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 {
|
||||
|
||||
+30
-1
@@ -15,6 +15,7 @@ import (
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers"
|
||||
"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/proto/proto"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -86,6 +87,15 @@ func main() {
|
||||
|
||||
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)
|
||||
auth := handlers.AuthGroup{Handlers: h}
|
||||
agentReg := handlers.NewAgentRegistrationGroup(h)
|
||||
@@ -164,6 +174,13 @@ func main() {
|
||||
agentsGroup.GET("", agents.List)
|
||||
}
|
||||
|
||||
// Jobs (requires admin permission)
|
||||
jobsGroup := v1.Group("/jobs")
|
||||
jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
|
||||
{
|
||||
jobsGroup.POST("", jobsHandlers.AddJob)
|
||||
}
|
||||
|
||||
// Agent registration
|
||||
agentRegGroup := v1.Group("/agents")
|
||||
{
|
||||
@@ -193,6 +210,18 @@ func main() {
|
||||
logsGroup.GET("/agents", logHandlers.GetAgents)
|
||||
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
|
||||
@@ -270,4 +299,4 @@ func main() {
|
||||
if err := g.Wait(); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,38 +2,72 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type JobsHandlers struct {
|
||||
cmder *commander.Commander
|
||||
svc *service.ScriptService
|
||||
}
|
||||
|
||||
func NewJobsHandlers(cmder *commander.Commander) JobsHandlers {
|
||||
return JobsHandlers{cmder}
|
||||
func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers {
|
||||
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) {
|
||||
err := func() error {
|
||||
type In struct {
|
||||
Command []string `json:"command"`
|
||||
Stdin *string `json:"stdin"`
|
||||
AID string `json:"agent_id"`
|
||||
}
|
||||
var in In
|
||||
var in AddJobIn
|
||||
if err := c.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
agent, ok := self.cmder.GetAgent(in.AID)
|
||||
agent, ok := self.cmder.GetAgent(in.AgentID)
|
||||
if !ok {
|
||||
c.Status(404)
|
||||
return fmt.Errorf("Agent not found")
|
||||
c.Status(http.StatusNotFound)
|
||||
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{
|
||||
Command: in.Command,
|
||||
Command: command,
|
||||
Stdin: in.Stdin,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -43,15 +77,7 @@ func (self *JobsHandlers) AddJob(c *gin.Context) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type Out 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(201, Out{
|
||||
c.JSON(http.StatusCreated, AddJobOut{
|
||||
ID: job.ID,
|
||||
Command: job.Command,
|
||||
Stdin: job.Stdin,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 = `
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
timestamp DateTime64(3) DEFAULT now(),
|
||||
|
||||
Reference in New Issue
Block a user