2026-03-15 17:57:35 +08:00
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-29 11:41:32 +08:00
|
|
|
|
"bufio"
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"devops/agent/script"
|
2026-03-15 17:57:35 +08:00
|
|
|
|
"devops/agent/tasks"
|
|
|
|
|
|
"devops/server/apps/task"
|
2026-03-29 11:41:32 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strconv"
|
2026-03-15 17:57:35 +08:00
|
|
|
|
|
|
|
|
|
|
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
|
|
|
|
|
"github.com/emicklei/go-restful/v3"
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
"github.com/infraboard/mcube/v2/exception"
|
|
|
|
|
|
"github.com/infraboard/mcube/v2/http/restful/response"
|
|
|
|
|
|
"github.com/infraboard/mcube/v2/ioc"
|
|
|
|
|
|
"github.com/infraboard/mcube/v2/ioc/config/gorestful"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
|
ioc.Api().Registry(&TaskApiHandler{})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 一个API, 提供给外部调用的接口,来触发一个Task的执行
|
|
|
|
|
|
// 参数 TaskSpec, 返回 Task
|
|
|
|
|
|
type TaskApiHandler struct {
|
|
|
|
|
|
ioc.ObjectImpl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *TaskApiHandler) Name() string {
|
|
|
|
|
|
return "tasks"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (i *TaskApiHandler) Version() string {
|
|
|
|
|
|
return "v1"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *TaskApiHandler) Init() error {
|
|
|
|
|
|
ws := gorestful.ObjectRouter(h)
|
|
|
|
|
|
|
|
|
|
|
|
// restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
|
|
|
|
|
// 这个库是 go-restful 的一个扩展库,用于生成 OpenAPI 规范的文档
|
|
|
|
|
|
tags := []string{"Task管理"}
|
|
|
|
|
|
ws.Route(ws.POST("").To(h.RunTask).
|
|
|
|
|
|
Doc("运行任务").
|
|
|
|
|
|
Metadata(restfulspec.KeyOpenAPITags, tags).
|
|
|
|
|
|
Reads(task.TaskSpec{}).
|
|
|
|
|
|
Writes(task.Task{}).
|
|
|
|
|
|
Returns(200, "OK", task.Task{}))
|
2026-03-29 11:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
ws.Route(ws.GET("/{taskId}/result").To(h.GetTaskResult).
|
|
|
|
|
|
Doc("获取任务执行结果").
|
|
|
|
|
|
Metadata(restfulspec.KeyOpenAPITags, tags).
|
|
|
|
|
|
Param(ws.PathParameter("taskId", "任务ID").DataType("string")).
|
|
|
|
|
|
Writes(script.ExecutionResult{}).
|
|
|
|
|
|
Returns(200, "OK", script.ExecutionResult{}))
|
|
|
|
|
|
|
|
|
|
|
|
ws.Route(ws.GET("/{taskId}/log").To(h.GetTaskLog).
|
|
|
|
|
|
Doc("获取任务执行日志").
|
|
|
|
|
|
Metadata(restfulspec.KeyOpenAPITags, tags).
|
|
|
|
|
|
Param(ws.PathParameter("taskId", "任务ID").DataType("string")))
|
2026-03-15 17:57:35 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RunTask 处理运行任务的API请求
|
|
|
|
|
|
// Body 参数是 TaskSpec, 返回 Task
|
|
|
|
|
|
func (h *TaskApiHandler) RunTask(r *restful.Request, w *restful.Response) {
|
|
|
|
|
|
// 解析请求参数,构造 TaskSpec
|
2026-03-29 11:41:32 +08:00
|
|
|
|
req := tasks.NewRunTaskRequest("", "")
|
2026-03-15 17:57:35 +08:00
|
|
|
|
if err := r.ReadEntity(req); err != nil {
|
|
|
|
|
|
// "github.com/infraboard/mcube/v2/http/restful/response"
|
|
|
|
|
|
// Failed 封装的 GoRestful框架的 异常返回
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 补充TaskId
|
2026-03-29 11:41:32 +08:00
|
|
|
|
if req.WorkDir == "" {
|
|
|
|
|
|
req.WorkDir = uuid.NewString()
|
2026-03-15 17:57:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 找到对应的 TaskRunner, 这里我们通过 TaskSpec 中的 Name 字段来找到对应的 TaskRunner
|
|
|
|
|
|
taskDebugRunner := tasks.GetTaskRunner(req.Name)
|
|
|
|
|
|
if taskDebugRunner == nil {
|
|
|
|
|
|
response.Failed(w, exception.NewInternalServerError("%s not found", req.Name))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理请求
|
|
|
|
|
|
taskResp, err := taskDebugRunner.Run(r.Request.Context(), req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
response.Success(w, taskResp)
|
|
|
|
|
|
}
|
2026-03-29 11:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 通过 taskid 获取 Task 的执行结果, 就是Workspace目录下的 result.json 文件内容
|
|
|
|
|
|
func (h *TaskApiHandler) GetTaskResult(r *restful.Request, w *restful.Response) {
|
|
|
|
|
|
// 1. 从请求路径中获取 taskId
|
|
|
|
|
|
taskId := r.PathParameter("taskId")
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 通过 taskId 构建工作目录路径, 工作目录结构是固定的, 比如 workspace/{taskId}
|
|
|
|
|
|
wordirPath, err := script.GetWorkDirAbsPath(taskId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 从工作目录下读取 result.json 文件, 这个文件是脚本执行完成后生成的, 包含了执行结果
|
|
|
|
|
|
resultFilePath := filepath.Join(wordirPath, "result.json")
|
|
|
|
|
|
resultFile, err := os.ReadFile(resultFilePath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result script.ExecutionResult
|
|
|
|
|
|
if err := json.Unmarshal(resultFile, &result); err != nil {
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(w, &result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 通过 taskid 获取 Task 的执行日志, 就是Workspace目录下的 stdout.log 文件内容
|
|
|
|
|
|
// 添加 offset,limit 参数,支持分页读取日志内容
|
|
|
|
|
|
func (h *TaskApiHandler) GetTaskLog(r *restful.Request, w *restful.Response) {
|
|
|
|
|
|
// 1. 从请求路径中获取 taskId
|
|
|
|
|
|
taskId := r.PathParameter("taskId")
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 从查询参数中获取 offset 和 limit(行数),默认值分别是0和100, ?offset=0&limit=100
|
|
|
|
|
|
offsetStr := r.QueryParameter("offset")
|
|
|
|
|
|
limitStr := r.QueryParameter("limit")
|
|
|
|
|
|
offset, err := strconv.Atoi(offsetStr)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
offset = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
limit, err := strconv.Atoi(limitStr)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
limit = 1000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 通过 taskId 构建工作目录路径, 工作目录结构是固定的, 比如 workspace/{taskId}
|
|
|
|
|
|
wordirPath, err := script.GetWorkDirAbsPath(taskId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 从工作目录下读取 stdout.log 文件, 这个文件是脚本执行过程中生成的, 包含了执行日志
|
|
|
|
|
|
logFilePath := filepath.Join(wordirPath, "stdout.txt")
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 读取日志文件内容,并根据 offset 和 limit 返回对应的日志片段
|
|
|
|
|
|
logFile, err := os.Open(logFilePath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.Failed(w, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer logFile.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// offset和limit(行数) 读取日志文件内容
|
|
|
|
|
|
logs := bytes.NewBuffer([]byte{})
|
|
|
|
|
|
scanner := bufio.NewScanner(logFile)
|
|
|
|
|
|
|
|
|
|
|
|
for i := 0; scanner.Scan() && i < offset+limit; i++ {
|
|
|
|
|
|
if i < offset {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
logs.Write(scanner.Bytes())
|
|
|
|
|
|
logs.Write([]byte("\n"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
|
|
w.Write(logs.Bytes())
|
|
|
|
|
|
}
|