package handlers import ( "errors" "fmt" "net/http" "os/exec" "strconv" "time" "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 JobsHandlers struct { tracker *commander.ConnTracker svc *service.ScriptService whereami string jobRepo *repository.JobRepository } func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string, jobRepo *repository.JobRepository) JobsHandlers { return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami, jobRepo: jobRepo} } // AddJobIn is the request body for creating a job. 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"` } // AddJobOut is the response body for a submitted job. type AddJobOut struct { ID int64 `json:"id"` Command []string `json:"command"` WaitURL string `json:"wait_url"` } // JobResult is the response body for a completed job. type JobResult 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"` } // WaitJobIn is the request body for waiting on a job. type WaitJobIn struct { AgentID string `json:"agent_id" binding:"required"` } // AddJob submits a job to an agent and returns a wait_url for the result. // @Summary Submit a job to an agent // @Description Sends a command to the specified agent and returns a URL to wait for the result // @Tags jobs // @Accept json // @Produce json // @Param body body AddJobIn true "Job request" // @Success 201 {object} AddJobOut // @Router /jobs [post] func (h *JobsHandlers) AddJob(c *gin.Context) { var in AddJobIn if err := c.Bind(&in); err != nil { c.Error(err) return } result, err := h.runCommand(c, in.AgentID, in.InterpreterID, in.Command, in.Stdin) if err != nil { c.Error(err) return } c.JSON(http.StatusCreated, result) } // runCommand resolves command, submits a job to the agent, and returns AddJobOut. // Shared between jobs and scripts handlers. func (h *JobsHandlers) runCommand( c *gin.Context, agentID string, interpID int64, command string, stdin *string, ) (*AddJobOut, error) { agent, ok := h.tracker.GetAgent(agentID) if !ok { return nil, fmt.Errorf("agent not found") } cmd, err := resolveCommand(c, h.svc, interpID, command) if err != nil { return nil, err } jid, err := agent.AddJob(models.JobForInsert{ Command: cmd, Stdin: stdin, }) if err != nil { return nil, err } waitURL := fmt.Sprintf("%s/api/v1/jobs/%d/wait", h.whereami, jid) return &AddJobOut{ ID: jid, Command: cmd, WaitURL: waitURL, }, nil } // WaitJob waits for a submitted job to complete (long-poll). // If the job is already done, returns immediately. // @Summary Wait for job result // @Description Long-polls for a job result. Returns immediately if the job is already finished. // @Tags jobs // @Accept json // @Produce json // @Param id path int true "Job ID" // @Param body body WaitJobIn true "Agent reference" // @Success 200 {object} JobResult // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /jobs/{id}/wait [post] func (h *JobsHandlers) WaitJob(c *gin.Context) { jid, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job id"}) return } var in WaitJobIn if err := c.Bind(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return } agent, ok := h.tracker.GetAgent(in.AgentID) if !ok { c.Status(http.StatusNotFound) c.Error(fmt.Errorf("agent not found")) return } job, err := agent.WaitJob(jid) if err != nil { c.Error(err) return } c.JSON(http.StatusOK, JobResult{ ID: job.ID, Command: job.Command, Stdin: job.Stdin, Stdout: job.Stdout, Stderr: job.Stderr, Status: job.Status, }) } func resolveCommand(c *gin.Context, svc *service.ScriptService, interpID int64, cmd string) ([]string, error) { if interpID == 0 { return []string{"sh", "-c", cmd}, nil } command, err := svc.ResolveCommand(c.Request.Context(), interpID, cmd) if err != nil { return nil, err } return command, nil } // @Summary Check command path // @Description Validates that a command binary exists on the system // @Tags jobs // @Accept json // @Param body body CheckCmdIn true "Command to check" // @Success 200 {object} CheckCmdOut // @Failure 404 {object} map[string]string // @Router /jobs/check_cmd [post] func (h *JobsHandlers) CheckCmd(c *gin.Context) { var in struct { Command string `json:"command" binding:"required"` } if err := c.Bind(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if _, err := exec.LookPath(in.Command); err != nil { if errors.Is(err, exec.ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "command not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, CheckCmdOut{Exists: true}) } type CheckCmdIn struct { Command string `json:"command" binding:"required" example:"bash"` } type CheckCmdOut struct { Exists bool `json:"exists"` } // JobMetricsOut is the response body for the job metrics endpoint. type JobMetricsOut struct { Total int `json:"total"` Success int `json:"success"` Failed int `json:"failed"` Pending int `json:"pending"` Period string `json:"period"` } // GetJobMetrics returns job success metrics over a parameterized period. // @Summary Get job metrics // @Description Returns total, successful, failed, and pending job counts over the given period // @Tags jobs // @Produce json // @Param period query string false "Time period (e.g. 1h, 24h, 7d)" default(24h) // @Success 200 {object} JobMetricsOut // @Failure 400 {object} map[string]string // @Security Bearer // @Router /jobs/metrics [get] func (h *JobsHandlers) GetJobMetrics(c *gin.Context) { periodStr := c.DefaultQuery("period", "24h") period, err := time.ParseDuration(periodStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid period, use Go duration format (e.g. 1h, 24h, 7d)"}) return } since := time.Now().Add(-period) metrics, err := h.jobRepo.GetJobMetrics(c.Request.Context(), since) if err != nil { c.Error(err) return } c.JSON(http.StatusOK, JobMetricsOut{ Total: metrics.Total, Success: metrics.Success, Failed: metrics.Failed, Pending: metrics.Pending, Period: periodStr, }) }