diff --git a/flytectl/Makefile b/flytectl/Makefile index 8104b44ca9..118f40cf06 100644 --- a/flytectl/Makefile +++ b/flytectl/Makefile @@ -20,6 +20,9 @@ generate: compile: go build -o bin/flytectl -ldflags=$(LD_FLAGS) main.go +compile_debug: + go build -gcflags='all=-N -l' -o bin/flytectl main.go + .PHONY: update_boilerplate update_boilerplate: @boilerplate/update.sh diff --git a/flytectl/clierrors/errors.go b/flytectl/clierrors/errors.go new file mode 100644 index 0000000000..a93f3c7a19 --- /dev/null +++ b/flytectl/clierrors/errors.go @@ -0,0 +1,17 @@ +package clierrors + +var ( + ErrInvalidStateUpdate = "Invalid state passed. Specify either activate or archive\n" + + ErrProjectNotPassed = "Project not passed\n" + ErrFailedProjectUpdate = "Project %v failed to get updated to %v state due to %v\n" + + ErrLPNotPassed = "Launch plan name not passed\n" + ErrFailedLPUpdate = "Launch plan %v failed to get updated due to %v\n" + + ErrWorkflowNotPassed = "Workflow name not passed\n" + ErrFailedWorkflowUpdate = "Workflow %v failed to get updated to due to %v\n" + + ErrTaskNotPassed = "Task name not passed\n" // #nosec + ErrFailedTaskUpdate = "Task %v failed to get updated to due to %v\n" +) diff --git a/flytectl/cmd/create/executionconfig_flags.go b/flytectl/cmd/create/executionconfig_flags.go index 59dc9adf41..974c284b0b 100755 --- a/flytectl/cmd/create/executionconfig_flags.go +++ b/flytectl/cmd/create/executionconfig_flags.go @@ -47,6 +47,5 @@ func (cfg ExecutionConfig) GetPFlagSet(prefix string) *pflag.FlagSet { cmdFlags.StringVar(&(executionConfig.KubeServiceAcct), fmt.Sprintf("%v%v", prefix, "kubeServiceAcct"), executionConfig.KubeServiceAcct, "kubernetes service account AuthRole for launching execution.") cmdFlags.StringVar(&(executionConfig.IamRoleARN), fmt.Sprintf("%v%v", prefix, "iamRoleARN"), executionConfig.IamRoleARN, "iam role ARN AuthRole for launching execution.") cmdFlags.StringVar(&(executionConfig.Relaunch), fmt.Sprintf("%v%v", prefix, "relaunch"), executionConfig.Relaunch, "execution id to be relaunched.") - return cmdFlags } diff --git a/flytectl/cmd/update/interfaces/mocks/updater.go b/flytectl/cmd/update/interfaces/mocks/updater.go new file mode 100644 index 0000000000..30935bb156 --- /dev/null +++ b/flytectl/cmd/update/interfaces/mocks/updater.go @@ -0,0 +1,50 @@ +// Code generated by mockery v1.0.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + cmdcore "github.com/flyteorg/flytectl/cmd/core" + + core "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" + + mock "github.com/stretchr/testify/mock" +) + +// Updater is an autogenerated mock type for the Updater type +type Updater struct { + mock.Mock +} + +type Updater_UpdateNamedEntity struct { + *mock.Call +} + +func (_m Updater_UpdateNamedEntity) Return(_a0 error) *Updater_UpdateNamedEntity { + return &Updater_UpdateNamedEntity{Call: _m.Call.Return(_a0)} +} + +func (_m *Updater) OnUpdateNamedEntity(ctx context.Context, name string, project string, domain string, rsType core.ResourceType, cmdCtx cmdcore.CommandContext) *Updater_UpdateNamedEntity { + c := _m.On("UpdateNamedEntity", ctx, name, project, domain, rsType, cmdCtx) + return &Updater_UpdateNamedEntity{Call: c} +} + +func (_m *Updater) OnUpdateNamedEntityMatch(matchers ...interface{}) *Updater_UpdateNamedEntity { + c := _m.On("UpdateNamedEntity", matchers...) + return &Updater_UpdateNamedEntity{Call: c} +} + +// UpdateNamedEntity provides a mock function with given fields: ctx, name, project, domain, rsType, cmdCtx +func (_m *Updater) UpdateNamedEntity(ctx context.Context, name string, project string, domain string, rsType core.ResourceType, cmdCtx cmdcore.CommandContext) error { + ret := _m.Called(ctx, name, project, domain, rsType, cmdCtx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, core.ResourceType, cmdcore.CommandContext) error); ok { + r0 = rf(ctx, name, project, domain, rsType, cmdCtx) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/flytectl/cmd/update/interfaces/updater.go b/flytectl/cmd/update/interfaces/updater.go new file mode 100644 index 0000000000..3ea18f678a --- /dev/null +++ b/flytectl/cmd/update/interfaces/updater.go @@ -0,0 +1,14 @@ +package interfaces + +import ( + "context" + + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" +) + +//go:generate mockery -name=Updater -case=underscore + +type Updater interface { + UpdateNamedEntity(ctx context.Context, name, project, domain string, rsType core.ResourceType, cmdCtx cmdCore.CommandContext) error +} diff --git a/flytectl/cmd/update/launch_plan.go b/flytectl/cmd/update/launch_plan.go new file mode 100644 index 0000000000..1f32a2cb04 --- /dev/null +++ b/flytectl/cmd/update/launch_plan.go @@ -0,0 +1,49 @@ +package update + +import ( + "context" + "fmt" + + "github.com/flyteorg/flytectl/clierrors" + "github.com/flyteorg/flytectl/cmd/config" + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" +) + +const ( + updateLPShort = "Updates launch plan metadata" + updateLPLong = ` +Following command updates the description on the launchplan. +:: + + flytectl update launchplan -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --description "Mergesort example" + +Archiving launchplan named entity is not supported and would throw an error. +:: + + flytectl update launchplan -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --archive + +Activating launchplan named entity would be a noop. +:: + + flytectl update launchplan -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --activate + +Usage +` +) + +func updateLPFunc(ctx context.Context, args []string, cmdCtx cmdCore.CommandContext) error { + project := config.GetConfig().Project + domain := config.GetConfig().Domain + if len(args) != 1 { + return fmt.Errorf(clierrors.ErrLPNotPassed) + } + name := args[0] + err := namedEntityConfig.UpdateNamedEntity(ctx, name, project, domain, core.ResourceType_LAUNCH_PLAN, cmdCtx) + if err != nil { + fmt.Printf(clierrors.ErrFailedLPUpdate, name, err) + return err + } + fmt.Printf("updated metadata successfully on %v", name) + return nil +} diff --git a/flytectl/cmd/update/launch_plan_test.go b/flytectl/cmd/update/launch_plan_test.go new file mode 100644 index 0000000000..aa8f8601de --- /dev/null +++ b/flytectl/cmd/update/launch_plan_test.go @@ -0,0 +1,44 @@ +package update + +import ( + "fmt" + "testing" + + "github.com/flyteorg/flytectl/cmd/testutils" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/admin" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func UpdateLPSetup() { + ctx = testutils.Ctx + cmdCtx = testutils.CmdCtx + mockClient = testutils.MockClient +} + +func TestLPUpdate(t *testing.T) { + testutils.Setup() + UpdateLPSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{"task1"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(&admin.NamedEntityUpdateResponse{}, nil) + assert.Nil(t, updateLPFunc(ctx, args, cmdCtx)) +} + +func TestLPUpdateFail(t *testing.T) { + testutils.Setup() + UpdateLPSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{"task1"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(nil, fmt.Errorf("failed to update")) + assert.NotNil(t, updateTaskFunc(ctx, args, cmdCtx)) +} + +func TestLPUpdateInvalidArgs(t *testing.T) { + testutils.Setup() + UpdateLPSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{} + assert.NotNil(t, updateTaskFunc(ctx, args, cmdCtx)) +} diff --git a/flytectl/cmd/update/named_entity.go b/flytectl/cmd/update/named_entity.go new file mode 100644 index 0000000000..5c3391d708 --- /dev/null +++ b/flytectl/cmd/update/named_entity.go @@ -0,0 +1,50 @@ +package update + +import ( + "context" + "fmt" + + "github.com/flyteorg/flytectl/clierrors" + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/admin" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" +) + +//go:generate pflags NamedEntityConfig --default-var namedEntityConfig + +var ( + namedEntityConfig = &NamedEntityConfig{} +) + +type NamedEntityConfig struct { + Archive bool `json:"archive" pflag:",archive named entity."` + Activate bool `json:"activate" pflag:",activate the named entity."` + Description string `json:"description" pflag:",description of the named entity."` +} + +func (n NamedEntityConfig) UpdateNamedEntity(ctx context.Context, name string, project string, domain string, rsType core.ResourceType, cmdCtx cmdCore.CommandContext) error { + archiveProject := n.Archive + activateProject := n.Activate + if activateProject == archiveProject && activateProject { + return fmt.Errorf(clierrors.ErrInvalidStateUpdate) + } + var nameEntityState admin.NamedEntityState + if activateProject { + nameEntityState = admin.NamedEntityState_NAMED_ENTITY_ACTIVE + } else if archiveProject { + nameEntityState = admin.NamedEntityState_NAMED_ENTITY_ARCHIVED + } + _, err := cmdCtx.AdminClient().UpdateNamedEntity(ctx, &admin.NamedEntityUpdateRequest{ + ResourceType: rsType, + Id: &admin.NamedEntityIdentifier{ + Project: project, + Domain: domain, + Name: name, + }, + Metadata: &admin.NamedEntityMetadata{ + Description: n.Description, + State: nameEntityState, + }, + }) + return err +} diff --git a/flytectl/cmd/update/named_entity_test.go b/flytectl/cmd/update/named_entity_test.go new file mode 100644 index 0000000000..9c322f1840 --- /dev/null +++ b/flytectl/cmd/update/named_entity_test.go @@ -0,0 +1,48 @@ +package update + +import ( + "fmt" + "testing" + + "github.com/flyteorg/flytectl/cmd/testutils" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/admin" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" + "github.com/stretchr/testify/mock" + + "github.com/stretchr/testify/assert" +) + +func NamedEntitySetup() { + ctx = testutils.Ctx + cmdCtx = testutils.CmdCtx + mockClient = testutils.MockClient +} + +func TestNamedEntity(t *testing.T) { + testutils.Setup() + NamedEntitySetup() + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(&admin.NamedEntityUpdateResponse{}, nil) + namedEntityConfig = &NamedEntityConfig{Archive: false, Activate: true, Description: "named entity description"} + assert.Nil(t, namedEntityConfig.UpdateNamedEntity(ctx, "namedEntity", "project", "domain", + core.ResourceType_WORKFLOW, cmdCtx)) + namedEntityConfig = &NamedEntityConfig{Archive: true, Activate: false, Description: "named entity description"} + assert.Nil(t, namedEntityConfig.UpdateNamedEntity(ctx, "namedEntity", "project", "domain", + core.ResourceType_WORKFLOW, cmdCtx)) +} + +func TestNamedEntityValidationFailure(t *testing.T) { + testutils.Setup() + NamedEntitySetup() + namedEntityConfig = &NamedEntityConfig{Archive: true, Activate: true, Description: "named entity description"} + assert.NotNil(t, namedEntityConfig.UpdateNamedEntity(ctx, "namedEntity", "project", "domain", + core.ResourceType_WORKFLOW, cmdCtx)) +} + +func TestNamedEntityFailure(t *testing.T) { + testutils.Setup() + NamedEntitySetup() + namedEntityConfig = &NamedEntityConfig{Archive: true, Activate: true, Description: "named entity description"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(nil, fmt.Errorf("failed to update")) + assert.NotNil(t, namedEntityConfig.UpdateNamedEntity(ctx, "namedEntity", "project", "domain", + core.ResourceType_WORKFLOW, cmdCtx)) +} diff --git a/flytectl/cmd/update/namedentityconfig_flags.go b/flytectl/cmd/update/namedentityconfig_flags.go new file mode 100755 index 0000000000..3d61711bec --- /dev/null +++ b/flytectl/cmd/update/namedentityconfig_flags.go @@ -0,0 +1,48 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots. + +package update + +import ( + "encoding/json" + "reflect" + + "fmt" + + "github.com/spf13/pflag" +) + +// If v is a pointer, it will get its element value or the zero value of the element type. +// If v is not a pointer, it will return it as is. +func (NamedEntityConfig) elemValueOrNil(v interface{}) interface{} { + if t := reflect.TypeOf(v); t.Kind() == reflect.Ptr { + if reflect.ValueOf(v).IsNil() { + return reflect.Zero(t.Elem()).Interface() + } else { + return reflect.ValueOf(v).Interface() + } + } else if v == nil { + return reflect.Zero(t).Interface() + } + + return v +} + +func (NamedEntityConfig) mustMarshalJSON(v json.Marshaler) string { + raw, err := v.MarshalJSON() + if err != nil { + panic(err) + } + + return string(raw) +} + +// GetPFlagSet will return strongly types pflags for all fields in NamedEntityConfig and its nested types. The format of the +// flags is json-name.json-sub-name... etc. +func (n NamedEntityConfig) GetPFlagSet(prefix string) *pflag.FlagSet { + cmdFlags := pflag.NewFlagSet("NamedEntityConfig", pflag.ExitOnError) + cmdFlags.BoolVar(&(namedEntityConfig.Activate), fmt.Sprintf("%v%v", prefix, "activate"), *new(bool), "Activates the named entity specified as argument.") + cmdFlags.BoolVar(&(namedEntityConfig.Archive), fmt.Sprintf("%v%v", prefix, "archive"), *new(bool), "Archives the named entity specified as argument.") + cmdFlags.StringVar(&(namedEntityConfig.Description), fmt.Sprintf("%v%v", prefix, "description"), namedEntityConfig.Description, "description of the namedentity.") + return cmdFlags +} diff --git a/flytectl/cmd/update/namedentityconfig_flags_test.go b/flytectl/cmd/update/namedentityconfig_flags_test.go new file mode 100755 index 0000000000..1f9506481b --- /dev/null +++ b/flytectl/cmd/update/namedentityconfig_flags_test.go @@ -0,0 +1,168 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots. + +package update + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/assert" +) + +var dereferencableKindsNamedEntityConfig = map[reflect.Kind]struct{}{ + reflect.Array: {}, reflect.Chan: {}, reflect.Map: {}, reflect.Ptr: {}, reflect.Slice: {}, +} + +// Checks if t is a kind that can be dereferenced to get its underlying type. +func canGetElementNamedEntityConfig(t reflect.Kind) bool { + _, exists := dereferencableKindsNamedEntityConfig[t] + return exists +} + +// This decoder hook tests types for json unmarshaling capability. If implemented, it uses json unmarshal to build the +// object. Otherwise, it'll just pass on the original data. +func jsonUnmarshalerHookNamedEntityConfig(_, to reflect.Type, data interface{}) (interface{}, error) { + unmarshalerType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() + if to.Implements(unmarshalerType) || reflect.PtrTo(to).Implements(unmarshalerType) || + (canGetElementNamedEntityConfig(to.Kind()) && to.Elem().Implements(unmarshalerType)) { + + raw, err := json.Marshal(data) + if err != nil { + fmt.Printf("Failed to marshal Data: %v. Error: %v. Skipping jsonUnmarshalHook", data, err) + return data, nil + } + + res := reflect.New(to).Interface() + err = json.Unmarshal(raw, &res) + if err != nil { + fmt.Printf("Failed to umarshal Data: %v. Error: %v. Skipping jsonUnmarshalHook", data, err) + return data, nil + } + + return res, nil + } + + return data, nil +} + +func decode_NamedEntityConfig(input, result interface{}) error { + config := &mapstructure.DecoderConfig{ + TagName: "json", + WeaklyTypedInput: true, + Result: result, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + jsonUnmarshalerHookNamedEntityConfig, + ), + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +func join_NamedEntityConfig(arr interface{}, sep string) string { + listValue := reflect.ValueOf(arr) + strs := make([]string, 0, listValue.Len()) + for i := 0; i < listValue.Len(); i++ { + strs = append(strs, fmt.Sprintf("%v", listValue.Index(i))) + } + + return strings.Join(strs, sep) +} + +func testDecodeJson_NamedEntityConfig(t *testing.T, val, result interface{}) { + assert.NoError(t, decode_NamedEntityConfig(val, result)) +} + +func testDecodeSlice_NamedEntityConfig(t *testing.T, vStringSlice, result interface{}) { + assert.NoError(t, decode_NamedEntityConfig(vStringSlice, result)) +} + +func TestNamedEntityConfig_GetPFlagSet(t *testing.T) { + val := NamedEntityConfig{} + cmdFlags := val.GetPFlagSet("") + assert.True(t, cmdFlags.HasFlags()) +} + +func TestNamedEntityConfig_SetFlags(t *testing.T) { + actual := NamedEntityConfig{} + cmdFlags := actual.GetPFlagSet("") + assert.True(t, cmdFlags.HasFlags()) + + t.Run("Test_archive", func(t *testing.T) { + t.Run("DefaultValue", func(t *testing.T) { + // Test that default value is set properly + if vBool, err := cmdFlags.GetBool("archive"); err == nil { + assert.Equal(t, bool(namedEntityConfig.Archive), vBool) + } else { + assert.FailNow(t, err.Error()) + } + }) + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("archive", testValue) + if vBool, err := cmdFlags.GetBool("archive"); err == nil { + testDecodeJson_NamedEntityConfig(t, fmt.Sprintf("%v", vBool), &actual.Archive) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) + t.Run("Test_activate", func(t *testing.T) { + t.Run("DefaultValue", func(t *testing.T) { + // Test that default value is set properly + if vBool, err := cmdFlags.GetBool("activate"); err == nil { + assert.Equal(t, bool(namedEntityConfig.Activate), vBool) + } else { + assert.FailNow(t, err.Error()) + } + }) + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("activate", testValue) + if vBool, err := cmdFlags.GetBool("activate"); err == nil { + testDecodeJson_NamedEntityConfig(t, fmt.Sprintf("%v", vBool), &actual.Activate) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) + t.Run("Test_description", func(t *testing.T) { + t.Run("DefaultValue", func(t *testing.T) { + // Test that default value is set properly + if vString, err := cmdFlags.GetString("description"); err == nil { + assert.Equal(t, string(namedEntityConfig.Description), vString) + } else { + assert.FailNow(t, err.Error()) + } + }) + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("description", testValue) + if vString, err := cmdFlags.GetString("description"); err == nil { + testDecodeJson_NamedEntityConfig(t, fmt.Sprintf("%v", vString), &actual.Description) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) +} diff --git a/flytectl/cmd/update/project.go b/flytectl/cmd/update/project.go index 5865c7e6af..0a1f5415d7 100644 --- a/flytectl/cmd/update/project.go +++ b/flytectl/cmd/update/project.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/flyteorg/flytectl/clierrors" + "github.com/flyteorg/flytectl/cmd/config" cmdCore "github.com/flyteorg/flytectl/cmd/core" "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/admin" @@ -65,23 +67,18 @@ Usage ` ) -var ( - projectConfig = &ProjectConfig{} - errProjectNotFound = "Project %v not found\n" - errInvalidUpdate = "Invalid state passed. Specify either activate or archive\n" - errFailedUpdate = "Project %v failed to get updated to %v state due to %v\n" -) +var projectConfig = &ProjectConfig{} func updateProjectsFunc(ctx context.Context, args []string, cmdCtx cmdCore.CommandContext) error { id := config.GetConfig().Project if id == "" { - fmt.Printf(errProjectNotFound, id) + fmt.Printf(clierrors.ErrProjectNotPassed) return nil } archiveProject := projectConfig.ArchiveProject activateProject := projectConfig.ActivateProject if activateProject == archiveProject { - return fmt.Errorf(errInvalidUpdate) + return fmt.Errorf(clierrors.ErrInvalidStateUpdate) } projectState := admin.Project_ACTIVE if archiveProject { @@ -92,8 +89,8 @@ func updateProjectsFunc(ctx context.Context, args []string, cmdCtx cmdCore.Comma State: projectState, }) if err != nil { - fmt.Printf(errFailedUpdate, id, projectState, err) - return nil + fmt.Printf(clierrors.ErrFailedProjectUpdate, id, projectState, err) + return err } fmt.Printf("Project %v updated to %v state\n", id, projectState) return nil diff --git a/flytectl/cmd/update/project_test.go b/flytectl/cmd/update/project_test.go index 6b0491a375..a3e7d9e2e5 100644 --- a/flytectl/cmd/update/project_test.go +++ b/flytectl/cmd/update/project_test.go @@ -84,7 +84,7 @@ func TestActivateProjectFuncWithError(t *testing.T) { modifyProjectFlags(&(projectConfig.ArchiveProject), false, &(projectConfig.ActivateProject), true) mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, errors.New("Error Updating Project")) err := updateProjectsFunc(ctx, args, cmdCtx) - assert.Nil(t, err) + assert.NotNil(t, err) mockClient.AssertCalled(t, "UpdateProject", ctx, projectUpdateRequest) } @@ -112,13 +112,13 @@ func TestArchiveProjectFuncWithError(t *testing.T) { } mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, errors.New("Error Updating Project")) err := updateProjectsFunc(ctx, args, cmdCtx) - assert.Nil(t, err) + assert.NotNil(t, err) mockClient.AssertCalled(t, "UpdateProject", ctx, projectUpdateRequest) } func TestEmptyProjectInput(t *testing.T) { setup() - defer teardownAndVerify(t, "Project not found\n") + defer teardownAndVerify(t, "Project not passed\n") config.GetConfig().Project = "" modifyProjectFlags(&(projectConfig.ArchiveProject), false, &(projectConfig.ActivateProject), true) mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, nil) diff --git a/flytectl/cmd/update/task.go b/flytectl/cmd/update/task.go new file mode 100644 index 0000000000..e38cdff16b --- /dev/null +++ b/flytectl/cmd/update/task.go @@ -0,0 +1,49 @@ +package update + +import ( + "context" + "fmt" + + "github.com/flyteorg/flytectl/clierrors" + "github.com/flyteorg/flytectl/cmd/config" + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" +) + +const ( + updateTaskShort = "Updates task metadata" + updateTaskLong = ` +Following command updates the description on the task. +:: + + flytectl update task -d development -p flytectldemo core.advanced.run_merge_sort.merge --description "Merge sort example" + +Archiving task named entity is not supported and would throw an error. +:: + + flytectl update task -d development -p flytectldemo core.advanced.run_merge_sort.merge --archive + +Activating task named entity would be a noop as archiving is not possible. +:: + + flytectl update task -d development -p flytectldemo core.advanced.run_merge_sort.merge --activate + +Usage +` +) + +func updateTaskFunc(ctx context.Context, args []string, cmdCtx cmdCore.CommandContext) error { + project := config.GetConfig().Project + domain := config.GetConfig().Domain + if len(args) != 1 { + return fmt.Errorf(clierrors.ErrTaskNotPassed) + } + name := args[0] + err := namedEntityConfig.UpdateNamedEntity(ctx, name, project, domain, core.ResourceType_TASK, cmdCtx) + if err != nil { + fmt.Printf(clierrors.ErrFailedTaskUpdate, name, err) + return err + } + fmt.Printf("updated metadata successfully on %v", name) + return nil +} diff --git a/flytectl/cmd/update/task_test.go b/flytectl/cmd/update/task_test.go new file mode 100644 index 0000000000..250f7f9fde --- /dev/null +++ b/flytectl/cmd/update/task_test.go @@ -0,0 +1,44 @@ +package update + +import ( + "fmt" + "testing" + + "github.com/flyteorg/flytectl/cmd/testutils" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/admin" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func UpdateTaskSetup() { + ctx = testutils.Ctx + cmdCtx = testutils.CmdCtx + mockClient = testutils.MockClient +} + +func TestTaskUpdate(t *testing.T) { + testutils.Setup() + UpdateTaskSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{"task1"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(&admin.NamedEntityUpdateResponse{}, nil) + assert.Nil(t, updateTaskFunc(ctx, args, cmdCtx)) +} + +func TestTaskUpdateFail(t *testing.T) { + testutils.Setup() + UpdateWorkflowSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{"workflow1"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(nil, fmt.Errorf("failed to update")) + assert.NotNil(t, updateTaskFunc(ctx, args, cmdCtx)) +} + +func TestTaskUpdateInvalidArgs(t *testing.T) { + testutils.Setup() + UpdateWorkflowSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{} + assert.NotNil(t, updateTaskFunc(ctx, args, cmdCtx)) +} diff --git a/flytectl/cmd/update/update.go b/flytectl/cmd/update/update.go index 33526e6dcd..3f801ce08f 100644 --- a/flytectl/cmd/update/update.go +++ b/flytectl/cmd/update/update.go @@ -1,8 +1,7 @@ package update import ( - cmdcore "github.com/flyteorg/flytectl/cmd/core" - + cmdCore "github.com/flyteorg/flytectl/cmd/core" "github.com/spf13/cobra" ) @@ -27,13 +26,16 @@ func CreateUpdateCommand() *cobra.Command { Short: updateShort, Long: updatecmdLong, } - - updateResourcesFuncs := map[string]cmdcore.CommandEntry{ - "project": {CmdFunc: updateProjectsFunc, Aliases: []string{"projects"}, ProjectDomainNotRequired: true, PFlagProvider: projectConfig, - Short: projectShort, - Long: projectLong}, + updateResourcesFuncs := map[string]cmdCore.CommandEntry{ + "launchplan": {CmdFunc: updateLPFunc, Aliases: []string{}, ProjectDomainNotRequired: false, PFlagProvider: namedEntityConfig, + Short: updateLPShort, Long: updateLPLong}, + "project": {CmdFunc: updateProjectsFunc, Aliases: []string{}, ProjectDomainNotRequired: true, PFlagProvider: projectConfig, + Short: projectShort, Long: projectLong}, + "task": {CmdFunc: updateTaskFunc, Aliases: []string{}, ProjectDomainNotRequired: false, PFlagProvider: namedEntityConfig, + Short: updateTaskShort, Long: updateTaskLong}, + "workflow": {CmdFunc: updateWorkflowFunc, Aliases: []string{}, ProjectDomainNotRequired: false, PFlagProvider: namedEntityConfig, + Short: updateWorkflowShort, Long: updateWorkflowLong}, } - - cmdcore.AddCommands(updateCmd, updateResourcesFuncs) + cmdCore.AddCommands(updateCmd, updateResourcesFuncs) return updateCmd } diff --git a/flytectl/cmd/update/update_test.go b/flytectl/cmd/update/update_test.go index c54701f40a..9a610f9f5d 100644 --- a/flytectl/cmd/update/update_test.go +++ b/flytectl/cmd/update/update_test.go @@ -12,14 +12,20 @@ func TestUpdateCommand(t *testing.T) { assert.Equal(t, updateCommand.Use, updateUse) assert.Equal(t, updateCommand.Short, updateShort) assert.Equal(t, updateCommand.Long, updatecmdLong) - assert.Equal(t, len(updateCommand.Commands()), 1) + assert.Equal(t, len(updateCommand.Commands()), 4) cmdNouns := updateCommand.Commands() // Sort by Use value. sort.Slice(cmdNouns, func(i, j int) bool { return cmdNouns[i].Use < cmdNouns[j].Use }) - assert.Equal(t, cmdNouns[0].Use, "project") - assert.Equal(t, cmdNouns[0].Aliases, []string{"projects"}) - assert.Equal(t, cmdNouns[0].Short, projectShort) - assert.Equal(t, cmdNouns[0].Long, projectLong) + useArray := []string{"launchplan", "project", "task", "workflow"} + aliases := [][]string{{}, {}, {}, {}} + shortArray := []string{updateLPShort, projectShort, updateTaskShort, updateWorkflowShort} + longArray := []string{updateLPLong, projectLong, updateTaskLong, updateWorkflowLong} + for i := range cmdNouns { + assert.Equal(t, cmdNouns[i].Use, useArray[i]) + assert.Equal(t, cmdNouns[i].Aliases, aliases[i]) + assert.Equal(t, cmdNouns[i].Short, shortArray[i]) + assert.Equal(t, cmdNouns[i].Long, longArray[i]) + } } diff --git a/flytectl/cmd/update/workflow.go b/flytectl/cmd/update/workflow.go new file mode 100644 index 0000000000..9e67796208 --- /dev/null +++ b/flytectl/cmd/update/workflow.go @@ -0,0 +1,49 @@ +package update + +import ( + "context" + "fmt" + + "github.com/flyteorg/flytectl/clierrors" + "github.com/flyteorg/flytectl/cmd/config" + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" +) + +const ( + updateWorkflowShort = "Updates workflow metadata" + updateWorkflowLong = ` +Following command updates the description on the workflow. +:: + + flytectl update workflow -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --description "Mergesort workflow example" + +Archiving workflow named entity would cause this to disapper from flyteconsole UI. +:: + + flytectl update workflow -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --archive + +Activating workflow named entity +:: + + flytectl update workflow -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --activate + +Usage +` +) + +func updateWorkflowFunc(ctx context.Context, args []string, cmdCtx cmdCore.CommandContext) error { + project := config.GetConfig().Project + domain := config.GetConfig().Domain + if len(args) != 1 { + return fmt.Errorf(clierrors.ErrWorkflowNotPassed) + } + name := args[0] + err := namedEntityConfig.UpdateNamedEntity(ctx, name, project, domain, core.ResourceType_WORKFLOW, cmdCtx) + if err != nil { + fmt.Printf(clierrors.ErrFailedWorkflowUpdate, name, err) + return err + } + fmt.Printf("updated metadata successfully on %v", name) + return nil +} diff --git a/flytectl/cmd/update/workflow_test.go b/flytectl/cmd/update/workflow_test.go new file mode 100644 index 0000000000..d6b5ba06ad --- /dev/null +++ b/flytectl/cmd/update/workflow_test.go @@ -0,0 +1,44 @@ +package update + +import ( + "fmt" + "testing" + + "github.com/flyteorg/flytectl/cmd/testutils" + "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/admin" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func UpdateWorkflowSetup() { + ctx = testutils.Ctx + cmdCtx = testutils.CmdCtx + mockClient = testutils.MockClient +} + +func TestWorkflowUpdate(t *testing.T) { + testutils.Setup() + UpdateWorkflowSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{"workflow1"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(&admin.NamedEntityUpdateResponse{}, nil) + assert.Nil(t, updateWorkflowFunc(ctx, args, cmdCtx)) +} + +func TestWorkflowUpdateFail(t *testing.T) { + testutils.Setup() + UpdateWorkflowSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{"workflow1"} + mockClient.OnUpdateNamedEntityMatch(mock.Anything, mock.Anything).Return(nil, fmt.Errorf("failed to update")) + assert.NotNil(t, updateWorkflowFunc(ctx, args, cmdCtx)) +} + +func TestWorkflowUpdateInvalidArgs(t *testing.T) { + testutils.Setup() + UpdateWorkflowSetup() + namedEntityConfig = &NamedEntityConfig{} + args = []string{} + assert.NotNil(t, updateWorkflowFunc(ctx, args, cmdCtx)) +} diff --git a/flytectl/docs/source/gen/flytectl_update.rst b/flytectl/docs/source/gen/flytectl_update.rst index 0424744484..e3e03e6496 100644 --- a/flytectl/docs/source/gen/flytectl_update.rst +++ b/flytectl/docs/source/gen/flytectl_update.rst @@ -73,5 +73,8 @@ SEE ALSO ~~~~~~~~ * :doc:`flytectl` - flyetcl CLI tool +* :doc:`flytectl_update_launchplan` - Updates launch plan metadata * :doc:`flytectl_update_project` - Updates project resources +* :doc:`flytectl_update_task` - Updates task metadata +* :doc:`flytectl_update_workflow` - Updates launch plan metadata diff --git a/flytectl/docs/source/gen/flytectl_update_launchplan.rst b/flytectl/docs/source/gen/flytectl_update_launchplan.rst new file mode 100644 index 0000000000..a110317ff2 --- /dev/null +++ b/flytectl/docs/source/gen/flytectl_update_launchplan.rst @@ -0,0 +1,94 @@ +.. _flytectl_update_launchplan: + +flytectl update launchplan +-------------------------- + +Updates launch plan metadata + +Synopsis +~~~~~~~~ + + + +Updates launchplan metadata. +Following command updates the description on the launchplan. +:: + + flytectl update launchplan -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --description "Mergesort example" + +Archiving launchplan named entity is not supported and would throw an error. +:: + + flytectl update launchplan -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --archive + +Activating launchplan named entity would be a noop. +:: + + flytectl update launchplan -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --activate + +Usage + + +:: + + flytectl update launchplan [flags] + +Options +~~~~~~~ + +:: + + --activate Activates the named entity specified as argument. + --archive Archives the named entity specified as argument. + --description string description of the namedentity. + -h, --help help for launchplan + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --admin.authorizationHeader string Custom metadata header to pass JWT + --admin.authorizationServerUrl string This is the URL to your IDP's authorization server' + --admin.clientId string Client ID + --admin.clientSecretLocation string File containing the client secret + --admin.endpoint string For admin types, specify where the uri of the service is located. + --admin.insecure Use insecure connection. + --admin.maxBackoffDelay string Max delay for grpc backoff (default "8s") + --admin.maxRetries int Max number of gRPC retries (default 4) + --admin.perRetryTimeout string gRPC per retry timeout (default "15s") + --admin.scopes strings List of scopes to request + --admin.tokenUrl string Your IDPs token endpoint + --admin.useAuth Whether or not to try to authenticate with options below + --adminutils.batchSize int Maximum number of records to retrieve per call. (default 100) + --adminutils.maxRecords int Maximum number of records to retrieve. (default 500) + --config string config file (default is $HOME/config.yaml) + -d, --domain string Specifies the Flyte project's domain. + --logger.formatter.type string Sets logging format type. (default "json") + --logger.level int Sets the minimum logging level. (default 4) + --logger.mute Mutes all logs regardless of severity. Intended for benchmarks/tests only. + --logger.show-source Includes source code location in logs. + -o, --output string Specifies the output type - supported formats [TABLE JSON YAML] (default "TABLE") + -p, --project string Specifies the Flyte project. + --root.domain string Specified the domain to work on. + --root.output string Specified the output type. + --root.project string Specifies the project to work on. + --storage.cache.max_size_mbs int Maximum size of the cache where the Blob store data is cached in-memory. If not specified or set to 0, cache is not used + --storage.cache.target_gc_percent int Sets the garbage collection target percentage. + --storage.connection.access-key string Access key to use. Only required when authtype is set to accesskey. + --storage.connection.auth-type string Auth Type to use [iam, accesskey]. (default "iam") + --storage.connection.disable-ssl Disables SSL connection. Should only be used for development. + --storage.connection.endpoint string URL for storage client to connect to. + --storage.connection.region string Region to connect to. (default "us-east-1") + --storage.connection.secret-key string Secret to use when accesskey is set. + --storage.container string Initial container to create -if it doesn't exist-.' + --storage.defaultHttpClient.timeout string Sets time out on the http client. (default "0s") + --storage.enable-multicontainer If this is true, then the container argument is overlooked and redundant. This config will automatically open new connections to new containers/buckets as they are encountered + --storage.limits.maxDownloadMBs int Maximum allowed download size (in MBs) per call. (default 2) + --storage.type string Sets the type of storage to configure [s3/minio/local/mem/stow]. (default "s3") + +SEE ALSO +~~~~~~~~ + +* :doc:`flytectl_update` - Used for updating flyte resources eg: project. + diff --git a/flytectl/docs/source/gen/flytectl_update_task.rst b/flytectl/docs/source/gen/flytectl_update_task.rst new file mode 100644 index 0000000000..358537b9c3 --- /dev/null +++ b/flytectl/docs/source/gen/flytectl_update_task.rst @@ -0,0 +1,94 @@ +.. _flytectl_update_task: + +flytectl update task +-------------------- + +Updates task metadata + +Synopsis +~~~~~~~~ + + + +Updates task metadata. +Following command updates the description on the task. +:: + + flytectl update task -d development -p flytectldemo core.advanced.run_merge_sort.merge --description "Merge sort example" + +Archiving task named entity would is not supported and would throw an error. +:: + + flytectl update task -d development -p flytectldemo core.advanced.run_merge_sort.merge --archive + +Activating workflow named entity would be a noop as archiving is not possible. +:: + + flytectl update task -d development -p flytectldemo core.advanced.run_merge_sort.merge --activate + +Usage + + +:: + + flytectl update task [flags] + +Options +~~~~~~~ + +:: + + --activate Activates the named entity specified as argument. + --archive Archives the named entity specified as argument. + --description string description of the namedentity. + -h, --help help for task + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --admin.authorizationHeader string Custom metadata header to pass JWT + --admin.authorizationServerUrl string This is the URL to your IDP's authorization server' + --admin.clientId string Client ID + --admin.clientSecretLocation string File containing the client secret + --admin.endpoint string For admin types, specify where the uri of the service is located. + --admin.insecure Use insecure connection. + --admin.maxBackoffDelay string Max delay for grpc backoff (default "8s") + --admin.maxRetries int Max number of gRPC retries (default 4) + --admin.perRetryTimeout string gRPC per retry timeout (default "15s") + --admin.scopes strings List of scopes to request + --admin.tokenUrl string Your IDPs token endpoint + --admin.useAuth Whether or not to try to authenticate with options below + --adminutils.batchSize int Maximum number of records to retrieve per call. (default 100) + --adminutils.maxRecords int Maximum number of records to retrieve. (default 500) + --config string config file (default is $HOME/config.yaml) + -d, --domain string Specifies the Flyte project's domain. + --logger.formatter.type string Sets logging format type. (default "json") + --logger.level int Sets the minimum logging level. (default 4) + --logger.mute Mutes all logs regardless of severity. Intended for benchmarks/tests only. + --logger.show-source Includes source code location in logs. + -o, --output string Specifies the output type - supported formats [TABLE JSON YAML] (default "TABLE") + -p, --project string Specifies the Flyte project. + --root.domain string Specified the domain to work on. + --root.output string Specified the output type. + --root.project string Specifies the project to work on. + --storage.cache.max_size_mbs int Maximum size of the cache where the Blob store data is cached in-memory. If not specified or set to 0, cache is not used + --storage.cache.target_gc_percent int Sets the garbage collection target percentage. + --storage.connection.access-key string Access key to use. Only required when authtype is set to accesskey. + --storage.connection.auth-type string Auth Type to use [iam, accesskey]. (default "iam") + --storage.connection.disable-ssl Disables SSL connection. Should only be used for development. + --storage.connection.endpoint string URL for storage client to connect to. + --storage.connection.region string Region to connect to. (default "us-east-1") + --storage.connection.secret-key string Secret to use when accesskey is set. + --storage.container string Initial container to create -if it doesn't exist-.' + --storage.defaultHttpClient.timeout string Sets time out on the http client. (default "0s") + --storage.enable-multicontainer If this is true, then the container argument is overlooked and redundant. This config will automatically open new connections to new containers/buckets as they are encountered + --storage.limits.maxDownloadMBs int Maximum allowed download size (in MBs) per call. (default 2) + --storage.type string Sets the type of storage to configure [s3/minio/local/mem/stow]. (default "s3") + +SEE ALSO +~~~~~~~~ + +* :doc:`flytectl_update` - Used for updating flyte resources eg: project. + diff --git a/flytectl/docs/source/gen/flytectl_update_workflow.rst b/flytectl/docs/source/gen/flytectl_update_workflow.rst new file mode 100644 index 0000000000..2cdd0c21ea --- /dev/null +++ b/flytectl/docs/source/gen/flytectl_update_workflow.rst @@ -0,0 +1,94 @@ +.. _flytectl_update_workflow: + +flytectl update workflow +------------------------ + +Updates launch plan metadata + +Synopsis +~~~~~~~~ + + + +Updates workflow metadata. +Following command updates the description on the workflow. +:: + + flytectl update workflow -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --description "Mergesort workflow example" + +Archiving workflow named entity would cause this to disapper from flyteconsole UI. +:: + + flytectl update workflow -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --archive + +Activating workflow named entity would unarchive it. +:: + + flytectl update workflow -p flytectldemo -d development core.advanced.run_merge_sort.merge_sort --activate + +Usage + + +:: + + flytectl update workflow [flags] + +Options +~~~~~~~ + +:: + + --activate Activates the named entity specified as argument. + --archive Archives the named entity specified as argument. + --description string description of the namedentity. + -h, --help help for workflow + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --admin.authorizationHeader string Custom metadata header to pass JWT + --admin.authorizationServerUrl string This is the URL to your IDP's authorization server' + --admin.clientId string Client ID + --admin.clientSecretLocation string File containing the client secret + --admin.endpoint string For admin types, specify where the uri of the service is located. + --admin.insecure Use insecure connection. + --admin.maxBackoffDelay string Max delay for grpc backoff (default "8s") + --admin.maxRetries int Max number of gRPC retries (default 4) + --admin.perRetryTimeout string gRPC per retry timeout (default "15s") + --admin.scopes strings List of scopes to request + --admin.tokenUrl string Your IDPs token endpoint + --admin.useAuth Whether or not to try to authenticate with options below + --adminutils.batchSize int Maximum number of records to retrieve per call. (default 100) + --adminutils.maxRecords int Maximum number of records to retrieve. (default 500) + --config string config file (default is $HOME/config.yaml) + -d, --domain string Specifies the Flyte project's domain. + --logger.formatter.type string Sets logging format type. (default "json") + --logger.level int Sets the minimum logging level. (default 4) + --logger.mute Mutes all logs regardless of severity. Intended for benchmarks/tests only. + --logger.show-source Includes source code location in logs. + -o, --output string Specifies the output type - supported formats [TABLE JSON YAML] (default "TABLE") + -p, --project string Specifies the Flyte project. + --root.domain string Specified the domain to work on. + --root.output string Specified the output type. + --root.project string Specifies the project to work on. + --storage.cache.max_size_mbs int Maximum size of the cache where the Blob store data is cached in-memory. If not specified or set to 0, cache is not used + --storage.cache.target_gc_percent int Sets the garbage collection target percentage. + --storage.connection.access-key string Access key to use. Only required when authtype is set to accesskey. + --storage.connection.auth-type string Auth Type to use [iam, accesskey]. (default "iam") + --storage.connection.disable-ssl Disables SSL connection. Should only be used for development. + --storage.connection.endpoint string URL for storage client to connect to. + --storage.connection.region string Region to connect to. (default "us-east-1") + --storage.connection.secret-key string Secret to use when accesskey is set. + --storage.container string Initial container to create -if it doesn't exist-.' + --storage.defaultHttpClient.timeout string Sets time out on the http client. (default "0s") + --storage.enable-multicontainer If this is true, then the container argument is overlooked and redundant. This config will automatically open new connections to new containers/buckets as they are encountered + --storage.limits.maxDownloadMBs int Maximum allowed download size (in MBs) per call. (default 2) + --storage.type string Sets the type of storage to configure [s3/minio/local/mem/stow]. (default "s3") + +SEE ALSO +~~~~~~~~ + +* :doc:`flytectl_update` - Used for updating flyte resources eg: project. +