Files
aiops/backend/controller/tool_config.go
T
2026-07-09 14:16:56 +08:00

360 lines
11 KiB
Go

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
}
}