package controller import ( "net/http" "strconv" "strings" "time" "ezops/model" "ezops/pkg/remoteexec" "github.com/gin-gonic/gin" ) // ListBuildNodes 构建节点列表 // Logan.Li func (h *PipelineHandler) ListBuildNodes(c *gin.Context) { masterCfg := h.ensureBuildMasterConfig() var nodes []model.BuildNode h.db.Order("id ASC").Find(&nodes) // 填充运行时负载与服务器信息 nodeMu.Lock() for i := range nodes { nodes[i].RunningCount = nodeRunningCount[nodes[i].ID] var srv model.Server if err := h.db.First(&srv, nodes[i].ServerID).Error; err == nil { nodes[i].Hostname = srv.Hostname nodes[i].IP = srv.IP } } masterRunning := nodeRunningCount[0] nodeMu.Unlock() c.JSON(http.StatusOK, model.Response{ Code: 200, Data: map[string]interface{}{ "list": nodes, "total": len(nodes), "master": map[string]interface{}{ "name": "Master", "max_executors": masterCfg.MaxExecutors, "running_count": masterRunning, }, }, }) } // CreateBuildNode 创建构建节点 // Logan.Li func (h *PipelineHandler) CreateBuildNode(c *gin.Context) { var req model.CreateBuildNodeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()}) return } req.Name = strings.TrimSpace(req.Name) if req.Name == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "节点名称不能为空"}) return } var srv model.Server if err := h.db.First(&srv, req.ServerID).Error; err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "关联服务器不存在"}) return } var dup int64 h.db.Model(&model.BuildNode{}).Where("name = ?", req.Name).Count(&dup) if dup > 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "节点名称已存在"}) return } node := model.BuildNode{ Name: req.Name, Description: req.Description, ServerID: req.ServerID, Labels: req.Labels, MaxExecutors: req.MaxExecutors, WorkspacePath: req.WorkspacePath, Enabled: true, } if node.MaxExecutors <= 0 { node.MaxExecutors = 2 } if strings.TrimSpace(node.WorkspacePath) == "" { node.WorkspacePath = "/data/aiops-workspace" } if err := h.db.Create(&node).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败"}) return } node.Hostname = srv.Hostname node.IP = srv.IP c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: node}) } // UpdateBuildNode 更新构建节点 // Logan.Li func (h *PipelineHandler) UpdateBuildNode(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) var node model.BuildNode if err := h.db.First(&node, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "节点不存在"}) return } var req model.UpdateBuildNodeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"}) return } updates := map[string]interface{}{} if req.Name != "" { updates["name"] = strings.TrimSpace(req.Name) } if req.Description != "" { updates["description"] = req.Description } if req.ServerID != nil { var srv model.Server if err := h.db.First(&srv, *req.ServerID).Error; err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "关联服务器不存在"}) return } updates["server_id"] = *req.ServerID } if req.Labels != "" { updates["labels"] = req.Labels } if req.MaxExecutors > 0 { updates["max_executors"] = req.MaxExecutors } if req.WorkspacePath != "" { updates["workspace_path"] = req.WorkspacePath } if req.Enabled != nil { updates["enabled"] = *req.Enabled } if len(updates) == 0 { c.JSON(http.StatusOK, model.Response{Code: 200, Data: node}) return } if err := h.db.Model(&node).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"}) return } h.db.First(&node, id) c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: node}) } // DeleteBuildNode 删除构建节点 // Logan.Li func (h *PipelineHandler) DeleteBuildNode(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) var inUse int64 h.db.Model(&model.Pipeline{}).Where("build_node_id = ?", id).Count(&inUse) if inUse > 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "仍有流水线绑定此节点,请先解除关联"}) return } if err := h.db.Delete(&model.BuildNode{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } // TestBuildNode 测试构建节点 SSH 连通性 // Logan.Li func (h *PipelineHandler) TestBuildNode(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) var node model.BuildNode if err := h.db.First(&node, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "节点不存在"}) return } var srv model.Server if err := h.db.First(&srv, node.ServerID).Error; err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "关联服务器不存在"}) return } cfg, err := buildSSHConfig(h.db, srv) if err != nil { c.JSON(http.StatusOK, model.Response{Code: 200, Data: map[string]interface{}{ "success": false, "message": err.Error(), }}) return } result, err := remoteexec.Exec(cfg, "echo aiops-node-ok && uname -a", 15*time.Second) if err != nil || !result.Success { msg := "" if result != nil { msg = result.Error if msg == "" { msg = result.Output } } if err != nil { msg = err.Error() } c.JSON(http.StatusOK, model.Response{Code: 200, Data: map[string]interface{}{ "success": false, "message": msg, }}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: map[string]interface{}{ "success": true, "message": strings.TrimSpace(result.Output), }}) } // UpdateBuildMaster 更新 Master 内置节点最大并发(1=串行按触发顺序排队) // Logan.Li func (h *PipelineHandler) UpdateBuildMaster(c *gin.Context) { var req model.UpdateBuildMasterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()}) return } cfg := h.setMasterMaxExecutors(req.MaxExecutors) nodeMu.Lock() running := nodeRunningCount[masterNodeID] nodeMu.Unlock() c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "已更新", Data: map[string]interface{}{ "name": "Master", "max_executors": cfg.MaxExecutors, "running_count": running, }, }) }