feat: 工具配置增强、站点拨测与日志中心多项改进
- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖 - 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败 - Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS - 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率 - 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示 - 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
This commit is contained in:
1 parent
cde7bbe540
commit
7e1c8f583c
84 files changed
+7729
-1507
No files matched your search
@@ -0,0 +1,263 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
"ezops/pkg/remoteexec"
|
||||
)
|
||||
|
||||
// Master 内置节点 ID(0 表示在本机执行) // Logan.Li
|
||||
const masterNodeID uint = 0
|
||||
|
||||
var (
|
||||
nodeRunningCount = make(map[uint]int)
|
||||
masterMaxExecutors = 2
|
||||
nodeMu sync.Mutex
|
||||
)
|
||||
|
||||
// ensureBuildMasterConfig 确保 Master 配置行存在并加载到内存 // Logan.Li
|
||||
func (h *PipelineHandler) ensureBuildMasterConfig() model.BuildMasterConfig {
|
||||
var cfg model.BuildMasterConfig
|
||||
if err := h.db.First(&cfg).Error; err != nil {
|
||||
cfg = model.BuildMasterConfig{MaxExecutors: 2}
|
||||
h.db.Create(&cfg)
|
||||
}
|
||||
if cfg.MaxExecutors <= 0 {
|
||||
cfg.MaxExecutors = 2
|
||||
}
|
||||
nodeMu.Lock()
|
||||
masterMaxExecutors = cfg.MaxExecutors
|
||||
nodeMu.Unlock()
|
||||
return cfg
|
||||
}
|
||||
|
||||
// setMasterMaxExecutors 更新 Master 并发上限(内存 + DB) // Logan.Li
|
||||
func (h *PipelineHandler) setMasterMaxExecutors(n int) model.BuildMasterConfig {
|
||||
if n <= 0 {
|
||||
n = 1
|
||||
}
|
||||
cfg := h.ensureBuildMasterConfig()
|
||||
cfg.MaxExecutors = n
|
||||
h.db.Model(&cfg).Update("max_executors", n)
|
||||
nodeMu.Lock()
|
||||
masterMaxExecutors = n
|
||||
nodeMu.Unlock()
|
||||
return cfg
|
||||
}
|
||||
|
||||
// PipelineExecCtx 流水线执行上下文(Master 或远程 SSH 节点) // Logan.Li
|
||||
type PipelineExecCtx struct {
|
||||
Remote bool
|
||||
SSHCfg *remoteexec.SSHConfig
|
||||
Workspace string
|
||||
NodeID uint
|
||||
NodeName string
|
||||
}
|
||||
|
||||
// tryAcquireBuildNode 尝试占用构建节点执行槽位 // Logan.Li
|
||||
func (h *PipelineHandler) tryAcquireBuildNode(pipeline model.Pipeline) (uint, bool) {
|
||||
nodeMu.Lock()
|
||||
defer nodeMu.Unlock()
|
||||
|
||||
// 指定节点
|
||||
if pipeline.BuildNodeID != nil && *pipeline.BuildNodeID > 0 {
|
||||
id := *pipeline.BuildNodeID
|
||||
var node model.BuildNode
|
||||
if h.db.First(&node, id).Error != nil || !node.Enabled {
|
||||
return h.acquireMasterLocked()
|
||||
}
|
||||
max := node.MaxExecutors
|
||||
if max <= 0 {
|
||||
max = 1
|
||||
}
|
||||
if nodeRunningCount[id] < max {
|
||||
nodeRunningCount[id]++
|
||||
return id, true
|
||||
}
|
||||
return id, false
|
||||
}
|
||||
|
||||
// 自动调度:Master 优先,其次负载最低的远程节点
|
||||
if id, ok := h.acquireMasterLocked(); ok {
|
||||
return id, true
|
||||
}
|
||||
|
||||
var nodes []model.BuildNode
|
||||
h.db.Where("enabled = ?", true).Order("id ASC").Find(&nodes)
|
||||
bestID := uint(0)
|
||||
bestScore := 2.0
|
||||
for _, n := range nodes {
|
||||
max := n.MaxExecutors
|
||||
if max <= 0 {
|
||||
max = 1
|
||||
}
|
||||
score := float64(nodeRunningCount[n.ID]) / float64(max)
|
||||
if score < 1.0 && score <= bestScore {
|
||||
bestScore = score
|
||||
bestID = n.ID
|
||||
}
|
||||
}
|
||||
if bestID > 0 {
|
||||
nodeRunningCount[bestID]++
|
||||
return bestID, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) acquireMasterLocked() (uint, bool) {
|
||||
if nodeRunningCount[masterNodeID] < masterMaxExecutors {
|
||||
nodeRunningCount[masterNodeID]++
|
||||
return masterNodeID, true
|
||||
}
|
||||
return masterNodeID, false
|
||||
}
|
||||
|
||||
// releaseBuildNode 释放构建节点执行槽位 // Logan.Li
|
||||
func (h *PipelineHandler) releaseBuildNode(nodeID uint) {
|
||||
nodeMu.Lock()
|
||||
defer nodeMu.Unlock()
|
||||
if nodeRunningCount[nodeID] > 0 {
|
||||
nodeRunningCount[nodeID]--
|
||||
}
|
||||
}
|
||||
|
||||
// buildExecCtx 根据节点 ID 构建执行上下文 // Logan.Li
|
||||
func (h *PipelineHandler) buildExecCtx(nodeID uint, workspace string) (*PipelineExecCtx, error) {
|
||||
ctx := &PipelineExecCtx{
|
||||
Workspace: workspace,
|
||||
NodeID: nodeID,
|
||||
NodeName: "Master",
|
||||
}
|
||||
if nodeID == masterNodeID {
|
||||
return ctx, nil
|
||||
}
|
||||
var node model.BuildNode
|
||||
if err := h.db.First(&node, nodeID).Error; err != nil {
|
||||
return nil, fmt.Errorf("构建节点不存在")
|
||||
}
|
||||
var srv model.Server
|
||||
if err := h.db.First(&srv, node.ServerID).Error; err != nil {
|
||||
return nil, fmt.Errorf("节点关联服务器不存在")
|
||||
}
|
||||
cfg, err := buildSSHConfig(h.db, srv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.Remote = true
|
||||
ctx.SSHCfg = &cfg
|
||||
ctx.NodeName = node.Name
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// remoteWorkspacePath 计算远程工作区路径 // Logan.Li
|
||||
func remoteWorkspacePath(node model.BuildNode, repoName, code string) string {
|
||||
base := strings.TrimRight(node.WorkspacePath, "/")
|
||||
if base == "" {
|
||||
base = "/data/aiops-workspace"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s-%s", base, repoName, code)
|
||||
}
|
||||
|
||||
// execOnCtx 在 Master 或远程节点执行 shell 命令 // Logan.Li
|
||||
func (h *PipelineHandler) execOnCtx(ctx *PipelineExecCtx, workDir, command string, timeout time.Duration) (string, error) {
|
||||
if timeout <= 0 {
|
||||
timeout = 30 * time.Minute
|
||||
}
|
||||
if !ctx.Remote {
|
||||
cmd := exec.Command("bash", "-c", command)
|
||||
if workDir != "" {
|
||||
cmd.Dir = workDir
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
fullCmd := command
|
||||
if workDir != "" {
|
||||
fullCmd = fmt.Sprintf("cd %s && %s", shellQuotePath(workDir), command)
|
||||
}
|
||||
result, err := remoteexec.Exec(*ctx.SSHCfg, fullCmd, timeout)
|
||||
output := ""
|
||||
if result != nil {
|
||||
output = result.Output
|
||||
}
|
||||
if err != nil || (result != nil && !result.Success) {
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
if result.Error != "" {
|
||||
return output, fmt.Errorf("%s", result.Error)
|
||||
}
|
||||
return output, fmt.Errorf("exit=%d", result.ExitCode)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// prepareWorkspace 初始化工作区(本地或远程) // Logan.Li
|
||||
func (h *PipelineHandler) prepareWorkspace(ctx *PipelineExecCtx, logFn func(string, string, string)) error {
|
||||
if !ctx.Remote {
|
||||
os.RemoveAll(ctx.Workspace)
|
||||
return os.MkdirAll(ctx.Workspace, 0755)
|
||||
}
|
||||
parent := filepath.Dir(ctx.Workspace)
|
||||
_, err := h.execOnCtx(ctx, "", fmt.Sprintf("rm -rf %s && mkdir -p %s", shellQuotePath(ctx.Workspace), shellQuotePath(parent)), 5*time.Minute)
|
||||
return err
|
||||
}
|
||||
|
||||
// syncRemoteWorkspaceToLocal 将远程工作区同步到 Master(部署阶段需要本地产物) // Logan.Li
|
||||
func (h *PipelineHandler) syncRemoteWorkspaceToLocal(ctx *PipelineExecCtx, localWS string, logFn func(string, string, string)) error {
|
||||
if !ctx.Remote || ctx.SSHCfg == nil {
|
||||
return nil
|
||||
}
|
||||
logFn("info", "同步", fmt.Sprintf(">>> 从节点 %s 回拉工作区", ctx.NodeName))
|
||||
tmpTar := localWS + ".tar.gz"
|
||||
os.RemoveAll(localWS)
|
||||
os.MkdirAll(localWS, 0755)
|
||||
|
||||
if err := remoteexec.StreamRemoteTar(*ctx.SSHCfg, ctx.Workspace, tmpTar, 30*time.Minute); err != nil {
|
||||
return fmt.Errorf("远程打包下载失败: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpTar)
|
||||
|
||||
extract := exec.Command("tar", "xzf", tmpTar, "-C", localWS)
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("解压工作区失败: %v, %s", err, string(out))
|
||||
}
|
||||
logFn("success", "同步", "工作区已同步到 Master")
|
||||
return nil
|
||||
}
|
||||
|
||||
// statOnCtx 检查远程/本地文件是否存在 // Logan.Li
|
||||
func (h *PipelineHandler) statOnCtx(ctx *PipelineExecCtx, path string) bool {
|
||||
if !ctx.Remote {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
out, err := h.execOnCtx(ctx, "", fmt.Sprintf("test -e %s && echo yes", shellQuotePath(path)), 30*time.Second)
|
||||
return err == nil && strings.Contains(out, "yes")
|
||||
}
|
||||
|
||||
func shellQuotePath(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
// pathExists 检查 Master/远程路径是否存在 // Logan.Li
|
||||
func (h *PipelineHandler) pathExists(ctx *PipelineExecCtx, path string) bool {
|
||||
return h.statOnCtx(ctx, path)
|
||||
}
|
||||
|
||||
// hasBuildFileCtx 检查目录是否包含可识别的构建文件 // Logan.Li
|
||||
func (h *PipelineHandler) hasBuildFileCtx(ctx *PipelineExecCtx, dir string) bool {
|
||||
for _, f := range []string{"pom.xml", "package.json", "go.mod", "Makefile"} {
|
||||
if h.pathExists(ctx, dir+"/"+f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user