- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖 - 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败 - Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS - 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率 - 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示 - 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
604 lines
18 KiB
Go
604 lines
18 KiB
Go
package toolinstaller
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"ezops/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ToolInstaller handles lazy installation of devops tools.
|
||
// Each tool gets a sync.Once to prevent concurrent installations.
|
||
type ToolInstaller struct {
|
||
db *gorm.DB
|
||
onces map[string]*sync.Once
|
||
mu sync.Mutex
|
||
}
|
||
|
||
// New creates a ToolInstaller.
|
||
func New(db *gorm.DB) *ToolInstaller {
|
||
return &ToolInstaller{
|
||
db: db,
|
||
onces: make(map[string]*sync.Once),
|
||
}
|
||
}
|
||
|
||
// ensureOnce returns a sync.Once for the given tool name.
|
||
func (ti *ToolInstaller) ensureOnce(name string) *sync.Once {
|
||
ti.mu.Lock()
|
||
defer ti.mu.Unlock()
|
||
if o, ok := ti.onces[name]; ok {
|
||
return o
|
||
}
|
||
o := &sync.Once{}
|
||
ti.onces[name] = o
|
||
return o
|
||
}
|
||
|
||
// InstallResult 工具安装结果(含脚本输出,供审计日志)// Logan.Li
|
||
type InstallResult struct {
|
||
Output string
|
||
VerifyOut string
|
||
Installed bool
|
||
Version string
|
||
Err error
|
||
}
|
||
|
||
// ForceInstall unconditionally installs a tool(重置 Once,允许重复安装)// Logan.Li
|
||
func (ti *ToolInstaller) ForceInstall(toolName string, version string) InstallResult {
|
||
var tool model.DeployTool
|
||
if err := ti.db.Where("name = ?", toolName).First(&tool).Error; err != nil {
|
||
return InstallResult{Err: fmt.Errorf("tool %s not found in DB: %w", toolName, err)}
|
||
}
|
||
|
||
ti.mu.Lock()
|
||
delete(ti.onces, toolName)
|
||
ti.mu.Unlock()
|
||
|
||
// Override version if specified
|
||
if version != "" && version != tool.DefaultVersion {
|
||
ti.db.Model(&tool).Update("default_version", version)
|
||
tool.DefaultVersion = version
|
||
}
|
||
|
||
return ti.installTool(&tool)
|
||
}
|
||
|
||
// EnsureAvailable checks if a tool is available and installs it if configured for lazy loading.
|
||
// Returns nil if the tool is ready, or an error if installation failed.
|
||
func (ti *ToolInstaller) EnsureAvailable(toolName string) error {
|
||
// Find tool in DB
|
||
var tool model.DeployTool
|
||
if err := ti.db.Where("name = ?", toolName).First(&tool).Error; err != nil {
|
||
// Tool not in DB — try to find it by path
|
||
if _, lookErr := exec.LookPath(toolName); lookErr == nil {
|
||
return nil // available even without DB config
|
||
}
|
||
return fmt.Errorf("tool %s not configured in DB", toolName)
|
||
}
|
||
|
||
if !tool.Enabled {
|
||
return fmt.Errorf("tool %s is disabled", toolName)
|
||
}
|
||
|
||
// Check if already installed
|
||
if tool.InstallMethod == model.ToolPreinstall || tool.InstallMethod == model.ToolLazy {
|
||
if inst, err := ti.checkInstalled(tool); err == nil {
|
||
// Update status if needed
|
||
if tool.Status != model.ToolInstalled {
|
||
ti.updateStatus(tool.ID, model.ToolInstalled, inst, "")
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
switch tool.InstallMethod {
|
||
case model.ToolPreinstall:
|
||
return fmt.Errorf("tool %s should be preinstalled but not found", toolName)
|
||
case model.ToolDisabled:
|
||
return fmt.Errorf("tool %s is disabled", toolName)
|
||
case model.ToolLazy:
|
||
// Use sync.Once to prevent concurrent installs
|
||
var installErr error
|
||
once := ti.ensureOnce(toolName)
|
||
once.Do(func() {
|
||
res := ti.installTool(&tool)
|
||
installErr = res.Err
|
||
})
|
||
// If install failed, reset the Once so next call can retry
|
||
if installErr != nil {
|
||
ti.mu.Lock()
|
||
delete(ti.onces, toolName)
|
||
ti.mu.Unlock()
|
||
}
|
||
return installErr
|
||
default:
|
||
return fmt.Errorf("unknown install method: %s", tool.InstallMethod)
|
||
}
|
||
}
|
||
|
||
// VerifyTool 检测工具是否已安装(文件存在 + 可执行验证,不依赖 PATH)// Logan.Li
|
||
func (ti *ToolInstaller) VerifyTool(tool model.DeployTool) (bool, string) {
|
||
switch tool.Name {
|
||
case "jdk":
|
||
for _, p := range []string{"/usr/local/bin/java", "/usr/local/share/jdk/bin/java"} {
|
||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||
cmd := exec.Command(p, "-version")
|
||
out, _ := cmd.CombinedOutput()
|
||
ver := strings.TrimSpace(strings.Split(string(out), "\n")[0])
|
||
if ver != "" {
|
||
return true, ver
|
||
}
|
||
return true, "installed"
|
||
}
|
||
}
|
||
case "maven":
|
||
for _, p := range []string{"/usr/local/bin/mvn", "/usr/local/share/maven/bin/mvn"} {
|
||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||
cmd := exec.Command(p, "--version")
|
||
out, err := cmd.Output()
|
||
if err == nil {
|
||
return true, strings.TrimSpace(strings.Split(string(out), "\n")[0])
|
||
}
|
||
return true, "installed"
|
||
}
|
||
}
|
||
case "nvm":
|
||
nvmSh := "/usr/local/share/nvm/nvm.sh"
|
||
if st, err := os.Stat(nvmSh); err == nil && !st.IsDir() {
|
||
cmd := exec.Command("bash", "-c", fmt.Sprintf(`. "%s" && nvm --version`, nvmSh))
|
||
out, err := cmd.Output()
|
||
if err == nil {
|
||
return true, strings.TrimSpace(string(out))
|
||
}
|
||
return true, tool.DefaultVersion
|
||
}
|
||
case "golang":
|
||
for _, p := range []string{"/usr/local/bin/go", "/usr/local/go/bin/go"} {
|
||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||
cmd := exec.Command(p, "version")
|
||
out, _ := cmd.CombinedOutput()
|
||
ver := strings.TrimSpace(string(out))
|
||
if ver != "" {
|
||
return true, ver
|
||
}
|
||
return true, "installed"
|
||
}
|
||
}
|
||
case "nodejs":
|
||
for _, p := range []string{"/usr/local/bin/node"} {
|
||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||
cmd := exec.Command(p, "--version")
|
||
out, _ := cmd.CombinedOutput()
|
||
ver := strings.TrimSpace(string(out))
|
||
if ver != "" {
|
||
return true, ver
|
||
}
|
||
return true, "installed"
|
||
}
|
||
}
|
||
default:
|
||
if ver, err := ti.runToolCheck(tool); err == nil {
|
||
return true, ver
|
||
}
|
||
}
|
||
return false, ""
|
||
}
|
||
|
||
// SyncToolStatus 根据磁盘实际状态刷新 DB(修复审计成功但页面仍失败/安装中)// Logan.Li
|
||
func (ti *ToolInstaller) SyncToolStatus(toolID uint) model.DeployTool {
|
||
var tool model.DeployTool
|
||
if err := ti.db.First(&tool, toolID).Error; err != nil {
|
||
return tool
|
||
}
|
||
ok, ver := ti.VerifyTool(tool)
|
||
if ok {
|
||
ti.updateStatus(tool.ID, model.ToolInstalled, ver, "")
|
||
tool.Status = model.ToolInstalled
|
||
tool.InstalledVer = ver
|
||
tool.ErrorMsg = ""
|
||
} else if tool.Status == model.ToolInstalling {
|
||
// 保持安装中,由超时逻辑处理
|
||
} else if tool.Status == model.ToolInstalled {
|
||
// 已标记安装但文件不存在 — 降为未安装
|
||
ti.updateStatus(tool.ID, model.ToolNotInstalled, "", "")
|
||
tool.Status = model.ToolNotInstalled
|
||
}
|
||
ti.db.First(&tool, toolID)
|
||
return tool
|
||
}
|
||
|
||
// runToolCheck 执行 PATH/检测命令(不回调 VerifyTool,避免递归栈溢出)// Logan.Li
|
||
func (ti *ToolInstaller) runToolCheck(tool model.DeployTool) (string, error) {
|
||
if tool.BinaryName != "" && tool.BinaryName != "nvm" {
|
||
if _, err := exec.LookPath(tool.BinaryName); err != nil {
|
||
return "", fmt.Errorf("binary %s not found", tool.BinaryName)
|
||
}
|
||
}
|
||
if tool.CheckCommand != "" {
|
||
shell := "sh"
|
||
if tool.Name == "nvm" || tool.Name == "nodejs" {
|
||
shell = "bash"
|
||
}
|
||
cmd := exec.Command(shell, "-c", tool.CheckCommand)
|
||
out, err := cmd.Output()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||
if len(lines) == 0 {
|
||
return "", fmt.Errorf("tool %s check empty output", tool.Name)
|
||
}
|
||
return strings.TrimSpace(lines[0]), nil
|
||
}
|
||
return "", fmt.Errorf("tool %s not available", tool.Name)
|
||
}
|
||
|
||
// checkInstalled runs the tool's check command and returns the version string.
|
||
func (ti *ToolInstaller) checkInstalled(tool model.DeployTool) (string, error) {
|
||
if ok, ver := ti.VerifyTool(tool); ok {
|
||
return ver, nil
|
||
}
|
||
return ti.runToolCheck(tool)
|
||
}
|
||
|
||
// installTool performs the actual installation.
|
||
func (ti *ToolInstaller) installTool(tool *model.DeployTool) InstallResult {
|
||
log.Printf("[ToolInstaller] Installing %s (method=%s)...", tool.Name, tool.InstallMethod)
|
||
|
||
// Mark as installing
|
||
ti.updateStatus(tool.ID, model.ToolInstalling, "", "")
|
||
|
||
var installOut string
|
||
var err error
|
||
if tool.InstallScript != "" {
|
||
installOut, err = ti.runInstallScript(tool)
|
||
} else if tool.DownloadURL != "" {
|
||
installOut, err = ti.downloadBinary(tool)
|
||
} else {
|
||
err = fmt.Errorf("no install script or download URL configured for %s", tool.Name)
|
||
}
|
||
|
||
if err != nil {
|
||
errMsg := err.Error()
|
||
if installOut != "" {
|
||
errMsg = installOut + "\n---\n" + errMsg
|
||
}
|
||
log.Printf("[ToolInstaller] ERROR installing %s: %s", tool.Name, errMsg)
|
||
ti.updateStatus(tool.ID, model.ToolFailed, "", truncateInstallLog(errMsg, 512))
|
||
return InstallResult{Output: installOut, Err: fmt.Errorf("failed to install %s: %w", tool.Name, err)}
|
||
}
|
||
|
||
// 安装后验证(优先文件检测,避免 PATH/source 问题)// Logan.Li
|
||
verifyOut := ti.captureVerifyOutput(*tool)
|
||
if ok, ver := ti.VerifyTool(*tool); ok {
|
||
log.Printf("[ToolInstaller] %s installed successfully: %s", tool.Name, ver)
|
||
ti.updateStatus(tool.ID, model.ToolInstalled, ver, "")
|
||
return InstallResult{Output: installOut, VerifyOut: verifyOut, Installed: true, Version: ver}
|
||
}
|
||
log.Printf("[ToolInstaller] WARNING: %s script ok but verify failed", tool.Name)
|
||
failMsg := "安装脚本执行完成但验证未通过,请点击「测试」重新检测"
|
||
ti.updateStatus(tool.ID, model.ToolFailed, "", failMsg)
|
||
return InstallResult{
|
||
Output: installOut, VerifyOut: verifyOut,
|
||
Err: fmt.Errorf("verify failed after install %s", tool.Name),
|
||
}
|
||
}
|
||
|
||
// captureVerifyOutput 执行验证命令并捕获输出(供审计)// Logan.Li
|
||
func (ti *ToolInstaller) captureVerifyOutput(tool model.DeployTool) string {
|
||
if tool.CheckCommand == "" {
|
||
return ""
|
||
}
|
||
shell := "sh"
|
||
if tool.Name == "nvm" || tool.Name == "nodejs" {
|
||
shell = "bash"
|
||
}
|
||
cmd := exec.Command(shell, "-c", tool.CheckCommand)
|
||
out, err := cmd.CombinedOutput()
|
||
text := strings.TrimSpace(string(out))
|
||
if err != nil && text == "" {
|
||
return err.Error()
|
||
}
|
||
return text
|
||
}
|
||
|
||
func truncateInstallLog(s string, n int) string {
|
||
s = strings.TrimSpace(s)
|
||
if len(s) > n {
|
||
return s[:n] + "..."
|
||
}
|
||
return s
|
||
}
|
||
|
||
// runInstallScript executes the tool's install script in a subshell.
|
||
func (ti *ToolInstaller) runInstallScript(tool *model.DeployTool) (string, error) {
|
||
// Write script to temp file
|
||
tmpFile, err := os.CreateTemp("", fmt.Sprintf("tool-install-%s-*.sh", tool.Name))
|
||
if err != nil {
|
||
return "", fmt.Errorf("cannot create temp script: %w", err)
|
||
}
|
||
defer os.Remove(tmpFile.Name())
|
||
|
||
version := tool.DefaultVersion
|
||
if version == "" {
|
||
version = "latest"
|
||
}
|
||
|
||
script := fmt.Sprintf("#!/bin/sh\nset -e\nTOOL_VERSION=%s\n%s\n", version, tool.InstallScript)
|
||
if _, err := tmpFile.WriteString(script); err != nil {
|
||
return "", fmt.Errorf("cannot write temp script: %w", err)
|
||
}
|
||
tmpFile.Close()
|
||
os.Chmod(tmpFile.Name(), 0755)
|
||
|
||
shell := "sh"
|
||
if tool.Name == "nvm" || tool.Name == "nodejs" {
|
||
shell = "bash"
|
||
}
|
||
cmd := exec.Command(shell, tmpFile.Name())
|
||
var buf bytes.Buffer
|
||
// 同时输出到容器日志便于排障 // Logan.Li
|
||
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
|
||
cmd.Stderr = io.MultiWriter(os.Stderr, &buf)
|
||
err = cmd.Run()
|
||
return buf.String(), err
|
||
}
|
||
|
||
// downloadBinary downloads a binary file to the install path.
|
||
// Uses multi-mirror fallback: tries each mirror URL in order, with HEAD verification
|
||
// to skip dead mirrors before attempting a full download.
|
||
func (ti *ToolInstaller) downloadBinary(tool *model.DeployTool) (string, error) {
|
||
version := tool.DefaultVersion
|
||
if version == "" {
|
||
// Try to get latest version
|
||
if tool.VersionURL != "" {
|
||
v, err := ti.fetchVersion(tool.VersionURL)
|
||
if err != nil {
|
||
log.Printf("[ToolInstaller] WARNING: cannot fetch version for %s: %v, using default", tool.Name, err)
|
||
} else {
|
||
version = v
|
||
}
|
||
}
|
||
}
|
||
if version == "" {
|
||
version = "latest"
|
||
}
|
||
|
||
// Resolve mirror URLs — domestic mirrors prioritized when in China
|
||
mirrorURLs := GetMirrorsForTool(tool.Name, tool.DownloadURL)
|
||
if len(mirrorURLs) == 0 {
|
||
return "", fmt.Errorf("no download URLs configured for %s", tool.Name)
|
||
}
|
||
|
||
// Try each mirror in order
|
||
var lastErr error
|
||
var logBuf strings.Builder
|
||
for i, urlTemplate := range mirrorURLs {
|
||
url := strings.ReplaceAll(urlTemplate, "{version}", version)
|
||
log.Printf("[ToolInstaller] Downloading %s from mirror[%d]: %s", tool.Name, i, url)
|
||
logBuf.WriteString(fmt.Sprintf("[%d] %s\n", i, url))
|
||
|
||
err := ti.tryDownload(url, tool)
|
||
if err == nil {
|
||
logBuf.WriteString(fmt.Sprintf("OK: downloaded from mirror[%d]\n", i))
|
||
return logBuf.String(), nil
|
||
}
|
||
lastErr = err
|
||
logBuf.WriteString(fmt.Sprintf("FAIL mirror[%d]: %v\n", i, err))
|
||
log.Printf("[ToolInstaller] Mirror[%d] failed for %s: %v, trying next...", i, tool.Name, err)
|
||
}
|
||
|
||
return logBuf.String(), fmt.Errorf("all %d mirrors exhausted for %s, last error: %w", len(mirrorURLs), tool.Name, lastErr)
|
||
}
|
||
|
||
// tryDownload attempts a single download from a URL.
|
||
func (ti *ToolInstaller) tryDownload(url string, tool *model.DeployTool) error {
|
||
// Use a client with timeout for the download phase.
|
||
// Large files (kubectl ~50MB) can take minutes on slow networks.
|
||
client := &http.Client{Timeout: 30 * time.Minute}
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
return fmt.Errorf("download failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return fmt.Errorf("download returned HTTP %d", resp.StatusCode)
|
||
}
|
||
|
||
// Write to temp file first, then rename (atomic-ish)
|
||
installPath := tool.InstallPath
|
||
if installPath == "" {
|
||
installPath = filepath.Join("/usr/local/bin", tool.BinaryName)
|
||
}
|
||
|
||
dir := filepath.Dir(installPath)
|
||
tmpFile, err := os.CreateTemp(dir, fmt.Sprintf(".%s-download-*", tool.Name))
|
||
if err != nil {
|
||
return fmt.Errorf("cannot create temp file: %w", err)
|
||
}
|
||
defer os.Remove(tmpFile.Name())
|
||
|
||
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
||
tmpFile.Close()
|
||
return fmt.Errorf("failed to save download: %w", err)
|
||
}
|
||
tmpFile.Close()
|
||
|
||
os.Chmod(tmpFile.Name(), 0755)
|
||
|
||
if err := os.Rename(tmpFile.Name(), installPath); err != nil {
|
||
// Fallback: copy
|
||
data, _ := os.ReadFile(tmpFile.Name())
|
||
if writeErr := os.WriteFile(installPath, data, 0755); writeErr != nil {
|
||
return fmt.Errorf("cannot move binary to %s: %w", installPath, writeErr)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// fetchVersion gets the latest version string from a URL.
|
||
func (ti *ToolInstaller) fetchVersion(url string) (string, error) {
|
||
client := &http.Client{Timeout: 15 * time.Second}
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return strings.TrimSpace(string(body)), nil
|
||
}
|
||
|
||
// updateStatus updates the tool's status in the database.
|
||
func (ti *ToolInstaller) updateStatus(toolID uint, status model.ToolStatus, version string, errMsg string) {
|
||
now := time.Now()
|
||
updates := map[string]interface{}{
|
||
"status": status,
|
||
"last_check": &now,
|
||
}
|
||
if version != "" {
|
||
updates["installed_ver"] = version // 与 GORM 列名一致 // Logan.Li
|
||
}
|
||
if errMsg != "" {
|
||
updates["error_msg"] = errMsg
|
||
} else if status == model.ToolInstalled {
|
||
updates["error_msg"] = ""
|
||
}
|
||
ti.db.Model(&model.DeployTool{}).Where("id = ?", toolID).Updates(updates)
|
||
}
|
||
|
||
// SeedTools inserts predefined tools into the database if the table is empty.
|
||
func (ti *ToolInstaller) SeedTools() {
|
||
var count int64
|
||
ti.db.Model(&model.DeployTool{}).Count(&count)
|
||
if count > 0 {
|
||
// U4: 即使已有数据也升级工具配置(git/docker-cli 从 preinstall→lazy)
|
||
ti.UpgradeTools()
|
||
return
|
||
}
|
||
|
||
tools := model.PredefinedTools()
|
||
now := time.Now()
|
||
for i := range tools {
|
||
tools[i].CreatedAt = now
|
||
tools[i].UpdatedAt = now
|
||
// Check if preinstalled tools are actually available
|
||
if tools[i].InstallMethod == model.ToolPreinstall {
|
||
if ver, err := ti.checkInstalled(tools[i]); err == nil {
|
||
tools[i].Status = model.ToolInstalled
|
||
tools[i].InstalledVer = ver
|
||
} else {
|
||
tools[i].Status = model.ToolNotInstalled
|
||
}
|
||
}
|
||
}
|
||
|
||
if err := ti.db.Create(&tools).Error; err != nil {
|
||
log.Printf("[ToolInstaller] Seed failed: %v", err)
|
||
} else {
|
||
log.Printf("[ToolInstaller] Seeded %d tools", len(tools))
|
||
}
|
||
|
||
// U4: 新种子也执行升级
|
||
ti.UpgradeTools()
|
||
}
|
||
|
||
// UpgradeTools updates existing tool configs to match latest predefined definitions.
|
||
// U4: git/docker-cli 从 preinstall 迁移到 lazy(镜像瘦身)。
|
||
func (ti *ToolInstaller) UpgradeTools() {
|
||
upgrades := map[string]struct {
|
||
installMethod model.ToolInstallMethod
|
||
installScript string
|
||
description string
|
||
}{
|
||
"git": {
|
||
installMethod: model.ToolLazy,
|
||
installScript: "apk add --no-cache git",
|
||
description: "版本控制工具,流水线代码拉取使用(首次使用自动安装)",
|
||
},
|
||
"docker-cli": {
|
||
installMethod: model.ToolLazy,
|
||
installScript: "apk add --no-cache docker-cli",
|
||
description: "Docker 命令行工具,流水线镜像构建/推送使用(首次使用自动安装)",
|
||
},
|
||
}
|
||
|
||
for name, up := range upgrades {
|
||
var tool model.DeployTool
|
||
if err := ti.db.Where("name = ?", name).First(&tool).Error; err != nil {
|
||
continue // tool not in DB, skip
|
||
}
|
||
if tool.InstallMethod == up.installMethod && tool.InstallScript == up.installScript {
|
||
continue // already up to date
|
||
}
|
||
log.Printf("[ToolInstaller] U4 upgrade: %s %s→%s", name, tool.InstallMethod, up.installMethod)
|
||
ti.db.Model(&model.DeployTool{}).Where("name = ?", name).Updates(map[string]interface{}{
|
||
"install_method": up.installMethod,
|
||
"install_script": up.installScript,
|
||
"description": up.description,
|
||
})
|
||
}
|
||
|
||
// 补全新增工具(如 golang)并校正排序 // Logan.Li
|
||
now := time.Now()
|
||
for _, def := range model.PredefinedTools() {
|
||
var existing model.DeployTool
|
||
if err := ti.db.Where("name = ?", def.Name).First(&existing).Error; err != nil {
|
||
def.CreatedAt = now
|
||
def.UpdatedAt = now
|
||
def.Status = model.ToolNotInstalled
|
||
if err := ti.db.Create(&def).Error; err != nil {
|
||
log.Printf("[ToolInstaller] Failed to seed missing tool %s: %v", def.Name, err)
|
||
} else {
|
||
log.Printf("[ToolInstaller] Added missing tool: %s", def.Name)
|
||
}
|
||
}
|
||
}
|
||
sortBumps := map[string]int{"jdk": 1, "maven": 2, "golang": 3, "nvm": 4, "nodejs": 5, "git": 6, "docker-cli": 7}
|
||
for name, order := range sortBumps {
|
||
ti.db.Model(&model.DeployTool{}).Where("name = ?", name).Update("sort_order", order)
|
||
}
|
||
|
||
// 同步 JDK/Maven/Go/NVM 安装脚本与检测命令修复 // Logan.Li
|
||
type toolPatch struct {
|
||
script string
|
||
check string
|
||
}
|
||
patchSync := map[string]toolPatch{}
|
||
for _, t := range model.PredefinedTools() {
|
||
if t.Name == "jdk" || t.Name == "maven" || t.Name == "golang" || t.Name == "nvm" || t.Name == "nodejs" {
|
||
patchSync[t.Name] = toolPatch{script: t.InstallScript, check: t.CheckCommand}
|
||
}
|
||
}
|
||
for name, p := range patchSync {
|
||
updates := map[string]interface{}{
|
||
"install_script": p.script,
|
||
"check_command": p.check,
|
||
}
|
||
if name == "nodejs" {
|
||
updates["default_version"] = "lts"
|
||
}
|
||
if name == "golang" {
|
||
updates["default_version"] = "1.24.0"
|
||
}
|
||
ti.db.Model(&model.DeployTool{}).Where("name = ?", name).Updates(updates)
|
||
}
|
||
}
|