补充script模块

This commit is contained in:
yumaojun03 2026-03-08 18:05:17 +08:00
parent f3cb6b013d
commit 31b329ca5b
24 changed files with 1293 additions and 5 deletions

View File

@ -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
```

View File

@ -1 +1,8 @@
# DevOps Agent
# DevOps Agent
1. WebSocket Agent (Jenkins Node), 把自己注册到 Api Server 作为一个任务运行阶段
2. 需要执行来自于 Api Server 下发的任务, 执行中需要把日志和执行结果返回给 Api Server
3. 怎么运行任务喃,我们是封装一个 script的模块, 然后调用这个模块来运行任务
4. 任务需要有任务名称, 与 任务参数 这些基础信息

View File

@ -0,0 +1,4 @@
# 连接管理
管理与Api的通信

View File

@ -3,10 +3,9 @@
首先实现一个自己版本的 脚本执行器
核心功能:
+ 支持workspace, 固定工作目录
+ 制定脚本路径,可以直接执行
+ 脚本输入是 环境变量
+ 制定脚本路径,可以直接执行, 脚本存放的路径是可以配置, 默认是 shells 目录
+ 脚本输入是 环境变量(环境变量有比较好的隔离性, 脚本的编写逻辑要清晰)
+ 输出到制定 文件(环境变量文件)
+ 执行过程中的日志
+ debug.sh, 生产调试脚本, 基于改脚本,可以重复执行(记录执行中的参数,方便回放)

View File

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

View File

@ -0,0 +1 @@
{}

View File

@ -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"`
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
# 脚本存放目录
## 系统变量
脚本执行过程中,会自动设置一些系统变量
+ WORKSPACE工作目录
+ SCRIPT_DIR脚本存放目录

View File

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

View File

@ -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 $?

View File

@ -0,0 +1,4 @@
# 任务执行模块
负责任务的具体执行

View File

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

View File

@ -0,0 +1,5 @@
# Task Debug 任务

View File

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

40
devops/docs/agent.drawio Normal file
View File

@ -0,0 +1,40 @@
<mxfile host="65bd71144e">
<diagram id="B-wfgpNCLNfbgqvdy5Yu" name="第 1 页">
<mxGraphModel dx="813" dy="831" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="server (api)" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="170" y="120" width="360" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="170" y="300" width="360" height="210" as="geometry"/>
</mxCell>
<mxCell id="6" value="下发任务" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="180" y="220" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="8" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="7" target="2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="12" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="7" target="11">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="7" value="connect&lt;div&gt;连接管理&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="200" y="320" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="9" value="&amp;nbsp;Agent" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="320" y="270" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="10" value="script&lt;div&gt;脚本执行&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="370" y="420" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="13" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="11" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="tasks&lt;br&gt;&lt;div&gt;任务执行&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="370" y="320" width="120" height="60" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

BIN
devops/docs/agent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

25
devops/go.mod Normal file
View File

@ -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
)

52
devops/go.sum Normal file
View File

@ -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=

View File

@ -0,0 +1,2 @@
# 业务模块存放模块

View File

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

View File

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