From c0a27b8d1fba190e6b103ca2784c50d23810050c Mon Sep 17 00:00:00 2001 From: joelei Date: Tue, 9 Dec 2025 21:01:21 +0800 Subject: [PATCH] add handler --- task/.golangci.yml | 96 ++++++ task/brokers/etcd/delay_task_test.go | 1 - task/brokers/etcd/etcd_test.go | 5 +- task/example/main.go | 3 +- task/go.mod | 17 ++ task/go.sum | 19 ++ task/handler.go | 418 +++++++++++++++++++++++++++ 7 files changed, 554 insertions(+), 5 deletions(-) create mode 100644 task/.golangci.yml create mode 100644 task/handler.go diff --git a/task/.golangci.yml b/task/.golangci.yml new file mode 100644 index 0000000..0221522 --- /dev/null +++ b/task/.golangci.yml @@ -0,0 +1,96 @@ +version: "2" + +run: + timeout: 5m + # tests: false # 过滤_test.go文件 + build-tags: + - goexperiment.jsonv2 + +issues: + # 显示所有 issue + max-issues-per-linter: 0 + max-same-issues: 0 + +formatters: + # 必须在enable打开才能用 + enable: + - gofmt + - gci + + settings: + gci: + sections: + - standard + - default + - prefix(git.ifooth.com/common/pkg/task) + custom-order: true + +linters: + enable: + # enable by default + - errcheck + - govet + - ineffassign + - staticcheck + - unused + + # custom + - goconst + - goheader + - gosec + - misspell + - nakedret + - revive + - unconvert + - unparam + - modernize + + # 忽略特定错误 + # exclusions: + # rules: + # - linters: + # - govet + # text: 'shadow: declaration of "err" shadows declaration' + + settings: + # 只开启特定的规则 + errcheck: + exclude-functions: + - (*os.File).Close + - (io.Closer).Close + - (net/http.ResponseWriter).Write + - io.Copy + - os.RemoveAll + govet: + enable: + - shadow + gosec: + includes: + - G201 # SQL query construction using format string + - G202 # SQL query construction using string concatenation + - G101 # Look for hard coded credentials + - G401 # Detect the usage of DES, RC4, MD5 or SHA1 + - G402 # Look for bad TLS connection settings + - G403 # Ensure minimum RSA key length of 2048 bits + - G504 # Import blocklist: net/http/cgi + misspell: + locale: US + revive: + rules: + - name: line-length-limit + arguments: + - 120 + - name: function-length + arguments: + - 80 # statements + - 80 # lines + - name: cyclomatic + arguments: + - 20 + - name: use-any + - name: early-return + - name: exported + arguments: + - checkPrivateReceivers + - sayRepetitiveInsteadOfStutters + - name: package-comments diff --git a/task/brokers/etcd/delay_task_test.go b/task/brokers/etcd/delay_task_test.go index 6f112a1..ed23371 100644 --- a/task/brokers/etcd/delay_task_test.go +++ b/task/brokers/etcd/delay_task_test.go @@ -18,7 +18,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.etcd.io/etcd/api/v3/mvccpb" ) diff --git a/task/brokers/etcd/etcd_test.go b/task/brokers/etcd/etcd_test.go index d2ec160..97440f8 100644 --- a/task/brokers/etcd/etcd_test.go +++ b/task/brokers/etcd/etcd_test.go @@ -19,11 +19,10 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/RichardKnop/machinery/v2/config" "github.com/RichardKnop/machinery/v2/tasks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFindTaskKey(t *testing.T) { diff --git a/task/example/main.go b/task/example/main.go index bbbb5fa..5e88ad6 100644 --- a/task/example/main.go +++ b/task/example/main.go @@ -21,6 +21,8 @@ import ( "syscall" "time" + "github.com/RichardKnop/machinery/v2/config" + "git.ifooth.com/common/pkg/task" etcdbackend "git.ifooth.com/common/pkg/task/backends/etcd" etcdbroker "git.ifooth.com/common/pkg/task/brokers/etcd" @@ -28,7 +30,6 @@ import ( istep "git.ifooth.com/common/pkg/task/steps/iface" mysqlstore "git.ifooth.com/common/pkg/task/stores/mysql" "git.ifooth.com/common/pkg/task/types" - "github.com/RichardKnop/machinery/v2/config" ) /* diff --git a/task/go.mod b/task/go.mod index ae510ba..ac2d364 100644 --- a/task/go.mod +++ b/task/go.mod @@ -2,10 +2,14 @@ module git.ifooth.com/common/pkg/task go 1.25 +replace git.ifooth.com/common/pkg => ../ + require ( + git.ifooth.com/common/pkg v0.0.0-00010101000000-000000000000 github.com/RichardKnop/machinery/v2 v2.0.16 github.com/google/uuid v1.6.0 github.com/prometheus/client_golang v1.20.5 + github.com/samber/lo v1.52.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.17 go.etcd.io/etcd/api/v3 v3.6.6 @@ -34,6 +38,13 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect @@ -47,6 +58,7 @@ require ( github.com/jstemmer/go-junit-report v0.9.1 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect @@ -64,6 +76,11 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect go.mongodb.org/mongo-driver v1.17.0 // indirect go.opencensus.io v0.22.5 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.36.0 // indirect diff --git a/task/go.sum b/task/go.sum index d8d0517..495cdcc 100644 --- a/task/go.sum +++ b/task/go.sum @@ -89,13 +89,26 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -192,6 +205,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -218,6 +233,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -263,6 +280,8 @@ go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= diff --git a/task/handler.go b/task/handler.go new file mode 100644 index 0000000..254762d --- /dev/null +++ b/task/handler.go @@ -0,0 +1,418 @@ +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 /api/v1/task/update", rest.Handle(s.Update)) + mux.HandleFunc("POST /api/v1/task/retry", rest.Handle(s.Retry)) + mux.HandleFunc("POST /api/v1/task/revoke", rest.Handle(s.Revoke)) + mux.HandleFunc("GET /api/v1/task/status", rest.Handle(s.Status)) + mux.HandleFunc("GET /api/v1/task/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 time.Duration `json:"executionDuration" swaggertype:"string"` + MaxExecutionDuration time.Duration `json:"maxExecutionDuration" swaggertype:"string"` + DetailURL string `json:"detailURL,omitempty"` +} + +// Step bcs_task step with execution duration +type Step struct { + *itypes.Step + ExecutionDuration time.Duration `json:"executionDuration" swaggertype:"string"` + MaxExecutionDuration time.Duration `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" in:"query=taskID" 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, + MaxExecutionDuration: time.Duration(v.MaxExecutionSeconds) * time.Second, + } + }) + + t := &Task{ + Task: taskData, + Steps: steps, + ExecutionDuration: time.Duration(taskData.ExecutionTime) * time.Millisecond, + MaxExecutionDuration: time.Duration(taskData.MaxExecutionSeconds) * time.Second, + } + + return t, nil +} + +// ListReq ... +type ListReq struct { + rest.PaginationReq + TaskID string `json:"taskID" in:"query=taskID"` + TaskType string `json:"taskType" in:"query=taskType"` + TaskName string `json:"taskName" in:"query=taskName"` + TaskIndex string `json:"taskIndex" in:"query=taskIndex"` + CurrentStep string `json:"currentStep" in:"query=currentStep"` + Status string `json:"status" in:"query=status" validate:"oneof='' SUCCESS FAILURE RUNNING TIMEOUT REVOKED NOTSTARTED INITIALIZING"` // nolint + Creator string `json:"creator" in:"query=creator"` +} + +// 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, + MaxExecutionDuration: time.Duration(v.MaxExecutionSeconds) * time.Second, + DetailURL: fmt.Sprintf("%s?taskID=%s", detailURL, v.TaskID), + } + }) + + resp := &rest.PaginationResp[Task]{ + Items: items, + Count: result.Count, + } + return resp, nil +}