feat(delivery): add HTTP handlers and route registration
Add DeliveryHandler (7 endpoints under /auth/api/v1/delivery/*): CreateMySQL, List, Get, Cancel, Targets, CreateTarget, UpsertQuota. Add ExecutionHandler for internal service callback. Register routes with JWT auth middleware and start scheduler goroutine when enabled. Relates-to: #97
This commit is contained in:
@@ -0,0 +1,289 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||||
|
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeliveryHandler struct{ service *service.DeliveryService }
|
||||||
|
|
||||||
|
func NewDeliveryHandler(s *service.DeliveryService) *DeliveryHandler {
|
||||||
|
return &DeliveryHandler{service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecutionHandler struct {
|
||||||
|
service *service.DeliveryService
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExecutionHandler(s *service.DeliveryService, token string) *ExecutionHandler {
|
||||||
|
return &ExecutionHandler{service: s, token: strings.TrimSpace(token)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type executionPayload struct {
|
||||||
|
TaskID string `json:"task_id" binding:"required"`
|
||||||
|
PayloadHash string `json:"payload_hash" binding:"required"`
|
||||||
|
IdempotencyKey string `json:"idempotency_key" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExecutionHandler) Create(c *gin.Context) {
|
||||||
|
provided := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||||
|
if h.token == "" || subtle.ConstantTimeCompare([]byte(provided), []byte(h.token)) != 1 {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid service credential"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req executionPayload
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job, replay, err := h.service.CreateExecution(c.Request.Context(), req.TaskID, req.PayloadHash, req.IdempotencyKey)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status := http.StatusCreated
|
||||||
|
if replay {
|
||||||
|
status = http.StatusOK
|
||||||
|
}
|
||||||
|
c.JSON(status, gin.H{"execution": job, "idempotent_replay": replay})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMySQL 提交 MySQL 交付请求
|
||||||
|
// @Summary 提交 MySQL 一键交付
|
||||||
|
// @Description 创建一个 MySQL 交付任务,调度器会自动分配主机、调用 AWX 执行部署
|
||||||
|
// @Tags delivery
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param Idempotency-Key header string true "幂等键(防重复提交,最长 128 字符)"
|
||||||
|
// @Param body body service.MySQLDeliveryInput true "交付参数"
|
||||||
|
// @Success 202 {object} map[string]any "任务已创建"
|
||||||
|
// @Success 200 {object} map[string]any "幂等重放(相同 Idempotency-Key 已存在)"
|
||||||
|
// @Failure 400 {object} map[string]any "参数错误"
|
||||||
|
// @Failure 401 {object} map[string]any "未授权"
|
||||||
|
// @Router /auth/api/v1/delivery/mysql [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) CreateMySQL(c *gin.Context) {
|
||||||
|
claims, ok := CurrentClaims(c)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req service.MySQLDeliveryInput
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, existing, err := h.service.CreateTask(c.Request.Context(), claims.UserID, claims.IsAdmin, c.GetHeader("Idempotency-Key"), req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status := http.StatusAccepted
|
||||||
|
if existing {
|
||||||
|
status = http.StatusOK
|
||||||
|
}
|
||||||
|
c.JSON(status, gin.H{"task": task, "idempotent_replay": existing})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取交付任务列表
|
||||||
|
// @Summary 获取交付任务列表
|
||||||
|
// @Description 返回当前用户可见的交付任务列表(管理员可见全部)
|
||||||
|
// @Tags delivery
|
||||||
|
// @Produce json
|
||||||
|
// @Param business_line_id query uint64 false "业务线 ID 过滤"
|
||||||
|
// @Success 200 {object} map[string]any "items: 任务数组"
|
||||||
|
// @Failure 401 {object} map[string]any "未授权"
|
||||||
|
// @Router /auth/api/v1/delivery/tasks [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) List(c *gin.Context) {
|
||||||
|
claims, ok := CurrentClaims(c)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var businessLineID uint64
|
||||||
|
if raw := c.Query("business_line_id"); raw != "" {
|
||||||
|
businessLineID, _ = strconv.ParseUint(raw, 10, 64)
|
||||||
|
}
|
||||||
|
items, err := h.service.ListTasks(c.Request.Context(), claims.UserID, claims.IsAdmin, businessLineID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取交付任务详情
|
||||||
|
// @Summary 获取交付任务详情
|
||||||
|
// @Description 返回指定任务的详细信息及事件流
|
||||||
|
// @Tags delivery
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "任务 ID"
|
||||||
|
// @Success 200 {object} map[string]any "task + events"
|
||||||
|
// @Failure 401 {object} map[string]any "未授权"
|
||||||
|
// @Failure 404 {object} map[string]any "任务不存在"
|
||||||
|
// @Router /auth/api/v1/delivery/tasks/{id} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) Get(c *gin.Context) {
|
||||||
|
claims, ok := CurrentClaims(c)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, events, err := h.service.GetTask(c.Request.Context(), c.Param("id"), claims.UserID, claims.IsAdmin)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"task": task, "events": events})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel 取消交付任务
|
||||||
|
// @Summary 取消交付任务
|
||||||
|
// @Description 取消一个正在执行或等待中的交付任务
|
||||||
|
// @Tags delivery
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "任务 ID"
|
||||||
|
// @Success 200 {object} map[string]any "ok: true"
|
||||||
|
// @Failure 401 {object} map[string]any "未授权"
|
||||||
|
// @Failure 409 {object} map[string]any "无法取消(状态冲突)"
|
||||||
|
// @Router /auth/api/v1/delivery/tasks/{id}/cancel [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) Cancel(c *gin.Context) {
|
||||||
|
claims, ok := CurrentClaims(c)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.Cancel(c.Request.Context(), c.Param("id"), claims.UserID, claims.IsAdmin); err != nil {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets 获取可用部署目标
|
||||||
|
// @Summary 获取可用部署目标
|
||||||
|
// @Description 返回所有已启用的部署目标(如 k8s 集群、主机池)
|
||||||
|
// @Tags delivery
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} map[string]any "items: 部署目标数组"
|
||||||
|
// @Failure 500 {object} map[string]any "内部错误"
|
||||||
|
// @Router /auth/api/v1/delivery/targets [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) Targets(c *gin.Context) {
|
||||||
|
var items []model.DeploymentTarget
|
||||||
|
if err := h.service.DB().WithContext(c.Request.Context()).Where("enabled = ?", true).Order("id ASC").Find(&items).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
type targetPayload struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
TargetType string `json:"target_type" binding:"required"`
|
||||||
|
AWXInventoryID uint64 `json:"awx_inventory_id" binding:"required"`
|
||||||
|
AWXTemplateID uint64 `json:"awx_template_id" binding:"required"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTarget 创建部署目标
|
||||||
|
// @Summary 创建部署目标
|
||||||
|
// @Description 管理员创建新的部署目标(目前仅支持 k8s 类型)
|
||||||
|
// @Tags delivery
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body targetPayload true "目标配置"
|
||||||
|
// @Success 201 {object} model.DeploymentTarget "目标已创建"
|
||||||
|
// @Failure 400 {object} map[string]any "参数错误"
|
||||||
|
// @Failure 401 {object} map[string]any "未授权"
|
||||||
|
// @Failure 409 {object} map[string]any "名称冲突"
|
||||||
|
// @Router /auth/api/v1/delivery/targets [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) CreateTarget(c *gin.Context) {
|
||||||
|
if !requirePlatformAdmin(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req targetPayload
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.TargetType != "k8s" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "only k8s targets are supported"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(req.Metadata)
|
||||||
|
item := model.DeploymentTarget{Name: req.Name, TargetType: req.TargetType, AWXInventoryID: req.AWXInventoryID, AWXTemplateID: req.AWXTemplateID, Enabled: true, Metadata: string(raw)}
|
||||||
|
if err := h.service.DB().Create(&item).Error; err != nil {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
type quotaPayload struct {
|
||||||
|
BusinessLineID uint64 `json:"business_line_id" binding:"required"`
|
||||||
|
TargetID uint64 `json:"target_id" binding:"required"`
|
||||||
|
CPUMilli int64 `json:"cpu_milli" binding:"required"`
|
||||||
|
MemoryMi int64 `json:"memory_mi" binding:"required"`
|
||||||
|
StorageGi int64 `json:"storage_gi" binding:"required"`
|
||||||
|
InstanceLimit int64 `json:"instance_limit" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertQuota 创建或更新资源配额
|
||||||
|
// @Summary 创建或更新资源配额
|
||||||
|
// @Description 管理员为指定业务线 + 部署目标设置资源配额
|
||||||
|
// @Tags delivery
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body quotaPayload true "配额参数"
|
||||||
|
// @Success 200 {object} model.ResourceQuota "配额已更新"
|
||||||
|
// @Failure 400 {object} map[string]any "参数错误"
|
||||||
|
// @Failure 401 {object} map[string]any "未授权"
|
||||||
|
// @Router /auth/api/v1/delivery/quotas [put]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func (h *DeliveryHandler) UpsertQuota(c *gin.Context) {
|
||||||
|
if !requirePlatformAdmin(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req quotaPayload
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var item model.ResourceQuota
|
||||||
|
err := h.service.DB().Where("business_line_id = ? AND target_id = ?", req.BusinessLineID, req.TargetID).First(&item).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
item = model.ResourceQuota{BusinessLineID: req.BusinessLineID, TargetID: req.TargetID}
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.CPUMilli = req.CPUMilli
|
||||||
|
item.MemoryMi = req.MemoryMi
|
||||||
|
item.StorageGi = req.StorageGi
|
||||||
|
item.InstanceLimit = req.InstanceLimit
|
||||||
|
if err := h.service.DB().Save(&item).Error; err != nil {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, item)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"github.com/1024XEngineer/xinfra/server/internal/config"
|
"github.com/1024XEngineer/xinfra/server/internal/config"
|
||||||
"github.com/1024XEngineer/xinfra/server/internal/handler"
|
"github.com/1024XEngineer/xinfra/server/internal/handler"
|
||||||
"github.com/1024XEngineer/xinfra/server/internal/service"
|
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||||
@@ -71,6 +72,10 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
|||||||
wayenService := service.NewWayenService(deps.Config, deps.DB)
|
wayenService := service.NewWayenService(deps.Config, deps.DB)
|
||||||
wayneRoleBindingService := service.NewWayneRoleBindingService(deps.Config, deps.DB)
|
wayneRoleBindingService := service.NewWayneRoleBindingService(deps.Config, deps.DB)
|
||||||
deploymentService := service.NewDeploymentService(deps.Config, deps.DB)
|
deploymentService := service.NewDeploymentService(deps.Config, deps.DB)
|
||||||
|
deliveryService := service.NewDeliveryService(deps.Config, deps.DB, auditService)
|
||||||
|
if deps.Config.DeliverySchedulerEnabled {
|
||||||
|
go deliveryService.Run(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
healthHandler := handler.NewHealthHandler(deps.DB)
|
healthHandler := handler.NewHealthHandler(deps.DB)
|
||||||
authHandler := handler.NewAuthHandler(deps.Config, authService)
|
authHandler := handler.NewAuthHandler(deps.Config, authService)
|
||||||
@@ -83,9 +88,12 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
|||||||
samlHandler := handler.NewSAMLHandler(deps.Config, authService)
|
samlHandler := handler.NewSAMLHandler(deps.Config, authService)
|
||||||
oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService)
|
oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService)
|
||||||
deploymentHandler := handler.NewDeploymentHandler(deps.Config, deps.DB, deploymentService)
|
deploymentHandler := handler.NewDeploymentHandler(deps.Config, deps.DB, deploymentService)
|
||||||
|
deliveryHandler := handler.NewDeliveryHandler(deliveryService)
|
||||||
|
executionHandler := handler.NewExecutionHandler(deliveryService, deps.Config.DeliveryServiceToken)
|
||||||
|
|
||||||
r.GET("/healthz", healthHandler.Healthz)
|
r.GET("/healthz", healthHandler.Healthz)
|
||||||
r.GET("/readyz", healthHandler.Readyz)
|
r.GET("/readyz", healthHandler.Readyz)
|
||||||
|
r.POST("/api/v1/executions", executionHandler.Create)
|
||||||
r.GET("/auth/.well-known/openid-configuration", oauthHandler.Discovery)
|
r.GET("/auth/.well-known/openid-configuration", oauthHandler.Discovery)
|
||||||
r.GET("/auth/oauth/authorize", oauthHandler.Authorize)
|
r.GET("/auth/oauth/authorize", oauthHandler.Authorize)
|
||||||
r.POST("/auth/oauth/token", oauthHandler.Token)
|
r.POST("/auth/oauth/token", oauthHandler.Token)
|
||||||
@@ -139,5 +147,12 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
|||||||
protected.GET("/deployments/:id/events", deploymentHandler.Events)
|
protected.GET("/deployments/:id/events", deploymentHandler.Events)
|
||||||
protected.POST("/deployments/:id/cancel", deploymentHandler.Cancel)
|
protected.POST("/deployments/:id/cancel", deploymentHandler.Cancel)
|
||||||
protected.GET("/clouddm/login", clouddmHandler.Login)
|
protected.GET("/clouddm/login", clouddmHandler.Login)
|
||||||
|
protected.GET("/delivery/targets", deliveryHandler.Targets)
|
||||||
|
protected.POST("/delivery/targets", deliveryHandler.CreateTarget)
|
||||||
|
protected.PUT("/delivery/quotas", deliveryHandler.UpsertQuota)
|
||||||
|
protected.POST("/delivery/mysql", deliveryHandler.CreateMySQL)
|
||||||
|
protected.GET("/delivery/tasks", deliveryHandler.List)
|
||||||
|
protected.GET("/delivery/tasks/:id", deliveryHandler.Get)
|
||||||
|
protected.POST("/delivery/tasks/:id/cancel", deliveryHandler.Cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user