package controller import ( "fmt" "log" "net/http" "strings" "time" "ezops/model" "ezops/pkg/toolinstaller" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // ==================== 工具配置 CRUD ==================== // ToolConfigHandler handles deploy tool configuration type ToolConfigHandler struct { db *gorm.DB installer *toolinstaller.ToolInstaller } // NewToolConfigHandler creates handler func NewToolConfigHandler(db *gorm.DB, ti *toolinstaller.ToolInstaller) *ToolConfigHandler { return &ToolConfigHandler{db: db, installer: ti} } // toolAuditCtx 工具安装审计上下文快照 // Logan.Li type toolAuditCtx struct { userID uint username string ip string userAgent string } func newToolAuditCtx(c *gin.Context) toolAuditCtx { uid, _ := c.Get("userID") uname, _ := c.Get("username") id, _ := uid.(uint) name, _ := uname.(string) return toolAuditCtx{ userID: id, username: name, ip: c.ClientIP(), userAgent: c.Request.UserAgent(), } } func truncateToolAuditText(s string, n int) string { s = strings.TrimSpace(s) if len(s) > n { return s[:n] + "..." } return s } // recordToolAudit 记录工具安装/检测审计(含安装脚本输出与验证结果)// Logan.Li func (h *ToolConfigHandler) recordToolAudit(ac toolAuditCtx, action, toolName, version string, status int, reqDetail, respDetail, errMsg string) { uid := ac.userID h.db.Create(&model.AuditLog{ UserID: &uid, Username: ac.username, IP: ac.ip, UserAgent: ac.userAgent, Module: "deploy_tool", Action: action, Resource: toolName, ResourceID: version, Request: truncateToolAuditText(reqDetail, 2048), Response: truncateToolAuditText(respDetail, 8192), Status: status, Error: truncateToolAuditText(errMsg, 512), }) } // ListTools returns all configured tools func (h *ToolConfigHandler) ListTools(c *gin.Context) { var tools []model.DeployTool h.db.Order("sort_order ASC").Find(&tools) // 列表加载时同步失败/安装中状态(磁盘已存在则自动纠正)// Logan.Li for i := range tools { if tools[i].Status == model.ToolFailed || tools[i].Status == model.ToolInstalling { tools[i] = h.installer.SyncToolStatus(tools[i].ID) } } c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: tools, Total: int64(len(tools))}}) } // GetTool returns a single tool func (h *ToolConfigHandler) GetTool(c *gin.Context) { var tool model.DeployTool if err := h.db.First(&tool, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: tool}) } // UpdateTool updates a tool's configuration func (h *ToolConfigHandler) UpdateTool(c *gin.Context) { var tool model.DeployTool if err := h.db.First(&tool, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"}) return } var req struct { DisplayName *string `json:"display_name"` Description *string `json:"description"` InstallMethod *model.ToolInstallMethod `json:"install_method"` Enabled *bool `json:"enabled"` SortOrder *int `json:"sort_order"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } updates := map[string]interface{}{} if req.DisplayName != nil { updates["display_name"] = *req.DisplayName } if req.Description != nil { updates["description"] = *req.Description } if req.InstallMethod != nil { updates["install_method"] = *req.InstallMethod if *req.InstallMethod == model.ToolDisabled { updates["status"] = model.ToolNotInstalled } } if req.Enabled != nil { updates["enabled"] = *req.Enabled } if req.SortOrder != nil { updates["sort_order"] = *req.SortOrder } if len(updates) > 0 { h.db.Model(&tool).Updates(updates) } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已更新"}) } // InstallTool triggers lazy installation with optional version override func (h *ToolConfigHandler) InstallTool(c *gin.Context) { var tool model.DeployTool if err := h.db.First(&tool, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"}) return } if !tool.Enabled { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "工具已禁用"}) return } if tool.InstallMethod != model.ToolLazy { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "仅懒加载工具可手动安装"}) return } version := tool.DefaultVersion if v := c.Query("version"); v != "" { version = v } if v := c.PostForm("version"); v != "" { version = v } var req struct { Version string `json:"version"` } if err := c.ShouldBindJSON(&req); err == nil && req.Version != "" { version = req.Version } if version != "" && version != tool.DefaultVersion { h.db.Model(&tool).Update("default_version", version) tool.DefaultVersion = version } h.db.Model(&tool).Updates(map[string]interface{}{ "status": model.ToolInstalling, "error_msg": "", }) ac := newToolAuditCtx(c) toolName := tool.Name toolDisplay := tool.DisplayName go func() { log.Printf("[ToolConfig] Background install triggered for %s (version: %s)", toolName, version) reqDetail := fmt.Sprintf("tool=%s version=%s", toolName, version) res := h.installer.ForceInstall(toolName, version) var latest model.DeployTool if h.db.Where("name = ?", toolName).First(&latest).Error == nil { latest = h.installer.SyncToolStatus(latest.ID) } respDetail := res.Output if res.VerifyOut != "" { respDetail += "\n--- verify ---\n" + res.VerifyOut } if res.Installed && res.Version != "" { respDetail += "\n--- version ---\n" + res.Version } if res.Err != nil { log.Printf("[ToolConfig] ERROR installing %s: %v", toolName, res.Err) h.recordToolAudit(ac, "install", toolDisplay, version, 0, reqDetail, respDetail, res.Err.Error()) return } success := res.Installed || latest.Status == model.ToolInstalled log.Printf("[ToolConfig] %s installed successfully (status=%s installed=%v)", toolName, latest.Status, success) if success { h.recordToolAudit(ac, "install", toolDisplay, version, 1, reqDetail, respDetail, "") } else { h.recordToolAudit(ac, "install", toolDisplay, version, 0, reqDetail, respDetail, latest.ErrorMsg) } }() c.JSON(http.StatusOK, model.Response{Code: 200, Message: "安装已触发", Data: gin.H{"version": version}}) } // UninstallTool 卸载工具(清理磁盘资源,可重新选择版本安装)// Logan.Li func (h *ToolConfigHandler) UninstallTool(c *gin.Context) { var tool model.DeployTool if err := h.db.First(&tool, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"}) return } if tool.InstallMethod != model.ToolLazy { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "仅懒加载工具可手动卸载"}) return } if tool.Status != model.ToolInstalled && tool.Status != model.ToolFailed { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "当前状态不可卸载,请先检测或等待安装结束"}) return } ac := newToolAuditCtx(c) toolName := tool.Name toolDisplay := tool.DisplayName installedVer := tool.InstalledVer reqDetail := fmt.Sprintf("tool=%s installed_ver=%s", toolName, installedVer) res := h.installer.Uninstall(toolName) respDetail := res.Output if res.Err != nil { h.recordToolAudit(ac, "uninstall", toolDisplay, installedVer, 0, reqDetail, respDetail, res.Err.Error()) c.JSON(http.StatusOK, model.Response{Code: 500, Message: "卸载失败: " + res.Err.Error(), Data: gin.H{"output": respDetail}}) return } h.recordToolAudit(ac, "uninstall", toolDisplay, installedVer, 1, reqDetail, respDetail, "") c.JSON(http.StatusOK, model.Response{Code: 200, Message: "卸载完成", Data: gin.H{"output": respDetail}}) } // CheckToolStatus re-checks a tool's installation status func (h *ToolConfigHandler) CheckToolStatus(c *gin.Context) { var tool model.DeployTool if err := h.db.First(&tool, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"}) return } // 优先磁盘验证,纠正「审计成功但 DB 仍失败/安装中」// Logan.Li available, version := h.installer.VerifyTool(tool) now := time.Now() if available { h.db.Model(&tool).Updates(map[string]interface{}{ "status": model.ToolInstalled, "installed_ver": version, "error_msg": "", "last_check": &now, }) tool.Status = model.ToolInstalled tool.InstalledVer = version tool.ErrorMsg = "" c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ "available": true, "version": version, "tool": tool, }}) return } // 安装中超时 if tool.Status == model.ToolInstalling && tool.LastCheck != nil && time.Since(*tool.LastCheck) > 10*time.Minute { msg := "安装超时,请重试" h.db.Model(&tool).Updates(map[string]interface{}{ "status": model.ToolFailed, "error_msg": msg, "last_check": &now, }) tool.Status = model.ToolFailed tool.ErrorMsg = msg } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ "available": false, "version": "", "tool": tool, }}) } // GetToolVersions returns available versions for a tool (jdk/node/maven) func (h *ToolConfigHandler) GetToolVersions(c *gin.Context) { var tool model.DeployTool if err := h.db.First(&tool, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"}) return } versions := getPresetVersions(tool.Name) c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ "name": tool.Name, "default": tool.DefaultVersion, "versions": versions, }}) } // getPresetVersions returns known versions for common tools func getPresetVersions(toolName string) []map[string]string { switch toolName { case "jdk": return []map[string]string{ {"version": "11.0.24+8", "label": "JDK 11 (LTS)"}, {"version": "17.0.13+7", "label": "JDK 17 (LTS) ★ 推荐"}, {"version": "21.0.5+11", "label": "JDK 21 (LTS)"}, {"version": "23.0.1+11", "label": "JDK 23 (最新)"}, } case "nodejs": return []map[string]string{ {"version": "lts", "label": "最新 LTS (nvm install --lts) ★ 推荐"}, {"version": "v24.18.0", "label": "Node 24.18.0 (LTS Krypton)"}, {"version": "v22.23.1", "label": "Node 22.23.1 (LTS Jod)"}, {"version": "v20.20.2", "label": "Node 20.20.2 (LTS Iron)"}, {"version": "v18.20.8", "label": "Node 18.20.8 (LTS Hydrogen)"}, {"version": "v16.20.2", "label": "Node 16.20.2 (LTS Gallium)"}, } case "maven": return []map[string]string{ {"version": "3.9.9", "label": "Maven 3.9.9 ★ 推荐"}, {"version": "3.8.8", "label": "Maven 3.8.8"}, {"version": "3.6.3", "label": "Maven 3.6.3"}, } case "golang": return []map[string]string{ {"version": "1.24.0", "label": "Go 1.24.0 ★ 推荐(与平台后端一致)"}, {"version": "1.23.6", "label": "Go 1.23.6 (LTS)"}, {"version": "1.22.12", "label": "Go 1.22.12"}, {"version": "1.21.13", "label": "Go 1.21.13"}, } case "nvm": return []map[string]string{ {"version": "0.40.1", "label": "NVM 0.40.1 ★ 推荐"}, {"version": "0.39.7", "label": "NVM 0.39.7"}, {"version": "0.38.0", "label": "NVM 0.38.0"}, } default: return nil } }