feat: switch base delivery to awx task logs
This commit is contained in:
@@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -178,65 +177,23 @@ func (h *DeliveryHandler) Cancel(c *gin.Context) {
|
||||
|
||||
// Targets 获取可用部署目标
|
||||
// @Summary 获取可用部署目标
|
||||
// @Description 返回所有已启用的部署目标(如 k8s 集群、主机池)
|
||||
// @Description 从 AWX 动态返回可用 Job Template 及其 Inventory hosts
|
||||
// @Tags delivery
|
||||
// @Produce json
|
||||
// @Param component query string false "组件过滤,例如 mysql"
|
||||
// @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 {
|
||||
items, err := h.service.ListTargets(c.Request.Context(), c.Query("component"))
|
||||
if 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"`
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TaskLogHandler struct {
|
||||
db *gorm.DB
|
||||
delivery *service.DeliveryService
|
||||
wayne *service.WayneRoleBindingService
|
||||
}
|
||||
|
||||
func NewTaskLogHandler(db *gorm.DB, delivery *service.DeliveryService, wayne *service.WayneRoleBindingService) *TaskLogHandler {
|
||||
return &TaskLogHandler{db: db, delivery: delivery, wayne: wayne}
|
||||
}
|
||||
|
||||
type taskLogSummary struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Service string `json:"service"`
|
||||
Name string `json:"name"`
|
||||
Runner string `json:"runner"`
|
||||
Status string `json:"status"`
|
||||
StatusText string `json:"status_text"`
|
||||
StatusClass string `json:"status_class"`
|
||||
BusinessLineID uint64 `json:"business_line_id"`
|
||||
ReferenceID string `json:"reference_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type taskLogLine struct {
|
||||
Time string `json:"time"`
|
||||
Message string `json:"message"`
|
||||
Class string `json:"class"`
|
||||
}
|
||||
|
||||
// List 聚合 AWX 交付任务和 Wayne 部署服务任务。
|
||||
func (h *TaskLogHandler) List(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
source := strings.ToLower(strings.TrimSpace(c.DefaultQuery("source", "all")))
|
||||
businessLineID, _ := strconv.ParseUint(c.Query("business_line_id"), 10, 64)
|
||||
|
||||
items := make([]taskLogSummary, 0)
|
||||
if source == "" || source == "all" || source == "awx" {
|
||||
awxItems, err := h.listAWXTasks(c, claims.UserID, claims.IsAdmin, businessLineID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
items = append(items, awxItems...)
|
||||
}
|
||||
if source == "" || source == "all" || source == "wayne" {
|
||||
wayneItems, err := h.listWayneTasks(c, claims.UserID, claims.IsAdmin, businessLineID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
items = append(items, wayneItems...)
|
||||
}
|
||||
sortTaskLogSummaries(items)
|
||||
if len(items) > 100 {
|
||||
items = items[:100]
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *TaskLogHandler) Get(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
id := c.Param("id")
|
||||
switch {
|
||||
case strings.HasPrefix(id, "awx:"):
|
||||
h.getAWXTask(c, strings.TrimPrefix(id, "awx:"), claims.UserID, claims.IsAdmin)
|
||||
case strings.HasPrefix(id, "wayne:publish:"):
|
||||
h.getWayneTask(c, id, claims.UserID, claims.IsAdmin)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown task log source"})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TaskLogHandler) listAWXTasks(c *gin.Context, userID uint64, isAdmin bool, businessLineID uint64) ([]taskLogSummary, error) {
|
||||
tasks, err := h.delivery.ListTasks(c.Request.Context(), userID, isAdmin, businessLineID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]taskLogSummary, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
items = append(items, awxTaskSummary(task))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (h *TaskLogHandler) listWayneTasks(c *gin.Context, userID uint64, isAdmin bool, businessLineID uint64) ([]taskLogSummary, error) {
|
||||
namespaces, err := h.visibleWayneNamespaces(c, userID, isAdmin, businessLineID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
histories, err := h.wayne.ListDeploymentHistories(c.Request.Context(), namespaces, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]taskLogSummary, 0, len(histories))
|
||||
for _, history := range histories {
|
||||
items = append(items, wayneTaskSummary(history))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (h *TaskLogHandler) getAWXTask(c *gin.Context, taskID string, userID uint64, isAdmin bool) {
|
||||
task, events, err := h.delivery.GetTask(c.Request.Context(), taskID, userID, isAdmin)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "task log not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
lines := make([]taskLogLine, 0, len(events)+16)
|
||||
for _, event := range events {
|
||||
lines = append(lines, taskLogLine{Time: formatTaskLogTime(event.CreatedAt), Message: "[" + event.ToState + "] " + event.Message, Class: classForTaskStatus(event.ToState)})
|
||||
}
|
||||
var execution model.ExecutionJob
|
||||
if err := h.db.WithContext(c.Request.Context()).Where("task_id = ?", task.ID).First(&execution).Error; err == nil && execution.ExecutorJobID != "" && execution.ExecutorJobID != "pending" {
|
||||
stdout, stdoutErr := h.delivery.AWXJobStdout(c.Request.Context(), execution.ExecutorJobID)
|
||||
if stdoutErr != nil {
|
||||
lines = append(lines, taskLogLine{Time: formatTaskLogTime(time.Now()), Message: "[awx] stdout fetch failed: " + stdoutErr.Error(), Class: "err"})
|
||||
} else {
|
||||
lines = append(lines, splitStdoutLines(stdout)...)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"task": awxTaskSummary(*task), "lines": lines})
|
||||
}
|
||||
|
||||
func (h *TaskLogHandler) getWayneTask(c *gin.Context, id string, userID uint64, isAdmin bool) {
|
||||
resourceID, historyID, ok := parseWaynePublishTaskID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Wayne task log id"})
|
||||
return
|
||||
}
|
||||
namespaces, err := h.visibleWayneNamespaces(c, userID, isAdmin, 0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
history, err := h.wayne.GetDeploymentHistory(c.Request.Context(), namespaces, resourceID, historyID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
lines := []taskLogLine{
|
||||
{Time: formatTaskLogTime(history.CreatedAt), Message: "[wayne] publish history #" + strconv.FormatInt(history.ID, 10), Class: classForWaynePublishStatus(history.Status)},
|
||||
{Time: formatTaskLogTime(history.CreatedAt), Message: "[deployment] " + history.ResourceName + " resource_id=" + strconv.FormatInt(history.ResourceID, 10), Class: ""},
|
||||
{Time: formatTaskLogTime(history.CreatedAt), Message: "[cluster] " + history.Cluster + " template_id=" + strconv.FormatInt(history.TemplateID, 10), Class: ""},
|
||||
{Time: formatTaskLogTime(history.CreatedAt), Message: "[user] " + history.User, Class: ""},
|
||||
}
|
||||
if strings.TrimSpace(history.Message) != "" {
|
||||
lines = append(lines, taskLogLine{Time: formatTaskLogTime(history.CreatedAt), Message: "[message] " + history.Message, Class: classForWaynePublishStatus(history.Status)})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"task": wayneTaskSummary(*history), "lines": lines})
|
||||
}
|
||||
|
||||
func awxTaskSummary(task model.DeliveryTask) taskLogSummary {
|
||||
return taskLogSummary{
|
||||
ID: "awx:" + task.ID,
|
||||
Source: "awx",
|
||||
Service: "mysql",
|
||||
Name: "MySQL 标准化交付 · " + task.InstanceName,
|
||||
Runner: "AWX Job Template #" + strconv.FormatUint(task.TargetID, 10),
|
||||
Status: task.Status,
|
||||
StatusText: textForTaskStatus(task.Status),
|
||||
StatusClass: classForTaskStatus(task.Status),
|
||||
BusinessLineID: task.BusinessLineID,
|
||||
ReferenceID: task.ID,
|
||||
CreatedAt: task.CreatedAt,
|
||||
UpdatedAt: task.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func wayneTaskSummary(history service.WayneDeploymentHistory) taskLogSummary {
|
||||
return taskLogSummary{
|
||||
ID: waynePublishTaskID(history),
|
||||
Source: "wayne",
|
||||
Service: "wayne-deployment",
|
||||
Name: wayneDeploymentTaskName(history),
|
||||
Runner: "Wayne Native API",
|
||||
Status: strconv.Itoa(history.Status),
|
||||
StatusText: textForWaynePublishStatus(history.Status),
|
||||
StatusClass: classForWaynePublishStatus(history.Status),
|
||||
BusinessLineID: history.BusinessLineID,
|
||||
ReferenceID: strconv.FormatInt(history.ID, 10),
|
||||
CreatedAt: history.CreatedAt,
|
||||
UpdatedAt: history.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TaskLogHandler) visibleWayneNamespaces(c *gin.Context, userID uint64, isAdmin bool, businessLineID uint64) ([]model.BusinessLineWayneNamespace, error) {
|
||||
query := h.db.WithContext(c.Request.Context()).Order("business_line_id ASC, wayne_namespace_id ASC")
|
||||
if businessLineID != 0 {
|
||||
query = query.Where("business_line_id = ?", businessLineID)
|
||||
}
|
||||
if !isAdmin {
|
||||
query = query.Where("business_line_id IN (?)", h.db.Model(&model.BusinessLineUser{}).Select("business_line_id").Where("user_id = ?", userID))
|
||||
}
|
||||
var namespaces []model.BusinessLineWayneNamespace
|
||||
if err := query.Find(&namespaces).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namespaces, nil
|
||||
}
|
||||
|
||||
func waynePublishTaskID(history service.WayneDeploymentHistory) string {
|
||||
return "wayne:publish:" + strconv.FormatInt(history.ResourceID, 10) + ":" + strconv.FormatInt(history.ID, 10)
|
||||
}
|
||||
|
||||
func parseWaynePublishTaskID(id string) (int64, int64, bool) {
|
||||
parts := strings.Split(id, ":")
|
||||
if len(parts) != 4 || parts[0] != "wayne" || parts[1] != "publish" {
|
||||
return 0, 0, false
|
||||
}
|
||||
resourceID, resourceErr := strconv.ParseInt(parts[2], 10, 64)
|
||||
historyID, historyErr := strconv.ParseInt(parts[3], 10, 64)
|
||||
if resourceErr != nil || historyErr != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return resourceID, historyID, true
|
||||
}
|
||||
|
||||
func wayneDeploymentTaskName(history service.WayneDeploymentHistory) string {
|
||||
name := strings.TrimSpace(history.ResourceName)
|
||||
if name == "" {
|
||||
name = strconv.FormatInt(history.ResourceID, 10)
|
||||
}
|
||||
return "Wayne 服务部署 · " + name
|
||||
}
|
||||
|
||||
func textForTaskStatus(status string) string {
|
||||
switch status {
|
||||
case model.TaskPending, model.TaskValidating, model.TaskDispatching:
|
||||
return "等待"
|
||||
case model.TaskRunning, model.TaskRegistering, model.TaskCanceling:
|
||||
return "执行中"
|
||||
case model.TaskFinished:
|
||||
return "成功"
|
||||
case model.TaskCanceled:
|
||||
return "已取消"
|
||||
default:
|
||||
return "失败"
|
||||
}
|
||||
}
|
||||
|
||||
func classForTaskStatus(status string) string {
|
||||
switch status {
|
||||
case model.TaskFinished:
|
||||
return "ok"
|
||||
case model.TaskExecutionFailed, model.TaskValidationFailed, model.TaskRegisterFailed, model.TaskCanceled:
|
||||
return "err"
|
||||
case model.TaskRunning, model.TaskDispatching, model.TaskRegistering, model.TaskCanceling:
|
||||
return "warn"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func textForWaynePublishStatus(status int) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "成功"
|
||||
case 0:
|
||||
return "失败"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func classForWaynePublishStatus(status int) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "ok"
|
||||
case 0:
|
||||
return "err"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func splitStdoutLines(stdout string) []taskLogLine {
|
||||
lines := make([]taskLogLine, 0)
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, taskLogLine{Time: "", Message: line, Class: classForOutputLine(line)})
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func classForOutputLine(line string) string {
|
||||
lower := strings.ToLower(line)
|
||||
switch {
|
||||
case strings.Contains(lower, "failed") || strings.Contains(lower, "fatal") || strings.Contains(lower, "error"):
|
||||
return "err"
|
||||
case strings.Contains(lower, "ok:") || strings.Contains(lower, "successful") || strings.Contains(lower, "success"):
|
||||
return "ok"
|
||||
case strings.Contains(lower, "changed:"):
|
||||
return "tag-ok"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func formatTaskLogTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format("15:04:05")
|
||||
}
|
||||
|
||||
func sortTaskLogSummaries(items []taskLogSummary) {
|
||||
for i := 1; i < len(items); i++ {
|
||||
item := items[i]
|
||||
j := i - 1
|
||||
for j >= 0 && items[j].UpdatedAt.Before(item.UpdatedAt) {
|
||||
items[j+1] = items[j]
|
||||
j--
|
||||
}
|
||||
items[j+1] = item
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user