package server import ( "context" "errors" "net/http" "galaxy/backend/internal/runtime" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "galaxy/backend/internal/telemetry" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // AdminEngineVersionsHandlers groups the engine-version-registry // handlers under `/api/v1/admin/engine-versions/*`. The implementation swaps // the placeholder bodies for real `*runtime.EngineVersionService` // calls; tests that omit the service fall back to the Stage-3 501 // envelope. type AdminEngineVersionsHandlers struct { svc *runtime.EngineVersionService logger *zap.Logger } // NewAdminEngineVersionsHandlers constructs the handler set. svc may // be nil — in that case every handler returns 501. func NewAdminEngineVersionsHandlers(svc *runtime.EngineVersionService, logger *zap.Logger) *AdminEngineVersionsHandlers { if logger == nil { logger = zap.NewNop() } return &AdminEngineVersionsHandlers{svc: svc, logger: logger.Named("http.admin.engine-versions")} } // List handles GET /api/v1/admin/engine-versions. func (h *AdminEngineVersionsHandlers) List() gin.HandlerFunc { if h == nil || h.svc == nil { return handlers.NotImplemented("adminEngineVersionsList") } return func(c *gin.Context) { ctx := c.Request.Context() items, err := h.svc.List(ctx) if err != nil { respondEngineVersionError(c, h.logger, "admin engine-versions list", ctx, err) return } c.JSON(http.StatusOK, engineVersionListToWire(items)) } } // Create handles POST /api/v1/admin/engine-versions. func (h *AdminEngineVersionsHandlers) Create() gin.HandlerFunc { if h == nil || h.svc == nil { return handlers.NotImplemented("adminEngineVersionsCreate") } return func(c *gin.Context) { var req engineVersionCreateRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } ctx := c.Request.Context() input := runtime.RegisterInput{Version: req.Version, ImageRef: req.ImageRef} if req.Enabled != nil { input.Enabled = req.Enabled } v, err := h.svc.Register(ctx, input) if err != nil { respondEngineVersionError(c, h.logger, "admin engine-versions create", ctx, err) return } c.JSON(http.StatusCreated, engineVersionToWire(v)) } } // Update handles PATCH /api/v1/admin/engine-versions/{id}. func (h *AdminEngineVersionsHandlers) Update() gin.HandlerFunc { if h == nil || h.svc == nil { return handlers.NotImplemented("adminEngineVersionsUpdate") } return func(c *gin.Context) { var req engineVersionUpdateRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } ctx := c.Request.Context() updated, err := h.svc.Update(ctx, c.Param("id"), runtime.UpdateInput{ ImageRef: req.ImageRef, Enabled: req.Enabled, }) if err != nil { respondEngineVersionError(c, h.logger, "admin engine-versions update", ctx, err) return } c.JSON(http.StatusOK, engineVersionToWire(updated)) } } // Disable handles POST /api/v1/admin/engine-versions/{id}/disable. func (h *AdminEngineVersionsHandlers) Disable() gin.HandlerFunc { if h == nil || h.svc == nil { return handlers.NotImplemented("adminEngineVersionsDisable") } return func(c *gin.Context) { ctx := c.Request.Context() updated, err := h.svc.Disable(ctx, c.Param("id")) if err != nil { respondEngineVersionError(c, h.logger, "admin engine-versions disable", ctx, err) return } c.JSON(http.StatusOK, engineVersionToWire(updated)) } } func respondEngineVersionError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) { switch { case errors.Is(err, runtime.ErrNotFound): httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "engine version not found") case errors.Is(err, runtime.ErrEngineVersionTaken): httperr.Abort(c, http.StatusConflict, httperr.CodeConflict, "engine version already registered") case errors.Is(err, runtime.ErrInvalidInput): httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error()) default: logger.Error(op+" failed", append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))..., ) httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "internal error") } } func engineVersionToWire(v runtime.EngineVersion) engineVersionWire { return engineVersionWire{ Version: v.Version, ImageRef: v.ImageRef, Enabled: v.Enabled, CreatedAt: v.CreatedAt.UTC().Format(timestampLayout), } } func engineVersionListToWire(items []runtime.EngineVersion) engineVersionListWire { out := engineVersionListWire{Items: make([]engineVersionWire, 0, len(items))} for _, v := range items { out.Items = append(out.Items, engineVersionToWire(v)) } return out } // engineVersionWire mirrors `EngineVersion` from openapi.yaml. type engineVersionWire struct { Version string `json:"version"` ImageRef string `json:"image_ref"` Enabled bool `json:"enabled"` CreatedAt string `json:"created_at"` } // engineVersionListWire mirrors `EngineVersionList`. type engineVersionListWire struct { Items []engineVersionWire `json:"items"` } // engineVersionCreateRequestWire mirrors `EngineVersionCreateRequest`. type engineVersionCreateRequestWire struct { Version string `json:"version"` ImageRef string `json:"image_ref"` Enabled *bool `json:"enabled,omitempty"` } // engineVersionUpdateRequestWire mirrors `EngineVersionUpdateRequest`. type engineVersionUpdateRequestWire struct { ImageRef *string `json:"image_ref,omitempty"` Enabled *bool `json:"enabled,omitempty"` }