From 3ff80606f49e72116c7e79454eb0dbce50a223ea Mon Sep 17 00:00:00 2001 From: pmahindrakar-oss <77798312+pmahindrakar-oss@users.noreply.github.com> Date: Sun, 7 Feb 2021 20:43:43 +0530 Subject: [PATCH] Archive and Update project command (#24) --- flytectl/cmd/root.go | 2 + flytectl/cmd/update/project.go | 52 +++++++ flytectl/cmd/update/project_test.go | 130 ++++++++++++++++ flytectl/cmd/update/projectconfig_flags.go | 47 ++++++ .../cmd/update/projectconfig_flags_test.go | 146 ++++++++++++++++++ flytectl/cmd/update/update.go | 22 +++ flytectl/cmd/update/update_test.go | 21 +++ flytectl/docs/CONTRIBUTING.md | 7 +- 8 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 flytectl/cmd/update/project.go create mode 100644 flytectl/cmd/update/project_test.go create mode 100755 flytectl/cmd/update/projectconfig_flags.go create mode 100755 flytectl/cmd/update/projectconfig_flags_test.go create mode 100644 flytectl/cmd/update/update.go create mode 100644 flytectl/cmd/update/update_test.go diff --git a/flytectl/cmd/root.go b/flytectl/cmd/root.go index 39f5d25e8d..7dca669327 100644 --- a/flytectl/cmd/root.go +++ b/flytectl/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "github.com/lyft/flytectl/cmd/update" "github.com/lyft/flytectl/cmd/register" "github.com/lyft/flytectl/cmd/get" @@ -38,6 +39,7 @@ func newRootCmd() *cobra.Command { rootCmd.AddCommand(viper.GetConfigCommand()) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(get.CreateGetCommand()) + rootCmd.AddCommand(update.CreateUpdateCommand()) rootCmd.AddCommand(register.RegisterCommand()) config.GetConfig() diff --git a/flytectl/cmd/update/project.go b/flytectl/cmd/update/project.go new file mode 100644 index 0000000000..b00d91bf59 --- /dev/null +++ b/flytectl/cmd/update/project.go @@ -0,0 +1,52 @@ +package update + +import ( + "context" + "fmt" + "github.com/lyft/flytectl/cmd/config" + cmdCore "github.com/lyft/flytectl/cmd/core" + "github.com/lyft/flyteidl/gen/pb-go/flyteidl/admin" +) + +//go:generate pflags ProjectConfig + +// Config hold configuration for project update flags. +type ProjectConfig struct { + ActivateProject bool `json:"activateProject" pflag:",Activates the project specified as argument."` + ArchiveProject bool `json:"archiveProject" pflag:",Archives the project specified as argument."` +} + +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" +) + +func updateProjectsFunc(ctx context.Context, args []string, cmdCtx cmdCore.CommandContext) error { + id := config.GetConfig().Project + if id == "" { + fmt.Printf(errProjectNotFound, id) + return nil + } + archiveProject := projectConfig.ArchiveProject + activateProject := projectConfig.ActivateProject + if activateProject == archiveProject { + fmt.Printf(errInvalidUpdate) + return nil + } + projectState := admin.Project_ACTIVE + if archiveProject { + projectState = admin.Project_ARCHIVED + } + _, err := cmdCtx.AdminClient().UpdateProject(ctx, &admin.Project{ + Id: id, + State: projectState, + }) + if err != nil { + fmt.Printf(errFailedUpdate, id, projectState, err) + return nil + } + 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 new file mode 100644 index 0000000000..cf9fe7678b --- /dev/null +++ b/flytectl/cmd/update/project_test.go @@ -0,0 +1,130 @@ +package update + +import ( + "bytes" + "context" + "errors" + "github.com/lyft/flytectl/cmd/config" + cmdCore "github.com/lyft/flytectl/cmd/core" + "github.com/lyft/flyteidl/clients/go/admin/mocks" + "github.com/lyft/flyteidl/gen/pb-go/flyteidl/admin" + "github.com/stretchr/testify/assert" + "io" + "log" + "os" + "testing" +) + +const projectValue = "dummyProject" + +var ( + reader *os.File + writer *os.File + err error + ctx context.Context + mockClient *mocks.AdminServiceClient + mockOutStream io.Writer + args []string + cmdCtx cmdCore.CommandContext + projectUpdateRequest *admin.Project + stdOut *os.File + stderr *os.File +) + +func setup() { + reader, writer, err = os.Pipe() + if err != nil { + panic(err) + } + stdOut = os.Stdout + stderr = os.Stderr + os.Stdout = writer + os.Stderr = writer + log.SetOutput(writer) + config.GetConfig().Project = projectValue + mockClient = new(mocks.AdminServiceClient) + mockOutStream = writer + cmdCtx = cmdCore.NewCommandContext(mockClient, mockOutStream) + projectUpdateRequest = &admin.Project{ + Id: projectValue, + State: admin.Project_ACTIVE, + } +} + +func teardownAndVerify(t *testing.T, expectedLog string) { + writer.Close() + os.Stdout = stdOut + os.Stderr = stderr + var buf bytes.Buffer + io.Copy(&buf, reader) + assert.Equal(t, expectedLog, buf.String()) +} + +func modifyProjectFlags(archiveProject *bool, newArchiveVal bool, activateProject *bool, newActivateVal bool) { + *archiveProject = newArchiveVal + *activateProject = newActivateVal +} + +func TestActivateProjectFunc(t *testing.T) { + setup() + defer teardownAndVerify(t, "Project dummyProject updated to ACTIVE state\n") + modifyProjectFlags(&(projectConfig.ArchiveProject), false, &(projectConfig.ActivateProject), true) + mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, nil) + updateProjectsFunc(ctx, args, cmdCtx) + mockClient.AssertCalled(t, "UpdateProject", ctx, projectUpdateRequest) +} + +func TestActivateProjectFuncWithError(t *testing.T) { + setup() + defer teardownAndVerify(t, "Project dummyProject failed to get updated to ACTIVE state due to Error Updating Project\n") + modifyProjectFlags(&(projectConfig.ArchiveProject), false, &(projectConfig.ActivateProject), true) + mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, errors.New("Error Updating Project")) + updateProjectsFunc(ctx, args, cmdCtx) + mockClient.AssertCalled(t, "UpdateProject", ctx, projectUpdateRequest) +} + +func TestArchiveProjectFunc(t *testing.T) { + setup() + defer teardownAndVerify(t, "Project dummyProject updated to ARCHIVED state\n") + modifyProjectFlags(&(projectConfig.ArchiveProject), true, &(projectConfig.ActivateProject), false) + projectUpdateRequest := &admin.Project{ + Id: projectValue, + State: admin.Project_ARCHIVED, + } + mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, nil) + err := updateProjectsFunc(ctx, args, cmdCtx) + assert.Nil(t, err) + mockClient.AssertCalled(t, "UpdateProject", ctx, projectUpdateRequest) +} + +func TestArchiveProjectFuncWithError(t *testing.T) { + setup() + defer teardownAndVerify(t, "Project dummyProject failed to get updated to ARCHIVED state due to Error Updating Project\n") + modifyProjectFlags(&(projectConfig.ArchiveProject), true, &(projectConfig.ActivateProject), false) + projectUpdateRequest := &admin.Project{ + Id: projectValue, + State: admin.Project_ARCHIVED, + } + mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, errors.New("Error Updating Project")) + updateProjectsFunc(ctx, args, cmdCtx) + mockClient.AssertCalled(t, "UpdateProject", ctx, projectUpdateRequest) +} + +func TestEmptyProjectInput(t *testing.T) { + setup() + defer teardownAndVerify(t, "Project not found\n") + config.GetConfig().Project = "" + modifyProjectFlags(&(projectConfig.ArchiveProject), false, &(projectConfig.ActivateProject), true) + mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, nil) + updateProjectsFunc(ctx, args, cmdCtx) + mockClient.AssertNotCalled(t, "UpdateProject", ctx, projectUpdateRequest) +} + +func TestInvalidInput(t *testing.T) { + setup() + defer teardownAndVerify(t, "Invalid state passed. Specify either activate or archive\n") + modifyProjectFlags(&(projectConfig.ArchiveProject), false, &(projectConfig.ActivateProject), false) + mockClient.OnUpdateProjectMatch(ctx, projectUpdateRequest).Return(nil, nil) + updateProjectsFunc(ctx, args, cmdCtx) + mockClient.AssertNotCalled(t, "UpdateProject", ctx, projectUpdateRequest) +} diff --git a/flytectl/cmd/update/projectconfig_flags.go b/flytectl/cmd/update/projectconfig_flags.go new file mode 100755 index 0000000000..e214a4fe1f --- /dev/null +++ b/flytectl/cmd/update/projectconfig_flags.go @@ -0,0 +1,47 @@ +// 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 (ProjectConfig) 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 (ProjectConfig) 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 ProjectConfig and its nested types. The format of the +// flags is json-name.json-sub-name... etc. +func (cfg ProjectConfig) GetPFlagSet(prefix string) *pflag.FlagSet { + cmdFlags := pflag.NewFlagSet("ProjectConfig", pflag.ExitOnError) + cmdFlags.BoolVarP(&(projectConfig.ActivateProject), fmt.Sprintf("%v%v", prefix, "activateProject"),"t", *new(bool), "Activates the project specified as argument.") + cmdFlags.BoolVarP(&(projectConfig.ArchiveProject), fmt.Sprintf("%v%v", prefix, "archiveProject"), "a", *new(bool), "Archives the project specified as argument.") + return cmdFlags +} diff --git a/flytectl/cmd/update/projectconfig_flags_test.go b/flytectl/cmd/update/projectconfig_flags_test.go new file mode 100755 index 0000000000..4a13ad3aff --- /dev/null +++ b/flytectl/cmd/update/projectconfig_flags_test.go @@ -0,0 +1,146 @@ +// 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 dereferencableKindsProjectConfig = 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 canGetElementProjectConfig(t reflect.Kind) bool { + _, exists := dereferencableKindsProjectConfig[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 jsonUnmarshalerHookProjectConfig(_, to reflect.Type, data interface{}) (interface{}, error) { + unmarshalerType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() + if to.Implements(unmarshalerType) || reflect.PtrTo(to).Implements(unmarshalerType) || + (canGetElementProjectConfig(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_ProjectConfig(input, result interface{}) error { + config := &mapstructure.DecoderConfig{ + TagName: "json", + WeaklyTypedInput: true, + Result: result, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + jsonUnmarshalerHookProjectConfig, + ), + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +func join_ProjectConfig(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_ProjectConfig(t *testing.T, val, result interface{}) { + assert.NoError(t, decode_ProjectConfig(val, result)) +} + +func testDecodeSlice_ProjectConfig(t *testing.T, vStringSlice, result interface{}) { + assert.NoError(t, decode_ProjectConfig(vStringSlice, result)) +} + +func TestProjectConfig_GetPFlagSet(t *testing.T) { + val := ProjectConfig{} + cmdFlags := val.GetPFlagSet("") + assert.True(t, cmdFlags.HasFlags()) +} + +func TestProjectConfig_SetFlags(t *testing.T) { + actual := ProjectConfig{} + cmdFlags := actual.GetPFlagSet("") + assert.True(t, cmdFlags.HasFlags()) + + t.Run("Test_activateProject", func(t *testing.T) { + t.Run("DefaultValue", func(t *testing.T) { + // Test that default value is set properly + if vBool, err := cmdFlags.GetBool("activateProject"); err == nil { + assert.Equal(t, bool(*new(bool)), vBool) + } else { + assert.FailNow(t, err.Error()) + } + }) + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("activateProject", testValue) + if vBool, err := cmdFlags.GetBool("activateProject"); err == nil { + testDecodeJson_ProjectConfig(t, fmt.Sprintf("%v", vBool), &actual.ActivateProject) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) + t.Run("Test_archiveProject", func(t *testing.T) { + t.Run("DefaultValue", func(t *testing.T) { + // Test that default value is set properly + if vBool, err := cmdFlags.GetBool("archiveProject"); err == nil { + assert.Equal(t, bool(*new(bool)), vBool) + } else { + assert.FailNow(t, err.Error()) + } + }) + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("archiveProject", testValue) + if vBool, err := cmdFlags.GetBool("archiveProject"); err == nil { + testDecodeJson_ProjectConfig(t, fmt.Sprintf("%v", vBool), &actual.ArchiveProject) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) +} diff --git a/flytectl/cmd/update/update.go b/flytectl/cmd/update/update.go new file mode 100644 index 0000000000..ac83b69200 --- /dev/null +++ b/flytectl/cmd/update/update.go @@ -0,0 +1,22 @@ +package update + +import ( + cmdcore "github.com/lyft/flytectl/cmd/core" + + "github.com/spf13/cobra" +) + +// CreateUpdateCommand will return update command +func CreateUpdateCommand() *cobra.Command { + updateCmd := &cobra.Command{ + Use: "update", + Short: "Update various resources.", + } + + updateResourcesFuncs := map[string]cmdcore.CommandEntry{ + "project": {CmdFunc: updateProjectsFunc, Aliases: []string{"projects"}, ProjectDomainNotRequired: true, PFlagProvider: projectConfig}, + } + + cmdcore.AddCommands(updateCmd, updateResourcesFuncs) + return updateCmd +} diff --git a/flytectl/cmd/update/update_test.go b/flytectl/cmd/update/update_test.go new file mode 100644 index 0000000000..749d00a6a0 --- /dev/null +++ b/flytectl/cmd/update/update_test.go @@ -0,0 +1,21 @@ +package update + +import ( + "github.com/stretchr/testify/assert" + "sort" + "testing" +) + +func TestUpdateCommand(t *testing.T) { + updateCommand := CreateUpdateCommand() + assert.Equal(t, updateCommand.Use , "update") + assert.Equal(t, updateCommand.Short , "Update various resources.") + assert.Equal(t, len(updateCommand.Commands()), 1) + 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"}) +} diff --git a/flytectl/docs/CONTRIBUTING.md b/flytectl/docs/CONTRIBUTING.md index 0249359719..391bee6d82 100644 --- a/flytectl/docs/CONTRIBUTING.md +++ b/flytectl/docs/CONTRIBUTING.md @@ -4,12 +4,13 @@ A local cluster can be setup via --> https://lyft.github.io/flyte/administrator/ Then, if having trouble connecting to local cluster see the following: + #1) Find/Set/Verify gRPC port for your local Flyte service: FLYTECTL_GRPC_PORT=`kubectl get service -n flyte flyteadmin -o json | jq '.spec.ports[] | select(.name=="grpc").port'` -#2) Setup Port forwarding: kubectl port-forward -n flyte service/flyteadmin 8081:$FLYTECTL_GRPC_PORT - -and #3) Update config line in https://github.com/lyft/flytectl/blob/master/config.yaml to dns:///localhost:8081 +#2) Setup Port forwarding: kubectl port-forward -n flyte service/flyteadmin 8081:$FLYTECTL_GRPC_PORT +#3) Update config line in https://github.com/lyft/flytectl/blob/master/config.yaml to dns:///localhost:8081 +#4) All new flags introduced for flytectl commands and subcommands should be camelcased. eg: bin/flytectl update project -p flytesnacks --activateProject