From a9e3591b2d9984f1b927c58d435c58a21cf4b38e Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Wed, 11 Dec 2024 16:14:19 +0300 Subject: [PATCH] feat: add sdk and cli for task CRUD operations Signed-off-by: Rodney Osodo --- .golangci.yaml | 1 + cmd/propellerd/main.go | 11 +++ go.mod | 8 +- go.sum | 15 ++++ pkg/sdk/sdk.go | 122 ++++++++++++++++++++++++++ pkg/sdk/task.go | 142 +++++++++++++++++++++++++++++++ propellerd/log.go | 45 ++++++++++ propellerd/tasks.go | 189 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 531 insertions(+), 2 deletions(-) create mode 100644 pkg/sdk/sdk.go create mode 100644 pkg/sdk/task.go create mode 100644 propellerd/log.go create mode 100644 propellerd/tasks.go diff --git a/.golangci.yaml b/.golangci.yaml index e1bd2e1..b473231 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -29,6 +29,7 @@ linters: - interfacebloat - dupl - err113 + - noctx linters-settings: gocritic: diff --git a/cmd/propellerd/main.go b/cmd/propellerd/main.go index 790a337..2b5fb15 100644 --- a/cmd/propellerd/main.go +++ b/cmd/propellerd/main.go @@ -3,6 +3,7 @@ package main import ( "log" + "github.com/absmach/propeller/pkg/sdk" "github.com/absmach/propeller/propellerd" "github.com/spf13/cobra" ) @@ -12,11 +13,21 @@ func main() { Use: "propellerd", Short: "Propeller Daemon", Long: `Propeller Daemon is a daemon that manages the lifecycle of Propeller components.`, + PersistentPreRun: func(_ *cobra.Command, _ []string) { + sdkConf := sdk.Config{ + ManagerURL: propellerd.DefManagerURL, + TLSVerification: propellerd.DefTLSVerification, + } + s := sdk.NewSDK(sdkConf) + propellerd.SetSDK(s) + }, } managerCmd := propellerd.NewManagerCmd() + tasksCmd := propellerd.NewTasksCmd() rootCmd.AddCommand(managerCmd) + rootCmd.AddCommand(tasksCmd) if err := rootCmd.Execute(); err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index 2721f14..bdb794d 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ require ( github.com/0x6flab/namegenerator v1.4.0 github.com/absmach/magistrala v0.15.1 github.com/eclipse/paho.mqtt.golang v1.5.0 + github.com/fatih/color v1.18.0 github.com/go-chi/chi/v5 v5.1.0 github.com/go-kit/kit v0.13.0 github.com/google/uuid v1.6.0 + github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 github.com/tetratelabs/wazero v1.8.2 @@ -31,6 +33,8 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.61.0 // indirect @@ -44,8 +48,8 @@ require ( golang.org/x/net v0.32.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/go.sum b/go.sum index cc9baab..eebac91 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -40,12 +42,19 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -87,14 +96,20 @@ golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 h1:v+j+5gpj0FopU0KKLDGfDo9ZRRpKdi5UBrCP0f76kuY= google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= diff --git a/pkg/sdk/sdk.go b/pkg/sdk/sdk.go new file mode 100644 index 0000000..4d66698 --- /dev/null +++ b/pkg/sdk/sdk.go @@ -0,0 +1,122 @@ +package sdk + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net/http" +) + +const CTJSON string = "application/json" + +type PageMetadata struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` +} + +type SDK interface { + // CreateTask creates a new task. + // + // example: + // task := sdk.Task{ + // Name: "John Doe" + // } + // task, _ := sdk.CreateTask(task) + // fmt.Println(task) + CreateTask(task Task) (Task, error) + + // GetTask gets a task by id. + // + // example: + // task, _ := sdk.GetTask("b1d10738-c5d7-4ff1-8f4d-b9328ce6f040") + // fmt.Println(task) + GetTask(id string) (Task, error) + + // ListTasks lists tasks. + // + // example: + // taskPage, _ := sdk.ListTasks(0, 10) + // fmt.Println(taskPage) + ListTasks(offset uint64, limit uint64) (TaskPage, error) + + // UpdateTask updates a task. + // + // example: + // task := sdk.Task{ + // Name: "John Doe" + // } + // task, _ := sdk.UpdateTask(task) + // fmt.Println(task) + UpdateTask(task Task) (Task, error) + + // DeleteTask deletes a task. + // + // example: + // task, _ := sdk.DeleteTask("b1d10738-c5d7-4ff1-8f4d-b9328ce6f040") + // fmt.Println(task) + DeleteTask(id string) error + + // StartTask starts a task. + // + // example: + // task, _ := sdk.StartTask("b1d10738-c5d7-4ff1-8f4d-b9328ce6f040") + // fmt.Println(task) + StartTask(id string) error + + // StopTask stops a task. + // + // example: + // task, _ := sdk.StopTask("b1d10738-c5d7-4ff1-8f4d-b9328ce6f040") + // fmt.Println(task) + StopTask(id string) error +} + +type propSDK struct { + managerURL string + client *http.Client +} + +type Config struct { + ManagerURL string + TLSVerification bool +} + +func NewSDK(cfg Config) SDK { + return &propSDK{ + managerURL: cfg.ManagerURL, + client: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: !cfg.TLSVerification, + }, + }, + }, + } +} + +func (sdk *propSDK) processRequest(method, reqURL string, data []byte, expectedRespCode int) ([]byte, error) { + req, err := http.NewRequest(method, reqURL, bytes.NewReader(data)) + if err != nil { + return []byte{}, err + } + + req.Header.Add("Content-Type", CTJSON) + + resp, err := sdk.client.Do(req) + if err != nil { + return []byte{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return []byte{}, err + } + + if resp.StatusCode != expectedRespCode { + return []byte{}, fmt.Errorf("unexpected response code: %d", resp.StatusCode) + } + + return body, nil +} diff --git a/pkg/sdk/task.go b/pkg/sdk/task.go new file mode 100644 index 0000000..be8e5c1 --- /dev/null +++ b/pkg/sdk/task.go @@ -0,0 +1,142 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" +) + +const tasksEndpoint = "/tasks" + +type Task struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + State uint8 `json:"state,omitempty"` + StartTime time.Time `json:"start_time"` + FinishTime time.Time `json:"finish_time"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TaskPage struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Total uint64 `json:"total"` + Tasks []Task `json:"tasks"` +} + +func (sdk *propSDK) CreateTask(task Task) (Task, error) { + data, err := json.Marshal(task) + if err != nil { + return Task{}, err + } + + url := sdk.managerURL + tasksEndpoint + + body, err := sdk.processRequest(http.MethodPost, url, data, http.StatusCreated) + if err != nil { + return Task{}, err + } + + var t Task + if err := json.Unmarshal(body, &t); err != nil { + return Task{}, err + } + + return t, nil +} + +func (sdk *propSDK) GetTask(id string) (Task, error) { + url := sdk.managerURL + tasksEndpoint + "/" + id + + body, err := sdk.processRequest(http.MethodGet, url, nil, http.StatusOK) + if err != nil { + return Task{}, err + } + + var t Task + if err := json.Unmarshal(body, &t); err != nil { + return Task{}, err + } + + return t, nil +} + +func (sdk *propSDK) ListTasks(offset, limit uint64) (TaskPage, error) { + queries := make([]string, 0) + if offset > 0 { + queries = append(queries, fmt.Sprintf("offset=%d", offset)) + } + if limit > 0 { + queries = append(queries, fmt.Sprintf("limit=%d", limit)) + } + query := "" + if len(queries) > 0 { + query = "?" + strings.Join(queries, "&") + } + url := sdk.managerURL + tasksEndpoint + query + + body, err := sdk.processRequest(http.MethodGet, url, nil, http.StatusOK) + if err != nil { + return TaskPage{}, err + } + + var t TaskPage + if err := json.Unmarshal(body, &t); err != nil { + return TaskPage{}, err + } + + return t, nil +} + +func (sdk *propSDK) UpdateTask(task Task) (Task, error) { + data, err := json.Marshal(task) + if err != nil { + return Task{}, err + } + url := sdk.managerURL + tasksEndpoint + "/" + task.ID + + body, err := sdk.processRequest(http.MethodPut, url, data, http.StatusOK) + if err != nil { + return Task{}, err + } + + var t Task + if err := json.Unmarshal(body, &t); err != nil { + return Task{}, err + } + + return t, nil +} + +func (sdk *propSDK) DeleteTask(id string) error { + url := sdk.managerURL + tasksEndpoint + "/" + id + + if _, err := sdk.processRequest(http.MethodDelete, url, nil, http.StatusNoContent); err != nil { + return err + } + + return nil +} + +func (sdk *propSDK) StartTask(id string) error { + url := fmt.Sprintf("%s/tasks/%s/start", sdk.managerURL, id) + + if _, err := sdk.processRequest(http.MethodPost, url, nil, http.StatusOK); err != nil { + return err + } + + return nil +} + +func (sdk *propSDK) StopTask(id string) error { + url := fmt.Sprintf("%s/tasks/%s/stop", sdk.managerURL, id) + + if _, err := sdk.processRequest(http.MethodPost, url, nil, http.StatusOK); err != nil { + return err + } + + return nil +} diff --git a/propellerd/log.go b/propellerd/log.go new file mode 100644 index 0000000..c9f74b6 --- /dev/null +++ b/propellerd/log.go @@ -0,0 +1,45 @@ +package propellerd + +import ( + "encoding/json" + "fmt" + + "github.com/fatih/color" + "github.com/hokaccha/go-prettyjson" + "github.com/spf13/cobra" +) + +func logJSONCmd(cmd cobra.Command, iList ...interface{}) { + for _, i := range iList { + m, err := json.Marshal(i) + if err != nil { + logErrorCmd(cmd, err) + + return + } + + pj, err := prettyjson.Format(m) + if err != nil { + logErrorCmd(cmd, err) + + return + } + + fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n\n", string(pj)) + } +} + +func logUsageCmd(cmd cobra.Command, u string) { + fmt.Fprintf(cmd.OutOrStdout(), color.YellowString("\nusage: %s\n\n"), u) +} + +func logErrorCmd(cmd cobra.Command, err error) { + boldRed := color.New(color.FgRed, color.Bold) + boldRed.Fprintf(cmd.ErrOrStderr(), "\nerror: ") + + fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\n", color.RedString(err.Error())) +} + +func logOKCmd(cmd cobra.Command) { + fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n\n", color.BlueString("ok")) +} diff --git a/propellerd/tasks.go b/propellerd/tasks.go new file mode 100644 index 0000000..bc3ec1a --- /dev/null +++ b/propellerd/tasks.go @@ -0,0 +1,189 @@ +package propellerd + +import ( + "github.com/absmach/propeller/pkg/sdk" + "github.com/spf13/cobra" +) + +var ( + DefTLSVerification = false + DefManagerURL = "http://localhost:7070" + defOffset uint64 = 0 + defLimit uint64 = 10 +) + +var psdk sdk.SDK + +func SetSDK(s sdk.SDK) { + psdk = s +} + +var tasksCmd = []cobra.Command{ + { + Use: "create ", + Short: "Create task", + Long: `Create task.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + logUsageCmd(*cmd, cmd.Use) + + return + } + + t, err := psdk.CreateTask(sdk.Task{ + Name: args[0], + }) + if err != nil { + logErrorCmd(*cmd, err) + + return + } + logJSONCmd(*cmd, t) + }, + }, + { + Use: "view ", + Short: "View task", + Long: `View task.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + logUsageCmd(*cmd, cmd.Use) + + return + } + + t, err := psdk.GetTask(args[0]) + if err != nil { + logErrorCmd(*cmd, err) + + return + } + logJSONCmd(*cmd, t) + }, + }, + { + Use: "update ", + Short: "Update task", + Long: `Update task.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + logUsageCmd(*cmd, cmd.Use) + + return + } + + t, err := psdk.UpdateTask(sdk.Task{ + ID: args[0], + }) + if err != nil { + logErrorCmd(*cmd, err) + + return + } + logJSONCmd(*cmd, t) + }, + }, + { + Use: "delete ", + Short: "Delete task", + Long: `Delete task.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + logUsageCmd(*cmd, cmd.Use) + + return + } + + if err := psdk.DeleteTask(args[0]); err != nil { + logErrorCmd(*cmd, err) + + return + } + logOKCmd(*cmd) + }, + }, + { + Use: "start ", + Short: "Start task", + Long: `Start task.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + logUsageCmd(*cmd, cmd.Use) + + return + } + + if err := psdk.StartTask(args[0]); err != nil { + logErrorCmd(*cmd, err) + + return + } + logOKCmd(*cmd) + }, + }, + { + Use: "stop ", + Short: "Stop task", + Long: `Stop task.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + logUsageCmd(*cmd, cmd.Use) + + return + } + + if err := psdk.StopTask(args[0]); err != nil { + logErrorCmd(*cmd, err) + + return + } + logOKCmd(*cmd) + }, + }, +} + +func NewTasksCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "tasks [create|view|update|delete|start|stop]", + Short: "Tasks manager", + Long: `Create, view, update, delete, start, stop tasks.`, + } + + for i := range tasksCmd { + cmd.AddCommand(&tasksCmd[i]) + } + + cmd.PersistentFlags().StringVarP( + &DefManagerURL, + "manager-url", + "m", + DefManagerURL, + "Manager URL", + ) + + cmd.PersistentFlags().Uint64VarP( + &defOffset, + "offset", + "o", + defOffset, + "Offset", + ) + + cmd.PersistentFlags().Uint64VarP( + &defLimit, + "limit", + "l", + defLimit, + "Limit", + ) + + cmd.PersistentFlags().BoolVarP( + &DefTLSVerification, + "tls-verification", + "v", + DefTLSVerification, + "TLS Verification", + ) + + return &cmd +}