- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖 - 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败 - Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS - 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率 - 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示 - 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
359 lines
11 KiB
Go
359 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
|
|
}
|
|
} |