From 31b329ca5bc209eb4de93b34b2608975e7f74be1 Mon Sep 17 00:00:00 2001 From: yumaojun03 <719118794@qq.com> Date: Sun, 8 Mar 2026 18:05:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85script=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devops/README.md | 7 +- devops/agent/README.md | 9 +- devops/agent/connect/README.md | 4 + devops/agent/script/README.md | 5 +- devops/agent/script/debug_script.go | 78 ++++++++ devops/agent/script/hashes.json | 1 + devops/agent/script/interface.go | 125 ++++++++++++ devops/agent/script/runner.go | 204 ++++++++++++++++++++ devops/agent/script/script_integrity.go | 169 +++++++++++++++++ devops/agent/script/tools.go | 22 +++ devops/agent/shells/README.md | 10 + devops/agent/shells/lib.sh | 33 ++++ devops/agent/shells/task_debug.sh | 208 ++++++++++++++++++++ devops/agent/tasks/README.md | 4 + devops/agent/tasks/interface.go | 13 ++ devops/agent/tasks/task_debug/README.md | 5 + devops/agent/tasks/task_debug/impl.go | 15 ++ devops/docs/agent.drawio | 40 ++++ devops/docs/agent.png | Bin 0 -> 38722 bytes devops/go.mod | 25 +++ devops/go.sum | 52 +++++ devops/server/apps/README.md | 2 + devops/server/apps/task/enum.go | 27 +++ devops/server/apps/task/model.go | 240 ++++++++++++++++++++++++ 24 files changed, 1293 insertions(+), 5 deletions(-) create mode 100644 devops/agent/connect/README.md create mode 100644 devops/agent/script/debug_script.go create mode 100644 devops/agent/script/hashes.json create mode 100644 devops/agent/script/interface.go create mode 100644 devops/agent/script/runner.go create mode 100644 devops/agent/script/script_integrity.go create mode 100644 devops/agent/script/tools.go create mode 100644 devops/agent/shells/README.md create mode 100644 devops/agent/shells/lib.sh create mode 100644 devops/agent/shells/task_debug.sh create mode 100644 devops/agent/tasks/README.md create mode 100644 devops/agent/tasks/interface.go create mode 100644 devops/agent/tasks/task_debug/README.md create mode 100644 devops/agent/tasks/task_debug/impl.go create mode 100644 devops/docs/agent.drawio create mode 100644 devops/docs/agent.png create mode 100644 devops/go.mod create mode 100644 devops/go.sum create mode 100644 devops/server/apps/README.md create mode 100644 devops/server/apps/task/enum.go create mode 100644 devops/server/apps/task/model.go diff --git a/devops/README.md b/devops/README.md index 27c761a..b9d4973 100644 --- a/devops/README.md +++ b/devops/README.md @@ -1,3 +1,8 @@ # DevOps平台 - +```sh +# 初始化 devops平台工程 +➜ devops git:(main) ✗ go mod tidy +go: finding module for package github.com/infraboard/mcube/tools/pretty +go: found github.com/infraboard/mcube/tools/pretty in github.com/infraboard/mcube v1.9.29 +``` diff --git a/devops/agent/README.md b/devops/agent/README.md index 0c16958..f515b6a 100644 --- a/devops/agent/README.md +++ b/devops/agent/README.md @@ -1 +1,8 @@ -# DevOps Agent \ No newline at end of file +# DevOps Agent + +1. WebSocket Agent (Jenkins Node), 把自己注册到 Api Server 作为一个任务运行阶段 +2. 需要执行来自于 Api Server 下发的任务, 执行中需要把日志和执行结果返回给 Api Server +3. 怎么运行任务喃,我们是封装一个 script的模块, 然后调用这个模块来运行任务 +4. 任务需要有任务名称, 与 任务参数 这些基础信息 + + diff --git a/devops/agent/connect/README.md b/devops/agent/connect/README.md new file mode 100644 index 0000000..ac77c4c --- /dev/null +++ b/devops/agent/connect/README.md @@ -0,0 +1,4 @@ +# 连接管理 + +管理与Api的通信 + diff --git a/devops/agent/script/README.md b/devops/agent/script/README.md index 535f7bb..84489b2 100644 --- a/devops/agent/script/README.md +++ b/devops/agent/script/README.md @@ -3,10 +3,9 @@ 首先实现一个自己版本的 脚本执行器 核心功能: - + 支持workspace, 固定工作目录 -+ 制定脚本路径,可以直接执行 -+ 脚本输入是 环境变量 ++ 制定脚本路径,可以直接执行, 脚本存放的路径是可以配置, 默认是 shells 目录 ++ 脚本输入是 环境变量(环境变量有比较好的隔离性, 脚本的编写逻辑要清晰) + 输出到制定 文件(环境变量文件) + 执行过程中的日志 + debug.sh, 生产调试脚本, 基于改脚本,可以重复执行(记录执行中的参数,方便回放) diff --git a/devops/agent/script/debug_script.go b/devops/agent/script/debug_script.go new file mode 100644 index 0000000..9e1fd55 --- /dev/null +++ b/devops/agent/script/debug_script.go @@ -0,0 +1,78 @@ +package script + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// WriteDebugScript 生成debug.sh调试脚本到工作目录 +// 通过环境变量 DEBUG_SCRIPT 控制是否生成 (值为 true 或 1 时生成) +// 返回生成的文件路径,如果未生成则返回空字符串 +func (s *ExecuteScriptRequest) WriteDebugScript(scriptPath string, args []string) (string, error) { + if !s.isDebugScriptEnabled() { + return "", nil + } + + var sb strings.Builder + + sb.WriteString("#!/bin/bash\n") + sb.WriteString("# ========== 调试脚本 (自动生成) ==========\n") + sb.WriteString("# 警告: 此脚本可能包含敏感信息,请勿提交到版本控制系统\n") + fmt.Fprintf(&sb, "# 生成时间: %s\n", time.Now().Format("2006-01-02 15:04:05")) + fmt.Fprintf(&sb, "# 工作目录: %s\n", s.workDir) + fmt.Fprintf(&sb, "# 脚本路径: %s\n", scriptPath) + if len(args) > 0 { + fmt.Fprintf(&sb, "# 脚本参数: %v\n", args) + } + sb.WriteString("# ==========================================\n\n") + + sb.WriteString("set -e\n\n") + + // 设置环境变量 + if len(s.envVars) > 0 { + sb.WriteString("# 设置环境变量\n") + // 按key排序以保证输出一致性 + keys := make([]string, 0, len(s.envVars)) + for k := range s.envVars { + keys = append(keys, k) + } + // 简单排序 + for i := 0; i < len(keys); i++ { + for j := i + 1; j < len(keys); j++ { + if keys[i] > keys[j] { + keys[i], keys[j] = keys[j], keys[i] + } + } + } + for _, key := range keys { + value := s.envVars[key] + // 转义值中的特殊字符 + escapedValue := strings.ReplaceAll(value, "'", "'\"'\"'") + fmt.Fprintf(&sb, "export %s='%s'\n", key, escapedValue) + } + sb.WriteString("\n") + } + + // 执行脚本命令 + sb.WriteString("# 执行脚本\n") + sb.WriteString("exec ") + sb.WriteString(scriptPath) + for _, arg := range args { + sb.WriteString(" ") + // 转义参数中的特殊字符 + escapedArg := strings.ReplaceAll(arg, "'", "'\"'\"'") + fmt.Fprintf(&sb, " '%s'", escapedArg) + } + sb.WriteString("\n") + + // 写入文件 + debugScriptPath := filepath.Join(s.workDir, "debug.sh") + if err := os.WriteFile(debugScriptPath, []byte(sb.String()), 0755); err != nil { + return "", fmt.Errorf("failed to write debug script: %v", err) + } + + return debugScriptPath, nil +} diff --git a/devops/agent/script/hashes.json b/devops/agent/script/hashes.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/devops/agent/script/hashes.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/devops/agent/script/interface.go b/devops/agent/script/interface.go new file mode 100644 index 0000000..218f4ae --- /dev/null +++ b/devops/agent/script/interface.go @@ -0,0 +1,125 @@ +package script + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +// ExecuteScriptRequest 定义了执行脚本所需的参数和配置 +type ExecuteScriptRequest struct { + ScriptPath string + Args []string + Assets []string // 资产目录列表 + + // 脚本工作目录(默认当前目录) + workDir string + // 脚本执行环境变量(默认空) + envVars map[string]string + // 脚本执行日志文件路径(默认空,表示不记录日志) + logFile string + // 脚本执行超时时间(默认 0,表示不超时) + timeout time.Duration + // 脚本执行的命令元数据(可选),用于日志记录和监控 + metadata *CommandMetadata + // 脚本执行结果文件路径(默认空,表示不保存结果) + resultFile string + // 需要收集内容的文件列表 + collectFiles []string + + // 日志回调函数, 用于实时输出日志(默认 nil,表示不使用回调) + logCallback func(string) + + // 进程组管理控制参数, 用于避免脚本执行过程中,产生的子进程无法被正确杀死的问题 + // 是否创建新的进程组(默认 true,用于杀死子进程树) + // 设为 false 时,子进程不会被放入新的进程组,不能被组杀 + createProcessGroup bool + // 是否自定义 Cancel 函数以杀死进程组(默认 true) + // 设为 false 时,使用默认的进程杀死方式 + useProcessGroupKill bool + + // 命令执行信息 + cmd *exec.Cmd +} + +// SetEnv 设置环境变量, key会被强制转换为大写 +func (s *ExecuteScriptRequest) SetEnv(key, value string) { + key = strings.ToUpper(strings.TrimSpace(key)) + s.envVars[key] = value + if s.metadata != nil { + s.metadata.EnvVars[key] = value + } +} + +// buildEnv 构建环境变量, 将请求中的环境变量与系统环境变量合并,返回一个新的环境变量列表 +func (s *ExecuteScriptRequest) buildEnv() []string { + env := os.Environ() // 获取系统环境变量 + + // 补充自定义环境变量 + for key, value := range s.envVars { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + + return env +} + +// isDebugScriptEnabled 检查是否启用调试脚本 +// 通过环境变量 DEBUG_SCRIPT 控制 (值为 true 或 1 时启用) +func (s *ExecuteScriptRequest) isDebugScriptEnabled() bool { + if val, ok := s.envVars["DEBUG_SCRIPT"]; ok { + return strings.EqualFold(val, "true") || val == "1" + } + return false +} + +func (r *ExecuteScriptRequest) WithWorkspacePrefix(workDirPrefix string) *ExecuteScriptRequest { + if strings.HasPrefix(r.workDir, workDirPrefix) { + return r + } + r.workDir = workDirPrefix + "/" + r.workDir + return r +} + +// CommandMetadata 命令元数据 +type CommandMetadata struct { + ID string `json:"id"` // 命令唯一ID + Name string `json:"name"` // 命令名称 + Description string `json:"description,omitempty"` // 命令描述 + Tags []string `json:"tags,omitempty"` // 标签 + CreatedBy string `json:"created_by"` // 创建者 + CreatedAt time.Time `json:"created_at"` // 创建时间 + Timeout time.Duration `json:"timeout"` // 超时时间 + EnvVars map[string]string `json:"env_vars"` // 环境变量 + WorkDir string `json:"work_dir"` // 工作目录 + RefTask string `json:"ref_task,omitempty"` // 关联的任务 +} + +// ExecutionResult 命令执行结果 +type ExecutionResult struct { + // 命令 + Command string `json:"command"` + // 错误原因 + Error string `json:"error,omitempty"` + // 命令退出码 + ExitCode int `json:"exit_code"` + // 命令开始执行时间 + StartTime time.Time `json:"start_time"` + // 命令结束执行时间 + EndTime *time.Time `json:"end_time"` + // 命令执行时长 + Duration time.Duration `json:"duration"` + // 命令执行是否成功 + Success bool `json:"success"` + // 是否跳过执行(跳过视为成功,但标记为 Skip 以便管道状态同步) + Skipped bool `json:"skipped,omitempty"` + // 非错误的说明信息(比如跳过原因等) + Message string `json:"message,omitempty"` + // 元数据 + Metadata *CommandMetadata `json:"metadata"` + // 文件内容集合 + FileContents map[string]string `json:"file_contents,omitempty"` + // 脚本输出参数(供下一个任务使用) + OutputParams map[string]string `json:"output_params,omitempty"` +} diff --git a/devops/agent/script/runner.go b/devops/agent/script/runner.go new file mode 100644 index 0000000..970d8fa --- /dev/null +++ b/devops/agent/script/runner.go @@ -0,0 +1,204 @@ +package script + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/rs/zerolog" +) + +// ScriptExcutor 脚本执行器,负责制定脚本的执行逻辑,比如准备环境变量、执行脚本、处理输出等 +type ScriptExcutor struct { + // 工作目录前缀,用于确保脚本执行在指定的目录下 + WorkDirPrefix string + // 脚本目录前缀,用于确保脚本路径在指定的目录下, 避免执行不安全的脚本 + ScriptDirPrefix string + // 是否启用脚本完整性校验, 通过编译时嵌入的脚本 hash 来校验脚本的完整性, 防止脚本被篡改 + IsVerifyScriptIntegrity bool + + // 日志记录器,用于记录脚本执行的日志信息 + log *zerolog.Logger + // 脚本完整性管理器,用于校验脚本的完整性 + integrityManager *ScriptIntegrityManager +} + +func (e *ScriptExcutor) Init() error { + // 初始化脚本完整性管理器 + e.integrityManager = NewScriptIntegrityManager(e.ScriptDirPrefix, e.IsVerifyScriptIntegrity) + return nil +} + +// ExecuteScript 执行脚本的核心方法,接受一个 ExecuteScriptRequest 请求,返回一个 ExecutionResult 结果 +func (e *ScriptExcutor) ExecuteScript(ctx context.Context, in *ExecuteScriptRequest) (*ExecutionResult, error) { + // 确保工作目录前缀, 避免脚本执行在不安全的目录下, 同时也方便清理执行结果 + in = in.WithWorkspacePrefix(e.WorkDirPrefix) + + // 确保工作目录存在 + if err := e.ensureWorkDir(in.workDir); err != nil { + e.log.Error().Str("task_id", in.metadata.ID).Str("work_dir", in.workDir).Err(err).Msg("创建工作目录失败") + return nil, err + } + + // 确保元数据存在 + if in.metadata == nil { + in.metadata = &CommandMetadata{ + ID: generateID(), + CreatedBy: getCurrentUser(), + CreatedAt: time.Now(), + WorkDir: in.workDir, + EnvVars: in.envVars, + Timeout: in.timeout, + } + } + + // zerolog 的用法 + e.log.Info().Str("task_id", in.metadata.ID).Str("script_path", in.ScriptPath).Msg("准备执行脚本") + + // 脚本路径消毒 + scriptPath := e.WithPrefixScripPath(in.ScriptPath) + fullScriptPath, err := e.sanitizeScriptPath(scriptPath) + if err != nil { + e.log.Error().Str("task_id", in.metadata.ID).Str("script_path", scriptPath).Err(err).Msg("脚本路径验证失败") + return nil, err + } + e.log.Info().Str("task_id", in.metadata.ID).Str("full_script_path", fullScriptPath).Msg("脚本路径验证成功") + + // 校验脚本完整性 + if err := e.integrityManager.VerifyScript(fullScriptPath); err != nil { + e.log.Error().Str("task_id", in.metadata.ID).Str("script_path", fullScriptPath).Err(err).Msg("脚本完整性校验失败") + return nil, fmt.Errorf("脚本完整性校验失败: %v", err) + } + + // 构建完整的shell命令 + shellArgs := []string{fullScriptPath} + shellArgs = append(shellArgs, in.Args...) + shellCommand := strings.Join(shellArgs, " ") + + e.log.Info().Str("task_id", in.metadata.ID).Str("command", shellCommand).Msg("脚本执行命令") + + // 注入标准变量 + e.InjectEnv(in) + + // 根据超时设置创建 context + execCtx := ctx + var cancel context.CancelFunc + if in.timeout > 0 { + execCtx, cancel = context.WithTimeout(ctx, in.timeout) + defer cancel() + e.log.Info().Str("task_id", in.metadata.ID).Dur("timeout", in.timeout).Msg("设置脚本执行超时时间") + } + + // 创建命令 + cmd := exec.CommandContext(execCtx, "/bin/sh", "-c", shellCommand) + cmd.Dir = in.workDir + cmd.Env = in.buildEnv() + // 根据参数设置进程组属性 + if in.createProcessGroup { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, // 创建新的进程组 + } + } + + // 根据参数设置自定义 Cancel 函数 + if in.useProcessGroupKill { + // 自定义 Cancel 函数,确保杀死进程组 + cmd.Cancel = func() error { + if cmd.Process == nil { + return nil + } + // 使用负 PID 杀死整个进程组 + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + } + + in.cmd = cmd + + // 脚本执行生成调试脚本(避免被脚本内部的 git clone 等操作清空) + if in.isDebugScriptEnabled() { + if debugScriptPath, writeErr := in.WriteDebugScript(fullScriptPath, in.Args); writeErr != nil { + e.log.Warn().Str("task_id", in.metadata.ID).Err(writeErr).Msg("生成调试脚本失败") + } else if debugScriptPath != "" { + e.log.Info().Str("task_id", in.metadata.ID).Str("debug_script", debugScriptPath).Msg("已生成调试脚本,可进入工作目录执行 ./debug.sh 进行调试") + } + } + + return nil, nil +} + +func (e *ScriptExcutor) InjectEnv(in *ExecuteScriptRequest) { + // 注入标准变量 + in.SetEnv("SCRIPT_DIR", e.ScriptDirPrefix) + in.SetEnv("WORKSPACE", in.workDir) + + // 创建 output.env 文件(用于脚本输出参数) + outputEnvFile := filepath.Join(in.workDir, "output.env") + // 创建空文件(如果已存在则清空) + if f, err := os.Create(outputEnvFile); err == nil { + f.Close() + // 注入环境变量,让脚本可以直接使用 + in.SetEnv("OUTPUT_ENV_FILE", outputEnvFile) + } +} + +// ensureWorkDir 确保工作目录存在 +func (e *ScriptExcutor) ensureWorkDir(workspace string) error { + if workspace == "" { + return fmt.Errorf("work directory cannot be empty") + } + + if _, err := os.Stat(workspace); os.IsNotExist(err) { + if err := os.MkdirAll(workspace, 0755); err != nil { + return err + } + } + return nil +} + +func (r *ScriptExcutor) WithPrefixScripPath(scriptName string) string { + return r.ScriptDirPrefix + "/" + scriptName +} + +// sanitizeScriptPath 脚本路径消毒, 确保脚本路径在指定的目录下,并且文件存在且可执行 +func (s *ScriptExcutor) sanitizeScriptPath(scriptPath string) (string, error) { + if strings.TrimSpace(scriptPath) == "" { + return "", fmt.Errorf("script path cannot be empty") + } + + // 确保脚本路径在工作目录内 + scriptFullPath := scriptPath + if !filepath.IsAbs(scriptPath) { + // 直接获取绝对路径 + fullPath, err := filepath.Abs(scriptPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for script path %s: %v", scriptFullPath, err) + } + scriptFullPath = fullPath + } + + // 检查文件是否存在 + if _, err := os.Stat(scriptFullPath); os.IsNotExist(err) { + return "", fmt.Errorf("script file does not exist: %s", scriptFullPath) + } + + // 检查文件是否可执行 + info, err := os.Stat(scriptFullPath) + if err != nil { + return "", fmt.Errorf("failed to get script file info: %v", err) + } + + // 如果是脚本文件,确保它有执行权限 + if info.Mode().Perm()&0111 == 0 { + // 尝试添加执行权限 + if err := os.Chmod(scriptFullPath, info.Mode()|0111); err != nil { + return "", fmt.Errorf("script file is not executable and cannot be made executable: %v", err) + } + } + + return scriptFullPath, nil +} diff --git a/devops/agent/script/script_integrity.go b/devops/agent/script/script_integrity.go new file mode 100644 index 0000000..24ea217 --- /dev/null +++ b/devops/agent/script/script_integrity.go @@ -0,0 +1,169 @@ +package script + +import ( + "crypto/sha256" + "embed" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "maps" + "os" + "path/filepath" + "sync" + + "github.com/infraboard/mcube/v2/ioc/config/log" + "github.com/rs/zerolog" +) + +// 编译时嵌入的脚本 hash 文件 +// +//go:embed hashes.json +var scriptHashesFS embed.FS + +// ScriptIntegrityManager 脚本完整性管理器 +// 脚本 hash 在编译时计算并硬编码到二进制中 +// 在执行脚本前校验 hash,防止脚本被篡改 +// 这确保了生产环境的脚本不会被篡改,即使脚本目录变为可写 +type ScriptIntegrityManager struct { + log *zerolog.Logger + mu sync.RWMutex + + // 注册的脚本 hash (相对路径 -> hash),从编译时常量加载 + registeredHashes map[string]string + + // 脚本根目录 + scriptDir string + + // 是否启用校验(编译时参数) + enabled bool +} + +// NewScriptIntegrityManager 创建脚本完整性管理器 +// 从编译时嵌入的 hash 文件加载脚本 hash +func NewScriptIntegrityManager(scriptDir string, enabled bool) *ScriptIntegrityManager { + m := &ScriptIntegrityManager{ + log: log.Sub("script_integrity"), + registeredHashes: make(map[string]string), + scriptDir: scriptDir, + enabled: enabled, + } + + // 如果启用了校验,加载编译时的 hash + if enabled { + if err := m.loadCompiledHashes(); err != nil { + m.log.Error().Err(err).Msg("加载编译时的脚本 hash 失败") + } + } + + return m +} + +// loadCompiledHashes 从编译时嵌入的 hash 文件加载脚本 hash +func (m *ScriptIntegrityManager) loadCompiledHashes() error { + data, err := scriptHashesFS.ReadFile("hashes.json") + if err != nil { + return fmt.Errorf("读取嵌入的 hash 文件失败: %v", err) + } + + var hashes map[string]string + if err := json.Unmarshal(data, &hashes); err != nil { + return fmt.Errorf("解析 hash 文件失败: %v", err) + } + + m.mu.Lock() + m.registeredHashes = hashes + m.mu.Unlock() + + m.log.Info().Int("count", len(hashes)).Msg("加载编译时的脚本 hash 完成") + return nil +} + +// Enable 启用校验 +func (m *ScriptIntegrityManager) Enable() { + m.mu.Lock() + defer m.mu.Unlock() + m.enabled = true +} + +// Disable 禁用校验 +func (m *ScriptIntegrityManager) Disable() { + m.mu.Lock() + defer m.mu.Unlock() + m.enabled = false +} + +// IsEnabled 是否启用校验 +func (m *ScriptIntegrityManager) IsEnabled() bool { + m.mu.RLock() + defer m.mu.RUnlock() + return m.enabled +} + +// VerifyScript 校验脚本完整性 +// 在执行脚本前调用,验证脚本是否被篡改 +func (m *ScriptIntegrityManager) VerifyScript(scriptPath string) error { + if !m.enabled { + return nil + } + + // 计算相对路径 + relPath, err := filepath.Rel(m.scriptDir, scriptPath) + if err != nil { + return fmt.Errorf("计算相对路径失败: %v", err) + } + + m.mu.RLock() + expectedHash, exists := m.registeredHashes[relPath] + m.mu.RUnlock() + + if !exists { + return fmt.Errorf("脚本未注册: %s (可能是新增的脚本,请重启 Agent)", relPath) + } + + // 计算当前 hash + currentHash, err := m.calculateFileHash(scriptPath) + if err != nil { + return fmt.Errorf("计算脚本 hash 失败: %v", err) + } + + // 对比 hash + if currentHash != expectedHash { + m.log.Error(). + Str("script", relPath). + Str("expected_hash", expectedHash). + Str("current_hash", currentHash). + Msg("脚本完整性校验失败:脚本可能被篡改") + return fmt.Errorf("脚本完整性校验失败: %s (hash 不匹配)", relPath) + } + + m.log.Debug().Str("script", relPath).Str("hash", currentHash).Msg("脚本完整性校验通过") + return nil +} + +// calculateFileHash 计算文件的 SHA256 hash +func (m *ScriptIntegrityManager) calculateFileHash(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} + +// GetRegisteredScripts 获取已注册的脚本列表 +func (m *ScriptIntegrityManager) GetRegisteredScripts() map[string]string { + m.mu.RLock() + defer m.mu.RUnlock() + + // 返回副本 + result := make(map[string]string, len(m.registeredHashes)) + maps.Copy(result, m.registeredHashes) + return result +} diff --git a/devops/agent/script/tools.go b/devops/agent/script/tools.go new file mode 100644 index 0000000..31dff53 --- /dev/null +++ b/devops/agent/script/tools.go @@ -0,0 +1,22 @@ +package script + +import ( + "fmt" + "os" + "time" +) + +// 工具函数 +func generateID() string { + return fmt.Sprintf("cmd_%d", time.Now().UnixNano()) +} + +func getCurrentUser() string { + if user := os.Getenv("USER"); user != "" { + return user + } + if user := os.Getenv("USERNAME"); user != "" { + return user + } + return "unknown" +} diff --git a/devops/agent/shells/README.md b/devops/agent/shells/README.md new file mode 100644 index 0000000..d3115a6 --- /dev/null +++ b/devops/agent/shells/README.md @@ -0,0 +1,10 @@ +# 脚本存放目录 + + + +## 系统变量 + +脚本执行过程中,会自动设置一些系统变量 + ++ WORKSPACE:工作目录 ++ SCRIPT_DIR:脚本存放目录 \ No newline at end of file diff --git a/devops/agent/shells/lib.sh b/devops/agent/shells/lib.sh new file mode 100644 index 0000000..5d85e02 --- /dev/null +++ b/devops/agent/shells/lib.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# 公共库脚本 +# 包含一些常用的函数和变量定义,供其他脚本调用 + +# 颜色定义 +COLOR_RED='\033[0;31m' +COLOR_GREEN='\033[0;32m' +COLOR_YELLOW='\033[0;33m' +COLOR_CYAN_BOLD='\033[1;36m' +COLOR_RESET='\033[0m' + +# 日志函数 +log_info() { + echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1" +} + +log_success() { + echo -e "${COLOR_GREEN}[SUCCESS] $(date '+%Y-%m-%d %H:%M:%S') - $1${COLOR_RESET}" +} + +log_warning() { + echo -e "${COLOR_YELLOW}[WARNING] $(date '+%Y-%m-%d %H:%M:%S') - $1${COLOR_RESET}" +} + +log_error() { + echo -e "${COLOR_RED}[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1${COLOR_RESET}" +} + +# 强调/高亮日志(用于强调性提示) +log_highlight() { + echo -e "${COLOR_CYAN_BOLD}[HINT] $(date '+%Y-%m-%d %H:%M:%S') - $1${COLOR_RESET}" +} diff --git a/devops/agent/shells/task_debug.sh b/devops/agent/shells/task_debug.sh new file mode 100644 index 0000000..67729e0 --- /dev/null +++ b/devops/agent/shells/task_debug.sh @@ -0,0 +1,208 @@ +#!/bin/bash + +# Task Debug Script +# 用于打印任务的详细信息,包括所有环境变量 +# 主要用于调试和排查问题 + +set -e + +# 引入公共库 +source "${SCRIPT_DIR:-.}/lib.sh" + +# 打印分隔线 +print_separator() { + log_info "========================================" +} + +# 打印标题 +print_title() { + print_separator + echo " $1" + print_separator +} + +# 打印键值对 +print_kv() { + printf "%-30s : %s\n" "$1" "$2" +} + +# 主函数 +main() { + log_highlight "开始任务调试信息输出" + + # 1. 打印任务基本信息 + print_title "任务基本信息" + print_kv "任务ID" "${TASK_ID:-未设置}" + print_kv "任务名称" "${TASK_NAME:-未设置}" + print_kv "任务类型" "${TASK_TYPE:-未设置}" + print_kv "任务状态" "${TASK_STATUS:-未设置}" + print_kv "任务描述" "${TASK_DESCRIPTION:-未设置}" + print_kv "执行者" "${TASK_RUN_BY:-未设置}" + print_kv "Agent 环境" "${TASK_AGENT_ENV:-未设置}" + print_kv "调度的 Agent" "${TASK_SCHEDULED_AGENT:-未设置}" + echo "" + + # 2. 打印标准环境变量 + print_title "标准环境变量" + print_kv "工作目录" "${WORKSPACE:-未设置}" + print_kv "脚本目录" "${SCRIPT_DIR:-未设置}" + print_kv "用户" "${USER:-未设置}" + print_kv "主机名" "${HOSTNAME:-未设置}" + print_kv "PWD" "${PWD:-未设置}" + print_kv "HOME" "${HOME:-未设置}" + print_kv "SHELL" "${SHELL:-未设置}" + echo "" + + # 3. 打印任务参数(PARAM_ 开头) + print_title "任务参数 (PARAM_*)" + local param_found=false + while IFS='=' read -r name value; do + if [[ $name == PARAM_* ]]; then + param_found=true + # 移除 PARAM_ 前缀显示 + local param_name="${name#PARAM_}" + print_kv "$param_name" "$value" + fi + done < <(env | sort) + + if [ "$param_found" = false ]; then + echo " (无任务参数)" + fi + echo "" + + # 4. 打印任务定义(DEFINE_ 开头) + print_title "任务定义 (DEFINE_*)" + local define_found=false + while IFS='=' read -r name value; do + if [[ $name == DEFINE_* ]]; then + define_found=true + # 移除 DEFINE_ 前缀显示 + local define_name="${name#DEFINE_}" + print_kv "$define_name" "$value" + fi + done < <(env | sort) + + if [ "$define_found" = false ]; then + echo " (无任务定义)" + fi + echo "" + + # 5. 打印所有环境变量(按字母排序) + print_title "所有环境变量" + env | sort | while IFS='=' read -r name value; do + # 截断过长的值 + if [ ${#value} -gt 100 ]; then + value="${value:0:100}... (truncated)" + fi + print_kv "$name" "$value" + done + echo "" + + # 6. 打印系统信息 + print_title "系统信息" + print_kv "操作系统" "$(uname -s)" + print_kv "内核版本" "$(uname -r)" + print_kv "架构" "$(uname -m)" + + if command -v lsb_release &> /dev/null; then + print_kv "发行版" "$(lsb_release -d | cut -f2-)" + elif [ -f /etc/os-release ]; then + print_kv "发行版" "$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)" + fi + + # 打印资源使用情况 + if command -v free &> /dev/null; then + local mem_info=$(free -h | grep Mem | awk '{print $3 "/" $2 " (used/total)"}') + print_kv "内存使用" "$mem_info" + fi + + if command -v df &> /dev/null; then + local disk_info=$(df -h . | tail -1 | awk '{print $3 "/" $2 " (" $5 " used)"}') + print_kv "磁盘使用" "$disk_info" + fi + + print_kv "CPU 核心数" "$(nproc 2>/dev/null || echo 'unknown')" + print_kv "当前时间" "$(date '+%Y-%m-%d %H:%M:%S %Z')" + echo "" + + # 7. 打印工作目录内容 + print_title "工作目录内容" + if [ -d "${WORKSPACE}" ]; then + print_kv "工作目录路径" "${WORKSPACE}" + echo "" + echo "文件列表:" + ls -lh "${WORKSPACE}" 2>/dev/null | tail -n +2 | while read -r line; do + echo " $line" + done + else + echo " 工作目录不存在: ${WORKSPACE:-未设置}" + fi + echo "" + + # 8. 打印网络信息 + print_title "网络信息" + if command -v ip &> /dev/null; then + echo "IP 地址:" + ip addr show | grep -E "inet |inet6 " | awk '{print " " $0}' + elif command -v ifconfig &> /dev/null; then + echo "IP 地址:" + ifconfig | grep -E "inet |inet6 " | awk '{print " " $0}' + fi + echo "" + + # 9. 打印 Docker 信息(如果可用) + if command -v docker &> /dev/null; then + print_title "Docker 信息" + print_kv "Docker 版本" "$(docker --version 2>/dev/null | cut -d' ' -f3 | tr -d ',')" + + # 检查 Docker 是否运行 + if docker info &> /dev/null; then + local running_containers=$(docker ps -q | wc -l) + local all_containers=$(docker ps -aq | wc -l) + print_kv "运行中的容器" "$running_containers" + print_kv "总容器数" "$all_containers" + + local images_count=$(docker images -q | wc -l) + print_kv "镜像数量" "$images_count" + else + echo " Docker daemon 未运行" + fi + echo "" + fi + + # 10. 打印进程信息 + print_title "进程信息" + print_kv "当前进程 PID" "$$" + print_kv "父进程 PID" "$PPID" + + if command -v ps &> /dev/null; then + echo "" + echo "当前进程树:" + ps auxf 2>/dev/null | grep -E "(PID|$$)" | head -5 | while read -r line; do + echo " $line" + done + fi + echo "" + + # 11. 环境变量统计 + print_title "环境变量统计" + local total_env=$(env | wc -l) + local param_count=$(env | grep -c "^PARAM_" || echo 0) + local define_count=$(env | grep -c "^DEFINE_" || echo 0) + local task_count=$(env | grep -c "^TASK_" || echo 0) + + print_kv "总环境变量数" "$total_env" + print_kv "任务参数数 (PARAM_*)" "$param_count" + print_kv "任务定义数 (DEFINE_*)" "$define_count" + print_kv "任务信息数 (TASK_*)" "$task_count" + echo "" + + print_separator + log_success "任务调试信息输出完成" + + return 0 +} + +# 执行主函数 +main "$@" +exit $? \ No newline at end of file diff --git a/devops/agent/tasks/README.md b/devops/agent/tasks/README.md new file mode 100644 index 0000000..f78bc34 --- /dev/null +++ b/devops/agent/tasks/README.md @@ -0,0 +1,4 @@ +# 任务执行模块 + +负责任务的具体执行 + diff --git a/devops/agent/tasks/interface.go b/devops/agent/tasks/interface.go new file mode 100644 index 0000000..8e60848 --- /dev/null +++ b/devops/agent/tasks/interface.go @@ -0,0 +1,13 @@ +package tasks + +import ( + "context" + "devops/server/apps/task" +) + +// Task 是一个接口,定义了任务的基本行为 +// 任务名称: task_debug, 任务描述: 调试任务, 任务类型: debug, 任务参数: {} +type TaskRunner interface { + // 任务需要的运行能力 + Run(context.Context, *task.TaskSpec) (*task.Task, error) +} diff --git a/devops/agent/tasks/task_debug/README.md b/devops/agent/tasks/task_debug/README.md new file mode 100644 index 0000000..b62881e --- /dev/null +++ b/devops/agent/tasks/task_debug/README.md @@ -0,0 +1,5 @@ +# Task Debug 任务 + + + + diff --git a/devops/agent/tasks/task_debug/impl.go b/devops/agent/tasks/task_debug/impl.go new file mode 100644 index 0000000..eae2419 --- /dev/null +++ b/devops/agent/tasks/task_debug/impl.go @@ -0,0 +1,15 @@ +package taskdebug + +import ( + "context" + "devops/server/apps/task" +) + +// 实现一个 task_debug 任务 + +type TaskDebugRunner struct{} + +func (t *TaskDebugRunner) Run(ctx context.Context, spec *task.TaskSpec) (*task.Task, error) { + // 使用脚本执行 + return nil, nil +} diff --git a/devops/docs/agent.drawio b/devops/docs/agent.drawio new file mode 100644 index 0000000..677284a --- /dev/null +++ b/devops/docs/agent.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devops/docs/agent.png b/devops/docs/agent.png new file mode 100644 index 0000000000000000000000000000000000000000..90a6443263312d3acb8dc6da71c00e3c8049fb87 GIT binary patch literal 38722 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 C$!UfF literal 0 HcmV?d00001 diff --git a/devops/go.mod b/devops/go.mod new file mode 100644 index 0000000..b80ccd7 --- /dev/null +++ b/devops/go.mod @@ -0,0 +1,25 @@ +module devops + +go 1.25.6 + +require ( + github.com/infraboard/mcube v1.9.29 + github.com/infraboard/mcube/v2 v2.1.3 + github.com/rs/zerolog v1.34.0 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/caarlos0/env/v6 v6.10.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/sys v0.35.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/devops/go.sum b/devops/go.sum new file mode 100644 index 0000000..2fd6380 --- /dev/null +++ b/devops/go.sum @@ -0,0 +1,52 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= +github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/infraboard/mcube v1.9.29 h1:sta2Ca+H83sXaQFaTKAX2uVwsXKhAbFbbUr5m1El3UE= +github.com/infraboard/mcube v1.9.29/go.mod h1:5VqpDng1zHVoLF9WXYelO/jV0WkxSURooVSHzMznx0U= +github.com/infraboard/mcube/v2 v2.1.3 h1:2UCceLoMkcjxp7btEZQgajyBW/Tzf7meB4OwEA8Hzs4= +github.com/infraboard/mcube/v2 v2.1.3/go.mod h1:M/UxG9LsdiBVdMKnoCnDOzr3CR7PNBXsygTbB5U6Ibg= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/devops/server/apps/README.md b/devops/server/apps/README.md new file mode 100644 index 0000000..2605aa1 --- /dev/null +++ b/devops/server/apps/README.md @@ -0,0 +1,2 @@ +# 业务模块存放模块 + diff --git a/devops/server/apps/task/enum.go b/devops/server/apps/task/enum.go new file mode 100644 index 0000000..3461fd2 --- /dev/null +++ b/devops/server/apps/task/enum.go @@ -0,0 +1,27 @@ +package task + +type STATUS string + +func (s STATUS) IsComplete() bool { + return s == STATUS_SUCCESS || s == STATUS_FAILED || s == STATUS_SKIP || s == STATUS_CANCELED +} + +func (s STATUS) String() string { + return string(s) +} + +const ( + STATUS_PENDDING STATUS = "等待处理" + STATUS_RUNNING STATUS = "运行中" + // 忽略执行, 等同为成功 + STATUS_SKIP STATUS = "忽略执行" + STATUS_SUCCESS STATUS = "成功" + STATUS_CANCELED STATUS = "取消" + STATUS_FAILED STATUS = "失败" +) + +const ( + CONDITION_OPERATOR_IN CONDITION_OPERATOR = "in" +) + +type CONDITION_OPERATOR string diff --git a/devops/server/apps/task/model.go b/devops/server/apps/task/model.go new file mode 100644 index 0000000..b31e241 --- /dev/null +++ b/devops/server/apps/task/model.go @@ -0,0 +1,240 @@ +package task + +import ( + "fmt" + "time" + + "github.com/infraboard/mcube/tools/pretty" +) + +type Task struct { + // 任务定义 + *TaskSpec + // 任务状态 + *TaskStatus +} + +func (e *Task) TableName() string { + return "devops_tasks" +} + +type TaskSpec struct { + // 任务Id(唯一标识,由调用方生成, 比如 uuid, 如果没有自动生成唯一Id) + Id string `json:"id" gorm:"column:id;type:string;primary_key"` + // 描述, 比如 "构建任务", "部署任务" + Description string `json:"description" gorm:"column:description;type:text"` + // 任务名称, 比如 "build", "deploy" 每一个名称 在Agent测有一个唯一的Task与之对应 + Name string `json:"name" gorm:"column:name;type:varchar(255)"` + // 创建时间 + CreateAt time.Time `json:"create_at" gorm:"column:create_at;type:datetime"` + // 任务定义, 比如名称, job定义 + Define string `json:"define" gorm:"column:define;type:text"` + // 运行参数, 比如构建任务的代码分支、部署任务的目标环境等 作为环境变量传递给任务脚本执行 + InputParams map[string]string `json:"input_params" gorm:"column:input_params;type:json;serializer:json;not null;default:'{}'"` + // 流水线任务Id(忽略) + PipelineTaskId string `json:"pipeline_task_id" gorm:"column:pipeline_task_id;type:varchar(100);index"` + + // 任务超时时间, 0表示不超时 + TimeoutSecond int64 `json:"timeout_second" gorm:"column:timeout_second;type:bigint"` + // 是否忽略错误 + IgnoreError *bool `json:"ignore_error" gorm:"column:ignore_error;type:bool;default:false"` + // 需要调度那个Agent执行, 为空表示不指定, 由调度系统根据任务类型和Agent能力自动选择一个Agent执行 + ScheduleAgent string `json:"schedule_agent" gorm:"column:schedule_agent;type:varchar(255);index"` + // 依赖的任务节点列表 + DependsTasks []string `json:"depends_tasks" gorm:"column:depends_tasks;type:json;serializer:json;not null;default:'[]'"` + // 执行条件(旧版,保留向后兼容) + When []Contiditon `json:"when" gorm:"column:when;type:json;serializer:json;"` + // when 条件表达式(新版 DAG 条件系统) + // 支持表达式如: "always", "never", "params.env == 'prod'", "deps.build.status == 'success'" + WhenCondition string `json:"when_condition" gorm:"column:when_condition;type:varchar(1000)"` + // 额外的其他属性 + Extras map[string]string `json:"extras" form:"extras" gorm:"column:extras;type:json;serializer:json;"` + // 标签 + Label map[string]string `json:"label" gorm:"column:label;type:json;serializer:json;"` +} + +// inputa == "a" +type Contiditon struct { + // 输入参数 + InputParam string `json:"input_param"` + // 操作符 + Operator CONDITION_OPERATOR `json:"operator"` + // In的值列吧 + Values []string `json:"values"` +} + +type TaskStatus struct { + // 状态 + Status STATUS `json:"status" gorm:"column:status;type:varchar(100);index"` + // 关联URL (比如日志URL, 结果URL等) + RefURL string `json:"ref_url" gorm:"column:ref_url;type:varchar(255)"` + // 失败原因 + Message string `json:"message" gorm:"column:message;type:text"` + // 异步任务调用时的返回详情 + Detail string `json:"detail,omitempty" gorm:"column:detail;type:text"` + // 启动人 + RunBy string `json:"run_by" gorm:"column:run_by;type:varchar(100)"` + // 开始时间 + StartAt *time.Time `json:"start_at" gorm:"column:start_at;type:datetime"` + // 更新时间 + UpdateAt time.Time `json:"update_at" gorm:"column:update_at;type:datetime"` + // 结束时间 + EndAt time.Time `json:"end_at" gorm:"column:end_at;type:datetime"` + // 其他信息 + Extras map[string]string `json:"extras" gorm:"column:extras;type:json;serializer:json;not null;default:'{}'"` + + // 调度时间 + ScheduledAt *time.Time `json:"scheduled_at" gorm:"column:scheduled_at;type:datetime"` + // 具体执行任务的AgentId + ScheduledAgentId *string `json:"scheduled_agent" gorm:"column:scheduled_agent;type:varchar(255);index"` + // 确认调度超时, 默认15秒 + ScheduledConfirmTTL int64 `json:"scheduled_confirm_ttl" gorm:"column:scheduled_confirm_ttl;type:bigint;default:15"` + // 调度确认, Agent确认接收任务, 下发成功后设置为true + ScheduledConfirmed *bool `json:"scheduled_confirmed" gorm:"column:scheduled_confirmed;type:boolean;default:false"` + + // 任务输出参数(供下一个任务使用) + Output map[string]string `json:"output,omitempty" gorm:"column:output;type:json;serializer:json;not null;default:'{}'"` +} + +// // 命令 +// Command string `json:"command"` +// // 错误原因 +// Error string `json:"error,omitempty"` +// // 命令退出码 +// ExitCode int `json:"exit_code"` +// // 命令开始执行时间 +// StartTime time.Time `json:"start_time"` +// // 命令结束执行时间 +// EndTime *time.Time `json:"end_time"` +// // 命令执行时长 +// Duration time.Duration `json:"duration"` +// // 命令执行是否成功 +// Success bool `json:"success"` +// // 是否跳过执行(跳过视为成功,但标记为 Skip 以便管道状态同步) +// Skipped bool `json:"skipped,omitempty"` +// // 非错误的说明信息(比如跳过原因等) +// Message string `json:"message,omitempty"` +// // 元数据 +// Metadata *CommandMetadata `json:"metadata"` +// // 文件内容集合 +// FileContents map[string]string `json:"file_contents,omitempty"` +// // 脚本输出参数(供下一个任务使用) +// OutputParams map[string]string `json:"output_params,omitempty"` + +func (r *TaskStatus) String() string { + return pretty.ToJSON(r) +} + +func (r *TaskStatus) SetScheduledConfirmed(confirmed bool) *TaskStatus { + r.ScheduledConfirmed = &confirmed + return r +} + +func (r *TaskStatus) SetScheduledAgentId(agentId string) *TaskStatus { + r.ScheduledAgentId = &agentId + return r +} + +func (r *TaskStatus) GetScheduledAgentId() string { + if r.ScheduledAgentId == nil { + return "" + } + return *r.ScheduledAgentId +} + +func (r *TaskStatus) TableName() string { + return "devops_tasks" +} + +func (r *TaskStatus) MarkedRunning() { + r.setStartAt(time.Now()) + r.Status = STATUS_RUNNING +} + +func (r *TaskStatus) IsRunning() bool { + return r.Status == STATUS_RUNNING +} + +// MarkScheduled 标记任务已被分配给指定的Agent +// 用于Agent模式下的任务分发 +// 同时设置确认超时时间窗口(默认15秒) +func (r *TaskStatus) MarkScheduled(agentId string) *TaskStatus { + now := time.Now() + r.ScheduledAt = &now + r.SetScheduledAgentId(agentId) + r.SetScheduledConfirmed(false) + // 确保设置确认超时时间,如果未设置则默认15秒 + if r.ScheduledConfirmTTL <= 0 { + r.ScheduledConfirmTTL = 15 + } + return r +} + +// IsScheduled 检查任务是否已被分配给Agent +func (r *TaskStatus) IsScheduled() bool { + return r.ScheduledAgentId != nil && *r.ScheduledAgentId != "" +} + +// IsScheduleConfirm 检查任务是否具有完整的调度信息(调度时间和超时时间都已设置) +// 用于判断是否需要进行调度超时检查 +func (r *TaskStatus) IsScheduleConfirm() bool { + return r.ScheduledAt != nil && r.ScheduledConfirmTTL > 0 +} + +// ConfirmScheduled 确认Agent已接收任务分配 +func (r *TaskStatus) ConfirmScheduled() *TaskStatus { + r.SetScheduledConfirmed(true) + return r +} + +// IsScheduleConfirmed 检查任务分配是否已被Agent确认 +func (r *TaskStatus) IsScheduleConfirmed() bool { + return r.ScheduledConfirmed != nil && *r.ScheduledConfirmed +} + +func (r *TaskStatus) setStartAt(t time.Time) { + r.StartAt = &t +} + +func (t *TaskStatus) WithExtra(key, value string) *TaskStatus { + t.Extras[key] = value + return t +} + +func (t *TaskStatus) WithRefURL(refURL string) *TaskStatus { + t.RefURL = refURL + return t +} + +func (t *TaskStatus) Failedf(format string, a ...any) *TaskStatus { + t.EndAt = time.Now() + t.Status = STATUS_FAILED + t.Message = fmt.Sprintf(format, a...) + return t +} + +func (t *TaskStatus) Canceledf(format string, a ...any) *TaskStatus { + t.EndAt = time.Now() + t.Status = STATUS_CANCELED + t.Message = fmt.Sprintf(format, a...) + return t +} + +func (t *TaskStatus) Success(format string, a ...any) *TaskStatus { + t.EndAt = time.Now() + t.Status = STATUS_SUCCESS + t.Message = fmt.Sprintf(format, a...) + return t +} + +func (t *TaskStatus) Skipf(format string, a ...any) *TaskStatus { + t.EndAt = time.Now() + t.Status = STATUS_SKIP + t.Message = fmt.Sprintf(format, a...) + return t +} + +func (t *TaskStatus) WithDetail(format string, a ...any) *TaskStatus { + t.Detail = fmt.Sprintf(format, a...) + return t +}