diff --git a/devops/agent/README.md b/devops/agent/README.md index 16fe8ee..7212e84 100644 --- a/devops/agent/README.md +++ b/devops/agent/README.md @@ -8,6 +8,207 @@ ## 开发脚本执行引擎 +### 执行引擎功能开发 + +```go +// 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 进行调试") + } + } + + // 执行脚本,并且把结果写到输出到文件里面 + // 开始执行命令 + + // 添加到运行中的命令列表中 + e.runningCommands[in.workDir] = in + defer delete(e.runningCommands, in.workDir) + + taskId := "unknown" + if in.metadata != nil { + taskId = in.metadata.ID + } + + // 获取日志写入器 + logWriter, err := in.getLogWriter() + if err != nil { + e.log.Error().Str("task_id", taskId).Err(err).Msg("创建日志写入器失败") + return nil, err + } + defer func() { + if closer, ok := logWriter.(io.Closer); ok { + closer.Close() + } + }() + + // 命令的日志输出到制定的地方 + in.cmd.Stdout = logWriter + in.cmd.Stderr = logWriter + + // 记录执行开始, 包括时间、工作目录、脚本路径和元数据等信息 + startTime := time.Now() + logHeader := fmt.Sprintf("\n=== Execution Started ===\nTime: %s\nWorkDir: %s\nScript: %s\nMetadata: %s\n", + startTime.Format("2006-01-02 15:04:05"), + in.workDir, + fullScriptPath, + in.metadata.String(), + ) + fmt.Fprint(logWriter, logHeader) + fmt.Fprint(logWriter, "\n") + + // 执行命令 + e.log.Info().Str("task_id", taskId).Msg("开始执行命令") + err = in.cmd.Run() + endTime := time.Now() + duration := endTime.Sub(startTime) + + // 构建执行结果 + result := &ExecutionResult{ + Command: fullScriptPath, + StartTime: startTime, + EndTime: &endTime, + Duration: duration, + Success: err == nil, + Metadata: in.metadata, + } + + if err != nil { + // 判断是否为超时错误 + if in.cmd.ProcessState != nil && in.cmd.ProcessState.ExitCode() == -1 { + e.log.Error().Str("task_id", taskId).Dur("duration", duration).Dur("timeout", in.timeout).Err(err).Msg("命令执行超时") + } else { + e.log.Error().Str("task_id", taskId).Dur("duration", duration).Err(err).Msg("命令执行失败") + } + + // 错误信息 + result.Error = err.Error() + if in.cmd.ProcessState != nil { + result.ExitCode = in.cmd.ProcessState.ExitCode() + } else { + result.ExitCode = -1 + } + } else { + e.log.Info().Str("task_id", taskId).Dur("duration", duration).Msg("命令执行成功") + } + + // 解析脚本输出参数(无论成功或失败都收集) + outputFile := filepath.Join(in.workDir, "output.env") + if outputParams, parseErr := ParseOutputParams(outputFile); parseErr == nil && len(outputParams) > 0 { + result.OutputParams = outputParams + e.log.Info().Str("task_id", taskId).Bool("success", result.Success).Int("param_count", len(outputParams)).Msg("成功解析脚本输出参数") + } else if parseErr != nil && !os.IsNotExist(parseErr) { + e.log.Warn().Str("task_id", taskId).Str("output_file", outputFile).Err(parseErr).Msg("解析输出参数文件失败") + } + + // 保存执行结果(无论成功或失败都保存结果,方便后续查询和调试) + e.log.Debug().Str("task_id", taskId).Str("result_file", in.GetResultFilePath()).Msg("准备保存执行结果") + if saveErr := in.saveResult(result); saveErr != nil { + e.log.Error().Str("task_id", taskId).Err(saveErr).Msg("保存执行结果失败") + } + + // 记录执行结束 + logFooter := fmt.Sprintf("\n=== Execution Finished ===\nTime: %s\nDuration: %v\nSuccess: %t\nExitCode: %d\nError: %v\n", + endTime.Format("2006-01-02 15:04:05"), + duration, + result.Success, + result.ExitCode, + err, + ) + fmt.Fprint(logWriter, logFooter) + return result, err +} +``` + +### 执行引擎调试 + 1. 构造单元测试的环境(ioc), 被测试的对象在ioc里面, 名字叫: script_excutor, 运行单元测试的时候,需要ioc容器启动,并且完成初始化 ```go package test @@ -27,8 +228,26 @@ func Setup() { } ``` +2. 通过单测验证脚本运行功能 +```go +func TestScriptExcutor_ExecuteScript(t *testing.T) { + // 直接使用单元测试的上下文, 方便取消 + req := script.NewExecuteScriptRequest("task_debug.sh", []string{}) + req.SetWorkDir("task-01") + req.SetTimeout(30 * time.Second) + req.SetDebugScript(true) + req.SetLogFile("stdout.txt") + req.SetLogCallback(func(s string) { + fmt.Print(s) + }) - + resp, err := script.ExecuteScript(t.Context(), req) + if err != nil { + t.Fatalf("failed to execute script: %v", err) + } + t.Logf("script execution result: %+v", resp) +} +``` ## 注册脚本执行引擎 @@ -97,3 +316,5 @@ func main() { cmd.Start() } ``` + + diff --git a/devops/agent/script/interface.go b/devops/agent/script/interface.go index b1a5fe6..8698981 100644 --- a/devops/agent/script/interface.go +++ b/devops/agent/script/interface.go @@ -33,6 +33,9 @@ func NewExecuteScriptRequest(scriptPath string, args []string) *ExecuteScriptReq envVars: make(map[string]string), createProcessGroup: true, // 默认启用进程组管理 useProcessGroupKill: true, // 默认使用进程组杀死方式 + timeout: 60 * time.Minute, + logFile: "stdout.log", + resultFile: "result.json", } } diff --git a/devops/agent/tasks/interface.go b/devops/agent/tasks/interface.go index 8e60848..60cebd0 100644 --- a/devops/agent/tasks/interface.go +++ b/devops/agent/tasks/interface.go @@ -5,6 +5,19 @@ import ( "devops/server/apps/task" ) +var ( + // Task 运行器注册表 + taskRunnerRegistry = make(map[string]TaskRunner) +) + +func RegisterTaskRunner(name string, runner TaskRunner) { + taskRunnerRegistry[name] = runner +} + +func GetTaskRunner(name string) TaskRunner { + return taskRunnerRegistry[name] +} + // Task 是一个接口,定义了任务的基本行为 // 任务名称: task_debug, 任务描述: 调试任务, 任务类型: debug, 任务参数: {} type TaskRunner interface { diff --git a/devops/agent/tasks/task_debug/impl.go b/devops/agent/tasks/task_debug/impl.go index eae2419..35d91f1 100644 --- a/devops/agent/tasks/task_debug/impl.go +++ b/devops/agent/tasks/task_debug/impl.go @@ -2,14 +2,155 @@ package taskdebug import ( "context" + "devops/agent/script" + "devops/agent/tasks" "devops/server/apps/task" + "fmt" + "strings" + "time" ) -// 实现一个 task_debug 任务 +const ( + TASK_NAME = "task_debug" +) +func init() { + tasks.RegisterTaskRunner(TASK_NAME, &TaskDebugRunner{}) +} + +// 实现一个 task_debug 任务 type TaskDebugRunner struct{} func (t *TaskDebugRunner) Run(ctx context.Context, spec *task.TaskSpec) (*task.Task, error) { + // 直接使用单元测试的上下文, 方便取消 + req := script.NewExecuteScriptRequest("task_debug.sh", []string{}) + req.SetWorkDir(spec.GetWorkDir()) + req.SetTimeout(spec.GetTimeout()) + req.SetDebugScript(true) + req.SetLogFile("stdout.txt") + + // 添加输入参数 + for k, v := range spec.InputParams { + req.SetEnv(k, v) + } + + resp, err := script.ExecuteScript(ctx, req) + if err != nil { + return nil, err + } + + // 将resp 转化为 TaskStatus + taskIns := task.NewTask(spec) + MapExecutionResultToTask(taskIns, resp) // 使用脚本执行 - return nil, nil + return taskIns, nil +} + +// MapExecutionResultToTask updates a Task with execution results +func MapExecutionResultToTask(task *task.Task, result *script.ExecutionResult) { + if task == nil || result == nil { + return + } + + // Update TaskStatus fields from ExecutionResult + task.TaskStatus = convertExecutionResultToStatus(result) + + // Optionally update some TaskSpec fields if needed + // For example, if you want to store output params in the task spec extras + if len(result.OutputParams) > 0 && task.TaskSpec.Extras == nil { + task.TaskSpec.Extras = make(map[string]string) + } + for k, v := range result.OutputParams { + task.TaskSpec.Extras["output_"+k] = v + } +} + +// convertExecutionResultToStatus converts ExecutionResult to TaskStatus +func convertExecutionResultToStatus(result *script.ExecutionResult) *task.TaskStatus { + status := &task.TaskStatus{ + Status: mapSuccessToStatus(result.Success, result.Skipped), + Message: getMessage(result), + Detail: getDetail(result), + StartAt: &result.StartTime, + EndAt: getEndTime(result), + UpdateAt: time.Now(), + Output: result.OutputParams, + Extras: make(map[string]string), + } + + // Add file contents to extras if present + if len(result.FileContents) > 0 { + for k, v := range result.FileContents { + status.Extras["file_"+k] = v + } + } + + // Add command execution details + if result.Command != "" { + status.Extras["executed_command"] = result.Command + } + + if result.ExitCode != 0 { + status.Extras["exit_code"] = fmt.Sprintf("%d", result.ExitCode) + } + + if result.Duration > 0 { + status.Extras["duration"] = result.Duration.String() + } + + return status +} + +// mapSuccessToStatus maps execution result success/skipped to task status +func mapSuccessToStatus(success bool, skipped bool) task.STATUS { + if skipped { + return task.STATUS_SKIP + } + if success { + return task.STATUS_SUCCESS + } + return task.STATUS_FAILED +} + +// getMessage combines error and message from execution result +func getMessage(result *script.ExecutionResult) string { + if result.Error != "" { + return result.Error + } + return result.Message +} + +// getDetail creates a detailed message from execution result +func getDetail(result *script.ExecutionResult) string { + var details []string + + if result.Command != "" { + details = append(details, fmt.Sprintf("Command: %s", result.Command)) + } + + if result.ExitCode != 0 { + details = append(details, fmt.Sprintf("Exit Code: %d", result.ExitCode)) + } + + if result.Duration > 0 { + details = append(details, fmt.Sprintf("Duration: %v", result.Duration)) + } + + if result.Error != "" && result.Error != result.Message { + details = append(details, fmt.Sprintf("Error: %s", result.Error)) + } + + if result.Message != "" && result.Message != result.Error { + details = append(details, fmt.Sprintf("Message: %s", result.Message)) + } + + return strings.Join(details, "\n") +} + +// getEndTime returns the end time or current time if nil +func getEndTime(result *script.ExecutionResult) time.Time { + if result.EndTime != nil { + return *result.EndTime + } + return time.Now() } diff --git a/devops/agent/tasks/task_test.go b/devops/agent/tasks/task_test.go new file mode 100644 index 0000000..db1f12a --- /dev/null +++ b/devops/agent/tasks/task_test.go @@ -0,0 +1,27 @@ +package tasks_test + +import ( + "devops/agent/tasks" + taskdebug "devops/agent/tasks/task_debug" + "devops/agent/test" + "devops/server/apps/task" + "testing" +) + +func TestTaskDebugRunner(t *testing.T) { + taskDebugRunner := tasks.GetTaskRunner(taskdebug.TASK_NAME) + + req := task.NewTaskSpec() + req.SetInputParams("PARAM_1", "PARAM_1_VALUE") + req.SetInputParams("PARAM_2", "PARAM_2_VALUE") + + taskResp, err := taskDebugRunner.Run(t.Context(), req) + if err != nil { + t.Fatalf("failed to run task: %v", err) + } + t.Log(taskResp) +} + +func init() { + test.Setup() +} diff --git a/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/debug.sh b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/debug.sh new file mode 100755 index 0000000..fb42c65 --- /dev/null +++ b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/debug.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# ========== 调试脚本 (自动生成) ========== +# 警告: 此脚本可能包含敏感信息,请勿提交到版本控制系统 +# 生成时间: 2026-03-15 17:01:45 +# 工作目录: /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f +# 脚本路径: /Users/yumaojun/Projects/go-course/go20/devops/agent/shells/task_debug.sh +# ========================================== + +set -e + +# 设置环境变量 +export DEBUG_SCRIPT='true' +export OUTPUT_ENV_FILE='/Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/output.env' +export PARAM_1='PARAM_1_VALUE' +export PARAM_2='PARAM_2_VALUE' +export SCRIPT_DIR='/Users/yumaojun/Projects/go-course/go20/devops/agent/shells' +export WORKSPACE='/Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f' + +# 执行脚本 +exec /Users/yumaojun/Projects/go-course/go20/devops/agent/shells/task_debug.sh diff --git a/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/output.env b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/output.env new file mode 100644 index 0000000..e69de29 diff --git a/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/result.json b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/result.json new file mode 100644 index 0000000..cecb516 --- /dev/null +++ b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/result.json @@ -0,0 +1,24 @@ +{ + "command": "/Users/yumaojun/Projects/go-course/go20/devops/agent/shells/task_debug.sh", + "exit_code": 0, + "start_time": "2026-03-15T17:01:45.550879+08:00", + "end_time": "2026-03-15T17:01:45.925979+08:00", + "duration": 375098333, + "success": true, + "metadata": { + "id": "cmd_1773565305550106000", + "name": "", + "created_by": "yumaojun", + "created_at": "2026-03-15T17:01:45.550107+08:00", + "timeout": 86400000000000, + "env_vars": { + "DEBUG_SCRIPT": "true", + "OUTPUT_ENV_FILE": "/Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/output.env", + "PARAM_1": "PARAM_1_VALUE", + "PARAM_2": "PARAM_2_VALUE", + "SCRIPT_DIR": "/Users/yumaojun/Projects/go-course/go20/devops/agent/shells", + "WORKSPACE": "/Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f" + }, + "work_dir": "/Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f" + } +} \ No newline at end of file diff --git a/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/stdout.txt b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/stdout.txt new file mode 100644 index 0000000..b566893 --- /dev/null +++ b/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/stdout.txt @@ -0,0 +1,245 @@ + +=== Execution Started === +Time: 2026-03-15 17:01:45 +WorkDir: /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f +Script: /Users/yumaojun/Projects/go-course/go20/devops/agent/shells/task_debug.sh +Metadata: ID=cmd_1773565305550106000, Name=, CreatedBy=yumaojun, CreatedAt=2026-03-15 17:01:45, Timeout=24h0m0s, WorkDir=/Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f + +[HINT] 2026-03-15 17:01:45 - 开始任务调试信息输出 +[INFO] 2026-03-15 17:01:45 - ======================================== + 任务基本信息 +[INFO] 2026-03-15 17:01:45 - ======================================== +任务ID : 未设置 +任务名称 : 未设置 +任务类型 : 未设置 +任务状态 : 未设置 +任务描述 : 未设置 +执行者 : 未设置 +Agent 环境 : 未设置 +调度的 Agent : 未设置 + +[INFO] 2026-03-15 17:01:45 - ======================================== + 标准环境变量 +[INFO] 2026-03-15 17:01:45 - ======================================== +工作目录 : /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f +脚本目录 : /Users/yumaojun/Projects/go-course/go20/devops/agent/shells +用户 : yumaojun +主机名 : oldfishmpb-9.local +PWD : /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f +HOME : /Users/yumaojun +SHELL : /bin/zsh + +[INFO] 2026-03-15 17:01:45 - ======================================== + 任务参数 (PARAM_*) +[INFO] 2026-03-15 17:01:45 - ======================================== +1 : PARAM_1_VALUE +2 : PARAM_2_VALUE + +[INFO] 2026-03-15 17:01:45 - ======================================== + 任务定义 (DEFINE_*) +[INFO] 2026-03-15 17:01:45 - ======================================== + (无任务定义) + +[INFO] 2026-03-15 17:01:45 - ======================================== + 所有环境变量 +[INFO] 2026-03-15 17:01:45 - ======================================== +APPLICATIONINSIGHTS_CONFIGURATION_CONTENT : {} +APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL : 1 +APPLICATION_INSIGHTS_NO_STATSBEAT : true +BUNDLED_DEBUGPY_PATH : /Users/yumaojun/.vscode/extensions/ms-python.debugpy-2025.18.0-darwin-arm64/bundled/libs/debugpy +COLORTERM : truecolor +COMMAND_MODE : unix2003 +DEBUG_SCRIPT : true +DOTNET_CLI_UI_LANGUAGE : en-US +DOTNET_NOLOGO : true +ELECTRON_NO_ATTACH_CONSOLE : 1 +ELECTRON_RUN_AS_NODE : 1 +ENABLE_PYTHON_MIGRATION : true +GIT_ASKPASS : /Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass.sh +GOMODCACHE : /Users/yumaojun/go/pkg/mod +GOPATH : /Users/yumaojun/go +GOPROXY : https://goproxy.cn,direct +GOTELEMETRY_GOPLS_CLIENT_START_TIME : 1706263009 +GOTELEMETRY_GOPLS_CLIENT_TOKEN : 92 +HOME : /Users/yumaojun +HOMEBREW_CELLAR : /opt/homebrew/Cellar +HOMEBREW_PREFIX : /opt/homebrew +HOMEBREW_REPOSITORY : /opt/homebrew +HTTPS_PROXY : http://localhost:8001 +HTTP_PROXY : http://localhost:8001 +INFOPATH : /opt/homebrew/share/info:/opt/homebrew/share/info: +JAVA_HOME : /Users/yumaojun/.sdkman/candidates/java/current +LANG : C.UTF-8 +LESS : -R +LOGNAME : yumaojun +LSCOLORS : Gxfxcxdxbxegedabagacad +LS_COLORS : di=1;36:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43 +MACH_PORT_RENDEZVOUS_PEER_VALDATION : 0 +MAVEN_HOME : /Users/yumaojun/.sdkman/candidates/maven/current +MallocNanoZone : 0 +NODE_TLS_REJECT_UNAUTHORIZED : undefined +OSLogRateLimit : 64 +OTEL_SERVICE_NAME : devops_agent +OUTPUT_ENV_FILE : /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f/... (truncated) +PAGER : less +PARAM_1 : PARAM_1_VALUE +PARAM_2 : PARAM_2_VALUE +PATH : /usr/local/go/bin:/Users/yumaojun/bin:/usr/local/bin:/Users/yumaojun/go/bin:/Users/yumaojun/Library/... (truncated) +PWD : /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f +PYDEVD_DISABLE_FILE_VALIDATION : 1 +PYTHONSTARTUP : /Users/yumaojun/Library/Application Support/Code/User/workspaceStorage/1109ca5f32ed51b417f544c694efb... (truncated) +PYTHON_BASIC_REPL : 1 +RUSTUP_DIST_SERVER : https://rsproxy.cn +RUSTUP_UPDATE_ROOT : https://rsproxy.cn/rustup +SCRIPT_DIR : /Users/yumaojun/Projects/go-course/go20/devops/agent/shells +SDKMAN_CANDIDATES_API : https://api.sdkman.io/2 +SDKMAN_CANDIDATES_DIR : /Users/yumaojun/.sdkman/candidates +SDKMAN_DIR : /Users/yumaojun/.sdkman +SDKMAN_PLATFORM : darwinarm64 +SHELL : /bin/zsh +SHLVL : 4 +SSH_AUTH_SOCK : /private/tmp/com.apple.launchd.lh9fi1XbLE/Listeners +TERM : xterm-256color +TERM_PROGRAM : vscode +TERM_PROGRAM_VERSION : 1.111.0 +TMPDIR : /var/folders/51/dnfr1hzd53x03k3fxnnyd4qc0000gn/T/ +USER : yumaojun +USER_ZDOTDIR : /Users/yumaojun +VSCEXT_ENABLE_PYTHON_BEST_EFFORTS_INSTALLATION : false +VSCEXT_MATCH_MANIFEST_VERSIONS : true +VSCEXT_PROXY_URL : http://localhost:8001 +VSCEXT_STACK_ANALYSIS_COMMAND : rhda.stackAnalysis +VSCEXT_TELEMETRY_ID : 5d2072f3-115e-4c18-a6a6-c1ab0dbc96c6 +VSCEXT_TRACK_RECOMMENDATION_ACCEPTANCE_COMMAND : rhda.trackRecommendationAcceptance +VSCEXT_TRUSTIFY_DA_BACKEND_URL : https://rhda.rhcloud.com +VSCEXT_TRUSTIFY_DA_DOCKER_PATH : docker +VSCEXT_TRUSTIFY_DA_GO_PATH : go +VSCEXT_TRUSTIFY_DA_GRADLE_PATH : gradle +VSCEXT_TRUSTIFY_DA_IMAGE_PLATFORM : +VSCEXT_TRUSTIFY_DA_MVN_ARGS : [] +VSCEXT_TRUSTIFY_DA_MVN_PATH : mvn +VSCEXT_TRUSTIFY_DA_NPM_PATH : npm +VSCEXT_TRUSTIFY_DA_PIP3_PATH : pip3 +VSCEXT_TRUSTIFY_DA_PIP_PATH : pip +VSCEXT_TRUSTIFY_DA_PNPM_PATH : pnpm +VSCEXT_TRUSTIFY_DA_PODMAN_PATH : podman +VSCEXT_TRUSTIFY_DA_PREFER_GRADLEW : true +VSCEXT_TRUSTIFY_DA_PREFER_MVNW : true +VSCEXT_TRUSTIFY_DA_PYTHON3_PATH : python3 +VSCEXT_TRUSTIFY_DA_PYTHON_PATH : python +VSCEXT_TRUSTIFY_DA_SKOPEO_CONFIG_PATH : +VSCEXT_TRUSTIFY_DA_SKOPEO_PATH : skopeo +VSCEXT_TRUSTIFY_DA_SYFT_CONFIG_PATH : +VSCEXT_TRUSTIFY_DA_SYFT_PATH : syft +VSCEXT_TRUSTIFY_DA_YARN_PATH : yarn +VSCEXT_USE_GO_MVS : false +VSCEXT_USE_PIP_DEP_TREE : false +VSCEXT_USE_PYTHON_VIRTUAL_ENVIRONMENT : false +VSCEXT_UTM_SOURCE : vscode +VSCEXT_VULNERABILITY_ALERT_SEVERITY : Error +VSCODE_CLI : 1 +VSCODE_CODE_CACHE_PATH : /Users/yumaojun/Library/Application Support/Code/CachedData/ce099c1ed25d9eb3076c11e4a280f3eb52b4fbeb +VSCODE_CRASH_REPORTER_PROCESS_TYPE : extensionHost +VSCODE_CWD : /Users/yumaojun/Projects/go-course/go20/devops +VSCODE_DEBUGPY_ADAPTER_ENDPOINTS : /Users/yumaojun/.vscode/extensions/ms-python.debugpy-2025.18.0-darwin-arm64/.noConfigDebugAdapterEnd... (truncated) +VSCODE_DOTNET_INSTALL_TOOL_ORIGINAL_HOME : /Users/yumaojun +VSCODE_ESM_ENTRYPOINT : vs/workbench/api/node/extensionHostProcess +VSCODE_GIT_ASKPASS_MAIN : /Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass-main.js +VSCODE_GIT_ASKPASS_NODE : /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper (Plugin).app/Contents/MacOS/Cod... (truncated) +VSCODE_GIT_IPC_HANDLE : /var/folders/51/dnfr1hzd53x03k3fxnnyd4qc0000gn/T/vscode-git-da870cf1eb.sock +VSCODE_HANDLES_UNCAUGHT_ERRORS : true +VSCODE_INJECTION : 1 +VSCODE_IPC_HOOK : /Users/yumaojun/Library/Application Support/Code/1.11-main.sock +VSCODE_L10N_BUNDLE_LOCATION : file:///Users/yumaojun/.vscode/extensions/ms-ceintl.vscode-language-pack-zh-hans-1.110.2026031109/tr... (truncated) +VSCODE_NLS_CONFIG : {"userLocale":"zh-cn","osLocale":"zh-cn","resolvedLanguage":"zh-cn","defaultMessagesFile":"/Applicat... (truncated) +VSCODE_PID : 28877 +VSCODE_PROFILE_INITIALIZED : 1 +VSCODE_PYTHON_AUTOACTIVATE_GUARD : 1 +WORKSPACE : /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f +XPC_FLAGS : 0x0 +XPC_SERVICE_NAME : 0 +ZDOTDIR : /Users/yumaojun +ZSH : /Users/yumaojun/.oh-my-zsh +_ : /usr/bin/env +__CFBundleIdentifier : com.microsoft.VSCode +__CF_USER_TEXT_ENCODING : 0x1F5:0x19:0x34 +http_proxy : http://localhost:8001 +https_proxy : http://localhost:8001 +workspaceFolder : /Users/yumaojun/Projects/go-course/go20/devops + +[INFO] 2026-03-15 17:01:45 - ======================================== + 系统信息 +[INFO] 2026-03-15 17:01:45 - ======================================== +操作系统 : Darwin +内核版本 : 25.3.0 +架构 : arm64 +磁盘使用 : 526Gi/1.8Ti (29% used) +CPU 核心数 : unknown +当前时间 : 2026-03-15 17:01:45 CST + +[INFO] 2026-03-15 17:01:45 - ======================================== + 工作目录内容 +[INFO] 2026-03-15 17:01:45 - ======================================== +工作目录路径 : /Users/yumaojun/Projects/go-course/go20/devops/agent/workspace/510fc395-97c3-4460-98a9-627d17c68d0f + +文件列表: + -rwxr-xr-x 1 yumaojun staff 980B Mar 15 17:01 debug.sh + -rw-r--r-- 1 yumaojun staff 0B Mar 15 17:01 output.env + -rw-r--r-- 1 yumaojun staff 10K Mar 15 17:01 stdout.txt + +[INFO] 2026-03-15 17:01:45 - ======================================== + 网络信息 +[INFO] 2026-03-15 17:01:45 - ======================================== +IP 地址: + inet 127.0.0.1 netmask 0xff000000 + inet6 ::1 prefixlen 128 + inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 + inet6 fe80::4fe:9546:f301:c912%en0 prefixlen 64 secured scopeid 0xe + inet 192.168.10.38 netmask 0xffffff00 broadcast 192.168.10.255 + inet6 2409:8a55:2e75:9ee1:14ea:6bfb:6c38:5558 prefixlen 64 autoconf secured + inet6 2409:8a55:2e75:9ee1:f0fb:2d8e:30b8:f36e prefixlen 64 deprecated autoconf temporary + inet6 2409:8a55:2e75:9ee1::a0c prefixlen 64 dynamic + inet6 2409:8a55:2e75:9ee1:7d95:f17f:244c:7f69 prefixlen 64 deprecated autoconf temporary + inet6 2409:8a55:2e75:9ee1:9c3b:28fb:41e2:f670 prefixlen 64 autoconf temporary + inet6 fe80::e076:f5ff:fe6c:bb23%awdl0 prefixlen 64 scopeid 0x10 + inet6 fe80::e076:f5ff:fe6c:bb23%llw0 prefixlen 64 scopeid 0x11 + inet6 fe80::2aaa:254b:5496:fcd7%utun0 prefixlen 64 scopeid 0x12 + inet6 fe80::3a94:3e19:56f2:7729%utun1 prefixlen 64 scopeid 0x13 + inet6 fe80::3a73:5c32:f0b4:764c%utun2 prefixlen 64 scopeid 0x14 + inet6 fe80::ce81:b1c:bd2c:69e%utun3 prefixlen 64 scopeid 0x15 + +[INFO] 2026-03-15 17:01:45 - ======================================== + Docker 信息 +[INFO] 2026-03-15 17:01:45 - ======================================== +Docker 版本 : 29.2.1 +运行中的容器 : 2 +总容器数 : 18 +镜像数量 : 44 + +[INFO] 2026-03-15 17:01:45 - ======================================== + 进程信息 +[INFO] 2026-03-15 17:01:45 - ======================================== +当前进程 PID : 54962 +父进程 PID : 54960 + +当前进程树: + +[INFO] 2026-03-15 17:01:45 - ======================================== + 环境变量统计 +[INFO] 2026-03-15 17:01:45 - ======================================== +总环境变量数 : 123 +任务参数数 (PARAM_*) : 2 +任务定义数 (DEFINE_*) : 0 +0 +任务信息数 (TASK_*) : 0 +0 + +[INFO] 2026-03-15 17:01:45 - ======================================== +[SUCCESS] 2026-03-15 17:01:45 - 任务调试信息输出完成 + +=== Execution Finished === +Time: 2026-03-15 17:01:45 +Duration: 375.098333ms +Success: true +ExitCode: 0 +Error: diff --git a/devops/server/apps/task/model.go b/devops/server/apps/task/model.go index b31e241..2b99d45 100644 --- a/devops/server/apps/task/model.go +++ b/devops/server/apps/task/model.go @@ -4,9 +4,20 @@ import ( "fmt" "time" + "github.com/google/uuid" "github.com/infraboard/mcube/tools/pretty" ) +func NewTask(spec *TaskSpec) *Task { + return &Task{ + TaskSpec: spec, + TaskStatus: &TaskStatus{ + Status: STATUS_PENDDING, + UpdateAt: time.Now(), + }, + } +} + type Task struct { // 任务定义 *TaskSpec @@ -18,6 +29,19 @@ func (e *Task) TableName() string { return "devops_tasks" } +func (e *Task) String() string { + return fmt.Sprintf("%s", pretty.ToJSON(e)) +} + +func NewTaskSpec() *TaskSpec { + return &TaskSpec{ + Id: uuid.NewString(), + CreateAt: time.Now(), + TimeoutSecond: 60 * 60 * 24, + InputParams: map[string]string{}, + } +} + type TaskSpec struct { // 任务Id(唯一标识,由调用方生成, 比如 uuid, 如果没有自动生成唯一Id) Id string `json:"id" gorm:"column:id;type:string;primary_key"` @@ -53,6 +77,30 @@ type TaskSpec struct { Label map[string]string `json:"label" gorm:"column:label;type:json;serializer:json;"` } +func (t *TaskSpec) GetWorkDir() string { + if t.PipelineTaskId != "" { + return fmt.Sprintf("%s/%s", t.PipelineTaskId, t.Id) + } + + return t.Id +} + +func (t *TaskSpec) SetInputParams(key, value string) *TaskSpec { + if t.InputParams == nil { + t.InputParams = make(map[string]string) + } + t.InputParams[key] = value + return t +} + +func (t *TaskSpec) GetTimeout() time.Duration { + if t.TimeoutSecond > 0 { + return time.Duration(t.TimeoutSecond) * time.Second + } + + return time.Duration(60) * time.Minute +} + // inputa == "a" type Contiditon struct { // 输入参数 @@ -96,31 +144,6 @@ type TaskStatus struct { 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) }