feat(service): add deployment task integration

This commit is contained in:
mac
2026-07-21 18:26:50 +08:00
parent c324e2ec59
commit 2db8264c8e
12 changed files with 2226 additions and 382 deletions
+8 -2
View File
@@ -15,11 +15,17 @@ const ClaimsKey = "claims"
func AuthMiddleware(cfg config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
value := c.GetHeader("Authorization")
if !strings.HasPrefix(value, "Bearer ") {
tokenValue := ""
if strings.HasPrefix(value, "Bearer ") {
tokenValue = strings.TrimPrefix(value, "Bearer ")
} else {
tokenValue = strings.TrimSpace(c.Query("access_token"))
}
if tokenValue == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"})
return
}
claims, err := auth.Parse(cfg.JWTSecret, strings.TrimPrefix(value, "Bearer "))
claims, err := auth.Parse(cfg.JWTSecret, tokenValue)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
+249
View File
@@ -0,0 +1,249 @@
package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/1024XEngineer/xinfra/server/internal/config"
"github.com/1024XEngineer/xinfra/server/internal/model"
"github.com/1024XEngineer/xinfra/server/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type DeploymentHandler struct {
cfg config.Config
db *gorm.DB
deployments *service.DeploymentService
}
func NewDeploymentHandler(cfg config.Config, db *gorm.DB, deployments *service.DeploymentService) *DeploymentHandler {
return &DeploymentHandler{cfg: cfg, db: db, deployments: deployments}
}
func (h *DeploymentHandler) Create(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing auth claims"})
return
}
var req service.DeploymentCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !h.ensureBusinessLineMember(c, req.BusinessLineID, claims.UserID, claims.IsAdmin) {
return
}
deployment, err := h.deployments.Create(c.Request.Context(), req, claims.UserID, claims.Username)
if err != nil {
status := http.StatusBadGateway
if errors.Is(err, service.ErrDeploymentNotConfigured) {
status = http.StatusServiceUnavailable
}
c.JSON(status, gin.H{
"error": err.Error(),
"deployment_id": deployment.DeploymentID,
"status": deployment.Status,
})
return
}
c.JSON(http.StatusAccepted, gin.H{"deployment_id": deployment.DeploymentID, "status": deployment.Status})
}
func (h *DeploymentHandler) Get(c *gin.Context) {
deployment, ok := h.getAuthorizedDeployment(c)
if !ok {
return
}
events, err := h.deployments.Events(c.Request.Context(), deployment.DeploymentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deployment": deployment, "events": events})
}
func (h *DeploymentHandler) Events(c *gin.Context) {
deployment, ok := h.getAuthorizedDeployment(c)
if !ok {
return
}
events, err := h.deployments.Events(c.Request.Context(), deployment.DeploymentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
w := c.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
for _, event := range events {
if err := writeSSE(w, event.Type, service.DeploymentEventData(event)); err != nil {
return
}
}
if isTerminalDeploymentStatus(deployment.Status) {
_ = writeSSE(w, service.DeploymentEventDone, gin.H{"status": deployment.Status})
return
}
ch, unsubscribe := h.deployments.Subscribe(deployment.DeploymentID)
defer unsubscribe()
flusher, _ := w.(http.Flusher)
if flusher != nil {
flusher.Flush()
}
for {
select {
case <-c.Request.Context().Done():
return
case event := <-ch:
if err := writeSSE(w, event.Event.Type, event.Data); err != nil {
return
}
if event.Event.Type == service.DeploymentEventDone {
return
}
}
}
}
func (h *DeploymentHandler) Cancel(c *gin.Context) {
deployment, ok := h.getAuthorizedDeployment(c)
if !ok {
return
}
if err := h.deployments.Cancel(c.Request.Context(), deployment.DeploymentID); err != nil {
writeDeploymentError(c, err)
return
}
c.JSON(http.StatusAccepted, gin.H{"deployment_id": deployment.DeploymentID, "status": service.DeploymentStatusCanceling})
}
func (h *DeploymentHandler) InternalEvent(c *gin.Context) {
if !h.authorizeInternal(c) {
return
}
deploymentID := strings.TrimSpace(c.Param("id"))
var req service.DeploymentEventRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
event, err := h.deployments.AppendEvent(c.Request.Context(), deploymentID, req)
if err != nil {
writeDeploymentError(c, err)
return
}
c.JSON(http.StatusAccepted, gin.H{"event_id": event.ID, "seq": event.Seq})
}
func (h *DeploymentHandler) InternalFinish(c *gin.Context) {
if !h.authorizeInternal(c) {
return
}
deploymentID := strings.TrimSpace(c.Param("id"))
var req service.DeploymentFinishRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
deployment, err := h.deployments.Finish(c.Request.Context(), deploymentID, req)
if err != nil {
writeDeploymentError(c, err)
return
}
c.JSON(http.StatusAccepted, gin.H{"deployment_id": deployment.DeploymentID, "status": deployment.Status})
}
func (h *DeploymentHandler) getAuthorizedDeployment(c *gin.Context) (model.Deployment, bool) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing auth claims"})
return model.Deployment{}, false
}
deploymentID := strings.TrimSpace(c.Param("id"))
deployment, err := h.deployments.Get(c.Request.Context(), deploymentID)
if err != nil {
writeDeploymentError(c, err)
return model.Deployment{}, false
}
if !h.ensureBusinessLineMember(c, deployment.BusinessLineID, claims.UserID, claims.IsAdmin) {
return model.Deployment{}, false
}
return deployment, true
}
func (h *DeploymentHandler) ensureBusinessLineMember(c *gin.Context, businessLineID uint64, userID uint64, isAdmin bool) bool {
if businessLineID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "business_line_id is required"})
return false
}
if isAdmin {
return true
}
var binding model.BusinessLineUser
err := h.db.WithContext(c.Request.Context()).Where("business_line_id = ? AND user_id = ?", businessLineID, userID).First(&binding).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not assigned to this business line"})
return false
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *DeploymentHandler) authorizeInternal(c *gin.Context) bool {
if h.cfg.AnsibleInternalToken == "" {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "ansible internal token is not configured"})
return false
}
value := c.GetHeader("Authorization")
if strings.TrimPrefix(value, "Bearer ") != h.cfg.AnsibleInternalToken {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid internal token"})
return false
}
return true
}
func writeSSE(w gin.ResponseWriter, event string, data any) error {
body, err := json.Marshal(data)
if err != nil {
return err
}
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, body); err != nil {
return err
}
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
return nil
}
func writeDeploymentError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrDeploymentNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrDeploymentForbidden):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrDeploymentInvalidState):
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrDeploymentNotConfigured):
c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
}
func isTerminalDeploymentStatus(status string) bool {
return status == service.DeploymentStatusSuccess || status == service.DeploymentStatusFailed || status == service.DeploymentStatusCanceled
}