package task import ( "context" "encoding/base64" "fmt" "net/http" "net/url" "time" "git.ifooth.com/common/pkg/rest" "git.ifooth.com/common/pkg/validator" "github.com/samber/lo" istore "git.ifooth.com/common/pkg/task/stores/iface" "git.ifooth.com/common/pkg/task/types" itypes "git.ifooth.com/common/pkg/task/types" ) type service struct { host string routePrefix string mgr *TaskManager } var ( // TaskStatusSlice ... TaskStatusSlice = []string{ types.TaskStatusInit, types.TaskStatusRunning, types.TaskStatusSuccess, types.TaskStatusFailure, types.TaskStatusTimeout, types.TaskStatusRevoked, types.TaskStatusNotStarted, } ) func NewHandler(mgr *TaskManager, host string, routePrefix string) http.Handler { mux := http.NewServeMux() s := &service{ mgr: mgr, host: host, routePrefix: routePrefix, } // 任务管理 mux.HandleFunc("POST /update", rest.Handle(s.Update)) mux.HandleFunc("POST /retry", rest.Handle(s.Retry)) mux.HandleFunc("POST /revoke", rest.Handle(s.Revoke)) mux.HandleFunc("GET /status", rest.Handle(s.Status)) mux.HandleFunc("GET /list", rest.Handle(s.List)) return mux } // Task bcs_task task with execution duration type Task struct { *itypes.Task Steps []*Step `json:"steps,omitempty"` ExecutionDuration string `json:"executionDuration" swaggertype:"string"` MaxExecutionDuration string `json:"maxExecutionDuration" swaggertype:"string"` DetailURL string `json:"detailURL,omitempty"` } // Step bcs_task step with execution duration type Step struct { *itypes.Step ExecutionDuration string `json:"executionDuration,fmt" swaggertype:"string"` MaxExecutionDuration string `json:"maxExecutionDuration" swaggertype:"string"` } // StepReq ... type StepReq struct { Name string `json:"name"` Params *map[string]string `json:"params"` Payload *string `json:"payload"` RetryCount *uint32 `json:"retryCount"` Status *string `json:"status"` } // Validate ... func (s *StepReq) Validate() error { if s.Status != nil && !lo.Contains(TaskStatusSlice, *s.Status) { return fmt.Errorf("step status %s not valid", *s.Status) } return nil } // UpdateReq 请求参数 type UpdateReq struct { TaskID string `json:"taskID" validate:"required"` ResetStartTime bool `json:"resetStartTime"` Status string `json:"status"` Steps []*StepReq `json:"steps"` } // Validate ... func (r *UpdateReq) Validate() error { if r.Status != "" && !lo.Contains(TaskStatusSlice, r.Status) { return fmt.Errorf("task status %s not valid", r.Status) } for _, v := range r.Steps { if err := v.Validate(); err != nil { return err } } return nil } // RetryReq 请求参数 type RetryReq struct { TaskID string `json:"taskID" validate:"required"` StepName string `json:"stepName"` } // Update 更新任务 // // @ID UpdateTask // @Summary 更新任务 // @Description 更新任务 // @Tags 任务管理 // @Accept json // @Produce json // @Param request body UpdateReq true "task info" // @Success 200 {object} rest.APIResponse{data=nil} // @Failure 400 // @x-bk-apigateway {"isPublic": false, "appVerifiedRequired": true, "userVerifiedRequired": false} // @Router /api/v1/remotedevenv/task/update [post] func (s *service) Update(ctx context.Context, req *UpdateReq) (*rest.EmptyResp, error) { if err := validator.Struct(ctx, req); err != nil { return nil, err } t, err := s.mgr.GetTaskWithID(ctx, req.TaskID) if err != nil { return nil, err } if req.ResetStartTime { t.Start = time.Now() } if req.Status != "" { t.Status = req.Status } for _, v := range req.Steps { step, ok := t.GetStep(v.Name) if !ok { return nil, fmt.Errorf("step %s not found", v.Name) } if v.Params != nil { step.Params = *v.Params } if v.RetryCount != nil { step.RetryCount = *v.RetryCount } if v.Status != nil { step.Status = *v.Status } if v.Payload != nil { body, err := base64.StdEncoding.DecodeString(*v.Payload) if err != nil { return nil, err } step.Payload = string(body) } t.SetCurrentStep(v.Name) // 只更新DB if err := s.mgr.UpdateTask(ctx, t); err != nil { return nil, err } } resp := &rest.EmptyResp{} return resp, nil } // Retry 重试任务 // // @ID RetryTask // @Summary 重试任务 // @Description 重试任务 // @Tags 任务管理,clouddev,cloudpc // @Accept json // @Produce json // @Param request body RetryReq true "task info" // @Success 200 {object} rest.APIResponse{data=nil} // @Failure 400 // @x-bk-apigateway {"isPublic": false, "appVerifiedRequired": true, "userVerifiedRequired": false} // @Router /api/v1/remotedevenv/task/retry [post] func (s *service) Retry(ctx context.Context, req *RetryReq) (*rest.EmptyResp, error) { if err := validator.Struct(ctx, req); err != nil { return nil, err } t, err := s.mgr.GetTaskWithID(ctx, req.TaskID) if err != nil { return nil, err } resp := &rest.EmptyResp{} if t.Status != itypes.TaskStatusFailure && t.Status != itypes.TaskStatusRevoked && t.Status != itypes.TaskStatusTimeout { return nil, fmt.Errorf("task %s already in process %s", req.TaskID, t.Status) } if t.Status == itypes.TaskStatusTimeout { t.Start = time.Now() } // 只重试单步骤 if req.StepName != "" { step, ok := t.GetStep(req.StepName) if !ok { return nil, fmt.Errorf("step %s not found", req.StepName) } if step.Status == itypes.TaskStatusSuccess { return nil, fmt.Errorf("step %s already success", req.StepName) } if step.Status == itypes.TaskStatusFailure { step.RetryCount = 0 step.Status = itypes.TaskStatusNotStarted } t.SetCurrentStep(req.StepName) if err := s.mgr.RetryAt(t, req.StepName); err != nil { return nil, err } return resp, nil } // 全量重试必须有失败的任务 notSuccessStep := lo.Filter(t.Steps, func(v *itypes.Step, _ int) bool { return v.Status != itypes.TaskStatusSuccess }) if len(notSuccessStep) == 0 { return nil, fmt.Errorf("task %s all step already success", req.TaskID) } // 错误步骤重试次数置为0, 只当前步骤有效 for _, step := range t.Steps { if step.Status == itypes.TaskStatusFailure { step.RetryCount = 0 step.Status = itypes.TaskStatusNotStarted } } // 任务级重试 if err := s.mgr.RetryAll(t); err != nil { return nil, err } return resp, nil } // commonReq 任务请求参数 type commonReq struct { TaskID string `json:"taskID" req:"taskID,in=query" validate:"required"` } // Revoke 取消任务 // // @ID RevokeTask // @Summary 取消任务 // @Description 取消任务 // @Tags 任务管理,clouddev,cloudpc // @Accept json // @Produce json // @Param request body commonReq true "common info" // @Success 200 {object} rest.APIResponse{data=nil} // @Failure 400 // @x-bk-apigateway {"isPublic": false, "appVerifiedRequired": true, "userVerifiedRequired": false} // @Router /api/v1/remotedevenv/task/revoke [post] func (s *service) Revoke(ctx context.Context, req *commonReq) (*rest.EmptyResp, error) { if err := validator.Struct(ctx, req); err != nil { return nil, err } t, err := s.mgr.GetTaskWithID(ctx, req.TaskID) if err != nil { return nil, err } if t.Status != itypes.TaskStatusRunning { return nil, fmt.Errorf("task %s not running", req.TaskID) } if err := s.mgr.Revoke(t); err != nil { return nil, err } return &rest.EmptyResp{}, nil } // Status 任务状态 // // @ID TaskStatus // @Summary 任务状态 // @Description 获取任务状态 // @Tags 任务管理,clouddev,cloudpc // @Accept json // @Produce json // @Param taskID query string true "task id" // @Success 200 {object} rest.APIResponse{data=Task} // @Failure 400 // @x-bk-apigateway {"isPublic": false, "appVerifiedRequired": true, "userVerifiedRequired": false} // @Router /api/v1/remotedevenv/task/status [get] func (s *service) Status(ctx context.Context, req *commonReq) (*Task, error) { if err := validator.Struct(ctx, req); err != nil { return nil, err } taskData, err := s.mgr.GetTaskWithID(ctx, req.TaskID) if err != nil { return nil, err } steps := lo.Map(taskData.Steps, func(v *itypes.Step, _ int) *Step { return &Step{ Step: v, ExecutionDuration: (time.Duration(v.ExecutionTime) * time.Millisecond).String(), MaxExecutionDuration: (time.Duration(v.MaxExecutionSeconds) * time.Second).String(), } }) t := &Task{ Task: taskData, Steps: steps, ExecutionDuration: (time.Duration(taskData.ExecutionTime) * time.Millisecond).String(), MaxExecutionDuration: (time.Duration(taskData.MaxExecutionSeconds) * time.Second).String(), } return t, nil } // ListReq ... type ListReq struct { rest.PaginationReq TaskID string `json:"taskID" req:"taskID,in=query"` TaskType string `json:"taskType" req:"taskType,in=query"` TaskName string `json:"taskName" req:"taskType,in=query"` TaskIndex string `json:"taskIndex" req:"taskIndex,in=query"` CurrentStep string `json:"currentStep" req:"currentStep,in=query"` Status string `json:"status" req:"status,in=query" validate:"oneof='' SUCCESS FAILURE RUNNING TIMEOUT REVOKED NOTSTARTED INITIALIZING"` // nolint Creator string `json:"creator" req:"creator,in=query"` } // List 任务分页列表 // // @ID ListTask // @Summary 任务列表 // @Description 获取任务列表 // @Tags 任务管理 // @Accept json // @Produce json // @Param offset query int false "offset" // @Param limit query int false "limit" // @Param taskID query string false "task id" // @Param taskType query string false "task type" // @Param taskName query string false "task name" // @Param taskIndex query string false "task id" // @Param currentStep query string false "current step" // @Param status query string false "status" // @Param creator query string false "creator" // @Success 200 {object} rest.APIResponse{data=rest.PaginationResp[Task]} // @Failure 400 // @x-bk-apigateway {"isPublic": false, "appVerifiedRequired": true, "userVerifiedRequired": false} // @Router /api/v1/task/list [get] func (s *service) List(ctx context.Context, req *ListReq) (*rest.PaginationResp[Task], error) { if err := validator.Struct(ctx, req); err != nil { return nil, err } statusURL := "/api/v1/task/status" detailURL, err := url.JoinPath(s.host, s.routePrefix, statusURL) if err != nil { return nil, err } // 默认返回20条数据 if req.Limit == 0 { req.Limit = 20 } listReq := &istore.ListOption{ TaskID: req.TaskID, TaskType: req.TaskType, TaskName: req.TaskName, TaskIndex: req.TaskIndex, CurrentStep: req.CurrentStep, Status: req.Status, Creator: req.Creator, Limit: int64(req.Limit), Offset: int64(req.Offset), } result, err := s.mgr.ListTask(ctx, listReq) if err != nil { return nil, err } items := lo.Map(result.Items, func(v *itypes.Task, _ int) Task { return Task{ Task: v, ExecutionDuration: (time.Duration(v.ExecutionTime) * time.Millisecond).String(), MaxExecutionDuration: (time.Duration(v.MaxExecutionSeconds) * time.Second).String(), DetailURL: fmt.Sprintf("%s?taskID=%s", detailURL, v.TaskID), } }) resp := &rest.PaginationResp[Task]{ Items: items, Count: result.Count, } return resp, nil }