补充script模块
This commit is contained in:
parent
f3cb6b013d
commit
31b329ca5b
@ -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
|
||||
```
|
||||
|
||||
@ -1 +1,8 @@
|
||||
# DevOps Agent
|
||||
# DevOps Agent
|
||||
|
||||
1. WebSocket Agent (Jenkins Node), 把自己注册到 Api Server 作为一个任务运行阶段
|
||||
2. 需要执行来自于 Api Server 下发的任务, 执行中需要把日志和执行结果返回给 Api Server
|
||||
3. 怎么运行任务喃,我们是封装一个 script的模块, 然后调用这个模块来运行任务
|
||||
4. 任务需要有任务名称, 与 任务参数 这些基础信息
|
||||
|
||||
|
||||
|
||||
4
devops/agent/connect/README.md
Normal file
4
devops/agent/connect/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# 连接管理
|
||||
|
||||
管理与Api的通信
|
||||
|
||||
@ -3,10 +3,9 @@
|
||||
首先实现一个自己版本的 脚本执行器
|
||||
|
||||
核心功能:
|
||||
|
||||
+ 支持workspace, 固定工作目录
|
||||
+ 制定脚本路径,可以直接执行
|
||||
+ 脚本输入是 环境变量
|
||||
+ 制定脚本路径,可以直接执行, 脚本存放的路径是可以配置, 默认是 shells 目录
|
||||
+ 脚本输入是 环境变量(环境变量有比较好的隔离性, 脚本的编写逻辑要清晰)
|
||||
+ 输出到制定 文件(环境变量文件)
|
||||
+ 执行过程中的日志
|
||||
+ debug.sh, 生产调试脚本, 基于改脚本,可以重复执行(记录执行中的参数,方便回放)
|
||||
|
||||
78
devops/agent/script/debug_script.go
Normal file
78
devops/agent/script/debug_script.go
Normal 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
|
||||
}
|
||||
1
devops/agent/script/hashes.json
Normal file
1
devops/agent/script/hashes.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
125
devops/agent/script/interface.go
Normal file
125
devops/agent/script/interface.go
Normal 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"`
|
||||
}
|
||||
204
devops/agent/script/runner.go
Normal file
204
devops/agent/script/runner.go
Normal 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
|
||||
}
|
||||
169
devops/agent/script/script_integrity.go
Normal file
169
devops/agent/script/script_integrity.go
Normal 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
|
||||
}
|
||||
22
devops/agent/script/tools.go
Normal file
22
devops/agent/script/tools.go
Normal 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"
|
||||
}
|
||||
10
devops/agent/shells/README.md
Normal file
10
devops/agent/shells/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# 脚本存放目录
|
||||
|
||||
|
||||
|
||||
## 系统变量
|
||||
|
||||
脚本执行过程中,会自动设置一些系统变量
|
||||
|
||||
+ WORKSPACE:工作目录
|
||||
+ SCRIPT_DIR:脚本存放目录
|
||||
33
devops/agent/shells/lib.sh
Normal file
33
devops/agent/shells/lib.sh
Normal 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}"
|
||||
}
|
||||
208
devops/agent/shells/task_debug.sh
Normal file
208
devops/agent/shells/task_debug.sh
Normal 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 $?
|
||||
4
devops/agent/tasks/README.md
Normal file
4
devops/agent/tasks/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# 任务执行模块
|
||||
|
||||
负责任务的具体执行
|
||||
|
||||
13
devops/agent/tasks/interface.go
Normal file
13
devops/agent/tasks/interface.go
Normal 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)
|
||||
}
|
||||
5
devops/agent/tasks/task_debug/README.md
Normal file
5
devops/agent/tasks/task_debug/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Task Debug 任务
|
||||
|
||||
|
||||
|
||||
|
||||
15
devops/agent/tasks/task_debug/impl.go
Normal file
15
devops/agent/tasks/task_debug/impl.go
Normal 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
40
devops/docs/agent.drawio
Normal 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<div>连接管理</div>" 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="&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<div>脚本执行</div>" 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<br><div>任务执行</div>" 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
BIN
devops/docs/agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
25
devops/go.mod
Normal file
25
devops/go.mod
Normal 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
52
devops/go.sum
Normal 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=
|
||||
2
devops/server/apps/README.md
Normal file
2
devops/server/apps/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# 业务模块存放模块
|
||||
|
||||
27
devops/server/apps/task/enum.go
Normal file
27
devops/server/apps/task/enum.go
Normal 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
|
||||
240
devops/server/apps/task/model.go
Normal file
240
devops/server/apps/task/model.go
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user