Files
kandev/apps/backend/internal/task/handlers/executor_handlers.go
T
2026-02-21 22:14:56 +00:00

313 lines
11 KiB
Go

package handlers
import (
"context"
"errors"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/task/dto"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/service"
ws "github.com/kandev/kandev/pkg/websocket"
)
type ExecutorHandlers struct {
service *service.Service
logger *logger.Logger
}
func NewExecutorHandlers(svc *service.Service, log *logger.Logger) *ExecutorHandlers {
return &ExecutorHandlers{service: svc, logger: log}
}
func RegisterExecutorRoutes(router *gin.Engine, dispatcher *ws.Dispatcher, svc *service.Service, log *logger.Logger) {
handlers := NewExecutorHandlers(svc, log)
handlers.registerHTTP(router)
handlers.registerWS(dispatcher)
}
func (h *ExecutorHandlers) registerHTTP(router *gin.Engine) {
api := router.Group("/api/v1")
api.GET("/executors", h.httpListExecutors)
api.POST("/executors", h.httpCreateExecutor)
api.GET("/executors/:id", h.httpGetExecutor)
api.PATCH("/executors/:id", h.httpUpdateExecutor)
api.DELETE("/executors/:id", h.httpDeleteExecutor)
}
func (h *ExecutorHandlers) registerWS(dispatcher *ws.Dispatcher) {
dispatcher.RegisterFunc(ws.ActionExecutorList, h.wsListExecutors)
dispatcher.RegisterFunc(ws.ActionExecutorCreate, h.wsCreateExecutor)
dispatcher.RegisterFunc(ws.ActionExecutorGet, h.wsGetExecutor)
dispatcher.RegisterFunc(ws.ActionExecutorUpdate, h.wsUpdateExecutor)
dispatcher.RegisterFunc(ws.ActionExecutorDelete, h.wsDeleteExecutor)
}
func (h *ExecutorHandlers) listExecutors(ctx context.Context) (dto.ListExecutorsResponse, error) {
executors, err := h.service.ListExecutors(ctx)
if err != nil {
return dto.ListExecutorsResponse{}, err
}
profiles, _ := h.service.ListAllExecutorProfiles(ctx)
profilesByExecutor := make(map[string][]dto.ExecutorProfileDTO, len(executors))
for _, p := range profiles {
profilesByExecutor[p.ExecutorID] = append(profilesByExecutor[p.ExecutorID], dto.FromExecutorProfile(p))
}
resp := dto.ListExecutorsResponse{
Executors: make([]dto.ExecutorDTO, 0, len(executors)),
Total: len(executors),
}
for _, executor := range executors {
d := dto.FromExecutor(executor)
d.Profiles = profilesByExecutor[executor.ID]
resp.Executors = append(resp.Executors, d)
}
return resp, nil
}
func (h *ExecutorHandlers) httpListExecutors(c *gin.Context) {
resp, err := h.listExecutors(c.Request.Context())
if err != nil {
h.logger.Error("failed to list executors", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list executors"})
return
}
c.JSON(http.StatusOK, resp)
}
type httpCreateExecutorRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
IsSystem bool `json:"is_system"`
Resumable *bool `json:"resumable,omitempty"`
Config map[string]string `json:"config,omitempty"`
}
func (h *ExecutorHandlers) httpCreateExecutor(c *gin.Context) {
var body httpCreateExecutorRequest
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
if body.Name == "" || body.Type == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name and type are required"})
return
}
status := body.Status
if status == "" {
status = string(models.ExecutorStatusActive)
}
resumable := true
if body.Resumable != nil {
resumable = *body.Resumable
}
executor, err := h.service.CreateExecutor(c.Request.Context(), &service.CreateExecutorRequest{
Name: body.Name,
Type: models.ExecutorType(body.Type),
Status: models.ExecutorStatus(status),
IsSystem: body.IsSystem,
Resumable: resumable,
Config: body.Config,
})
if err != nil {
if errors.Is(err, service.ErrInvalidExecutorConfig) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Error("failed to create executor", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "executor not created"})
return
}
c.JSON(http.StatusOK, dto.FromExecutor(executor))
}
func (h *ExecutorHandlers) httpGetExecutor(c *gin.Context) {
executor, err := h.service.GetExecutor(c.Request.Context(), c.Param("id"))
if err != nil {
h.logger.Error("failed to get executor", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "executor not found"})
return
}
c.JSON(http.StatusOK, dto.FromExecutor(executor))
}
type httpUpdateExecutorRequest struct {
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
Status *string `json:"status,omitempty"`
Resumable *bool `json:"resumable,omitempty"`
Config map[string]string `json:"config,omitempty"`
}
func (h *ExecutorHandlers) httpUpdateExecutor(c *gin.Context) {
var body httpUpdateExecutorRequest
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
var execType *models.ExecutorType
if body.Type != nil {
value := models.ExecutorType(*body.Type)
execType = &value
}
var execStatus *models.ExecutorStatus
if body.Status != nil {
value := models.ExecutorStatus(*body.Status)
execStatus = &value
}
executor, err := h.service.UpdateExecutor(c.Request.Context(), c.Param("id"), &service.UpdateExecutorRequest{
Name: body.Name,
Type: execType,
Status: execStatus,
Resumable: body.Resumable,
Config: body.Config,
})
if err != nil {
if errors.Is(err, service.ErrInvalidExecutorConfig) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Error("failed to update executor", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "executor not updated"})
return
}
c.JSON(http.StatusOK, dto.FromExecutor(executor))
}
func (h *ExecutorHandlers) httpDeleteExecutor(c *gin.Context) {
if err := h.service.DeleteExecutor(c.Request.Context(), c.Param("id")); err != nil {
if errors.Is(err, service.ErrActiveTaskSessions) {
c.JSON(http.StatusConflict, gin.H{"error": "executor is used by an active agent session"})
return
}
h.logger.Error("failed to delete executor", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "executor not deleted"})
return
}
c.JSON(http.StatusOK, dto.SuccessResponse{Success: true})
}
func (h *ExecutorHandlers) wsListExecutors(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
resp, err := h.listExecutors(ctx)
if err != nil {
h.logger.Error("failed to list executors", zap.Error(err))
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list executors", nil)
}
return ws.NewResponse(msg.ID, msg.Action, resp)
}
type wsCreateExecutorRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
IsSystem bool `json:"is_system"`
Resumable *bool `json:"resumable,omitempty"`
Config map[string]string `json:"config,omitempty"`
}
func (h *ExecutorHandlers) wsCreateExecutor(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req wsCreateExecutorRequest
if err := msg.ParsePayload(&req); err != nil {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
}
if req.Name == "" || req.Type == "" {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name and type are required", nil)
}
status := req.Status
if status == "" {
status = string(models.ExecutorStatusActive)
}
resumable := true
if req.Resumable != nil {
resumable = *req.Resumable
}
executor, err := h.service.CreateExecutor(ctx, &service.CreateExecutorRequest{
Name: req.Name,
Type: models.ExecutorType(req.Type),
Status: models.ExecutorStatus(status),
IsSystem: req.IsSystem,
Resumable: resumable,
Config: req.Config,
})
if err != nil {
if errors.Is(err, service.ErrInvalidExecutorConfig) {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, err.Error(), nil)
}
h.logger.Error("failed to create executor", zap.Error(err))
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create executor", nil)
}
return ws.NewResponse(msg.ID, msg.Action, dto.FromExecutor(executor))
}
type wsGetExecutorRequest struct {
ID string `json:"id"`
}
func (h *ExecutorHandlers) wsGetExecutor(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req wsGetExecutorRequest
if err := msg.ParsePayload(&req); err != nil {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
}
if req.ID == "" {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
}
executor, err := h.service.GetExecutor(ctx, req.ID)
if err != nil {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Executor not found", nil)
}
return ws.NewResponse(msg.ID, msg.Action, dto.FromExecutor(executor))
}
type wsUpdateExecutorRequest struct {
ID string `json:"id"`
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
Status *string `json:"status,omitempty"`
Resumable *bool `json:"resumable,omitempty"`
Config map[string]string `json:"config,omitempty"`
}
func (h *ExecutorHandlers) wsUpdateExecutor(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req wsUpdateExecutorRequest
if err := msg.ParsePayload(&req); err != nil {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
}
if req.ID == "" {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
}
var execType *models.ExecutorType
if req.Type != nil {
value := models.ExecutorType(*req.Type)
execType = &value
}
var execStatus *models.ExecutorStatus
if req.Status != nil {
value := models.ExecutorStatus(*req.Status)
execStatus = &value
}
executor, err := h.service.UpdateExecutor(ctx, req.ID, &service.UpdateExecutorRequest{
Name: req.Name,
Type: execType,
Status: execStatus,
Resumable: req.Resumable,
Config: req.Config,
})
if err != nil {
if errors.Is(err, service.ErrInvalidExecutorConfig) {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, err.Error(), nil)
}
h.logger.Error("failed to update executor", zap.Error(err))
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update executor", nil)
}
return ws.NewResponse(msg.ID, msg.Action, dto.FromExecutor(executor))
}
func (h *ExecutorHandlers) wsDeleteExecutor(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
return wsDeleteWithActiveSessionCheck(ctx, msg, h.logger, "executor", h.service.DeleteExecutor)
}