From 97f1a59efc84eb6e195a1e06b662775543846706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Tue, 30 Jan 2024 16:27:59 +0000 Subject: [PATCH 01/11] Onboard OpenSearch service --- go.mod | 1 + go.sum | 2 + internal/cmd/config/set/set.go | 4 + internal/cmd/config/unset/unset.go | 7 + internal/cmd/config/unset/unset_test.go | 3 + .../opensearch/credentials/create/create.go | 119 +++++ .../credentials/create/create_test.go | 189 +++++++ .../cmd/opensearch/credentials/credentials.go | 31 ++ .../opensearch/credentials/delete/delete.go | 115 +++++ .../credentials/delete/delete_test.go | 242 +++++++++ .../credentials/describe/describe.go | 127 +++++ .../credentials/describe/describe_test.go | 242 +++++++++ .../cmd/opensearch/credentials/list/list.go | 147 ++++++ .../opensearch/credentials/list/list_test.go | 206 ++++++++ .../cmd/opensearch/instance/create/create.go | 253 +++++++++ .../opensearch/instance/create/create_test.go | 484 +++++++++++++++++ .../cmd/opensearch/instance/delete/delete.go | 114 ++++ .../opensearch/instance/delete/delete_test.go | 215 ++++++++ .../opensearch/instance/describe/describe.go | 113 ++++ .../instance/describe/describe_test.go | 215 ++++++++ internal/cmd/opensearch/instance/instance.go | 33 ++ internal/cmd/opensearch/instance/list/list.go | 142 +++++ .../cmd/opensearch/instance/list/list_test.go | 185 +++++++ .../cmd/opensearch/instance/update/update.go | 264 ++++++++++ .../opensearch/instance/update/update_test.go | 485 ++++++++++++++++++ internal/cmd/opensearch/opensearch.go | 29 ++ internal/cmd/opensearch/plans/plans.go | 147 ++++++ internal/cmd/opensearch/plans/plans_test.go | 185 +++++++ internal/cmd/root.go | 8 +- internal/pkg/config/config.go | 3 + .../pkg/services/opensearch/client/client.go | 37 ++ .../pkg/services/opensearch/utils/utils.go | 82 +++ .../services/opensearch/utils/utils_test.go | 144 ++++++ 33 files changed, 4570 insertions(+), 3 deletions(-) create mode 100644 internal/cmd/opensearch/credentials/create/create.go create mode 100644 internal/cmd/opensearch/credentials/create/create_test.go create mode 100644 internal/cmd/opensearch/credentials/credentials.go create mode 100644 internal/cmd/opensearch/credentials/delete/delete.go create mode 100644 internal/cmd/opensearch/credentials/delete/delete_test.go create mode 100644 internal/cmd/opensearch/credentials/describe/describe.go create mode 100644 internal/cmd/opensearch/credentials/describe/describe_test.go create mode 100644 internal/cmd/opensearch/credentials/list/list.go create mode 100644 internal/cmd/opensearch/credentials/list/list_test.go create mode 100644 internal/cmd/opensearch/instance/create/create.go create mode 100644 internal/cmd/opensearch/instance/create/create_test.go create mode 100644 internal/cmd/opensearch/instance/delete/delete.go create mode 100644 internal/cmd/opensearch/instance/delete/delete_test.go create mode 100644 internal/cmd/opensearch/instance/describe/describe.go create mode 100644 internal/cmd/opensearch/instance/describe/describe_test.go create mode 100644 internal/cmd/opensearch/instance/instance.go create mode 100644 internal/cmd/opensearch/instance/list/list.go create mode 100644 internal/cmd/opensearch/instance/list/list_test.go create mode 100644 internal/cmd/opensearch/instance/update/update.go create mode 100644 internal/cmd/opensearch/instance/update/update_test.go create mode 100644 internal/cmd/opensearch/opensearch.go create mode 100644 internal/cmd/opensearch/plans/plans.go create mode 100644 internal/cmd/opensearch/plans/plans_test.go create mode 100644 internal/pkg/services/opensearch/client/client.go create mode 100644 internal/pkg/services/opensearch/utils/utils.go create mode 100644 internal/pkg/services/opensearch/utils/utils_test.go diff --git a/go.mod b/go.mod index 84df72b58..a15ed64bd 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.2 github.com/stackitcloud/stackit-sdk-go/services/membership v0.3.4 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.10.3 + github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.9.2 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.7.5 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.4 github.com/stackitcloud/stackit-sdk-go/services/ske v0.9.2 diff --git a/go.sum b/go.sum index a93973d5e..607a95a6a 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,8 @@ github.com/stackitcloud/stackit-sdk-go/services/membership v0.3.4 h1:0OT/UBP55/G github.com/stackitcloud/stackit-sdk-go/services/membership v0.3.4/go.mod h1:6ovfcQJ96ivkBpSI933lVl2a/SWprpVGoK6YNKycLps= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.10.3 h1:M7ALIg1tE8MFLLw9Um0iyvdBgIhl83tJ0sWRjP7YqMM= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.10.3/go.mod h1:LWfUBjGQWF3SZivQdUdAC/WxJkx8ImJKy5GFMV3tXHY= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.9.2 h1:dwZ1NDD+AxTaZqAeR/0PY7yt32dbABhQH1Vsnt8A+hg= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.9.2/go.mod h1:M8mjTS5yR0XXoH9EpuULme9fEkLhUz4UOT7XSHUSRQ8= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.7.5 h1:Gu0z8MpErzBHxb9xx8B/4DduxckDmBRPWNaeoVcE8cQ= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.7.5/go.mod h1:MQ5eGWFmnDf9wUArqZ2g+nwJgMDkYDQUkoRVutaHrms= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.4 h1:XNL7bk5mwCovV8a3oIIC9PlNpPTUG3XNwdRqHS5V2no= diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 5c463443f..5e6c3fca5 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -23,6 +23,7 @@ const ( serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" + OpenSearchCustomEndpointFlag = "opensearch-custom-endpoint" ) type inputModel struct { @@ -86,6 +87,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account custom endpoint") cmd.Flags().String(skeCustomEndpointFlag, "", "SKE custom endpoint") cmd.Flags().String(resourceManagerCustomEndpointFlag, "", "Resource manager custom endpoint") + cmd.Flags().String(OpenSearchCustomEndpointFlag, "", "OpenSearch custom endpoint") err := viper.BindPFlag(config.DNSCustomEndpointKey, cmd.Flags().Lookup(dnsCustomEndpointFlag)) cobra.CheckErr(err) @@ -99,6 +101,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.ResourceManagerEndpointKey, cmd.Flags().Lookup(skeCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.OpenSearchCustomEndpointKey, cmd.Flags().Lookup(OpenSearchCustomEndpointFlag)) + cobra.CheckErr(err) } func parseInput(cmd *cobra.Command) (*inputModel, error) { diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index a635b63d4..2daa9fad6 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -24,6 +24,7 @@ const ( serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" + openSearchCustomEndpointFlag = "opensearch-custom-endpoint" ) type inputModel struct { @@ -37,6 +38,7 @@ type inputModel struct { ServiceAccountCustomEndpoint bool SKECustomEndpoint bool ResourceManagerCustomEndpoint bool + OpenSearchCustomEndpoint bool } func NewCmd() *cobra.Command { @@ -87,6 +89,9 @@ func NewCmd() *cobra.Command { if model.ResourceManagerCustomEndpoint { viper.Set(config.ResourceManagerEndpointKey, "") } + if model.OpenSearchCustomEndpoint { + viper.Set(config.OpenSearchCustomEndpointKey, "") + } err := viper.WriteConfig() if err != nil { @@ -110,6 +115,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(serviceAccountCustomEndpointFlag, false, "SKE custom endpoint") cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE custom endpoint") cmd.Flags().Bool(resourceManagerCustomEndpointFlag, false, "Resource Manager custom endpoint") + cmd.Flags().Bool(openSearchCustomEndpointFlag, false, "OpenSearch custom endpoint") } func parseInput(cmd *cobra.Command) *inputModel { @@ -124,5 +130,6 @@ func parseInput(cmd *cobra.Command) *inputModel { ServiceAccountCustomEndpoint: flags.FlagToBoolValue(cmd, serviceAccountCustomEndpointFlag), SKECustomEndpoint: flags.FlagToBoolValue(cmd, skeCustomEndpointFlag), ResourceManagerCustomEndpoint: flags.FlagToBoolValue(cmd, resourceManagerCustomEndpointFlag), + OpenSearchCustomEndpoint: flags.FlagToBoolValue(cmd, openSearchCustomEndpointFlag), } } diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 383420626..bb1902414 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -15,6 +15,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool serviceAccountCustomEndpointFlag: true, skeCustomEndpointFlag: true, resourceManagerCustomEndpointFlag: true, + openSearchCustomEndpointFlag: true, } for _, mod := range mods { mod(flagValues) @@ -30,6 +31,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ServiceAccountCustomEndpoint: true, SKECustomEndpoint: true, ResourceManagerCustomEndpoint: true, + OpenSearchCustomEndpoint: true, } for _, mod := range mods { mod(model) @@ -61,6 +63,7 @@ func TestParseInput(t *testing.T) { model.ServiceAccountCustomEndpoint = false model.SKECustomEndpoint = false model.ResourceManagerCustomEndpoint = false + model.OpenSearchCustomEndpoint = false }), }, { diff --git a/internal/cmd/opensearch/credentials/create/create.go b/internal/cmd/opensearch/credentials/create/create.go new file mode 100644 index 000000000..14ab59c72 --- /dev/null +++ b/internal/cmd/opensearch/credentials/create/create.go @@ -0,0 +1,119 @@ +package create + +import ( + "context" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/confirm" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + instanceIdFlag = "instance-id" + hidePasswordFlag = "hide-password" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + HidePassword bool +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create credentials for an OpenSearch instance", + Long: "Create credentials (username and password) for an OpenSearch instance", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create credentials for an OpenSearch instance`, + "$ stackit opensearch credentials create --instance-id xxx"), + examples.NewExample( + `Create credentials for an OpenSearch instance and hide the password in the output`, + "$ stackit opensearch credentials create --instance-id xxx --hide-password"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + instanceLabel = model.InstanceId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a credential for instance %s?", instanceLabel) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create OpenSearch credentials: %w", err) + } + + cmd.Printf("Created credential for instance %s. Credential ID: %s\n\n", instanceLabel, *resp.Id) + cmd.Printf("Username: %s\n", *resp.Raw.Credentials.Username) + if model.HidePassword { + cmd.Printf("Password: \n") + } else { + cmd.Printf("Password: %s\n", *resp.Raw.Credentials.Password) + } + cmd.Printf("Host: %s\n", *resp.Raw.Credentials.Host) + cmd.Printf("Port: %d\n", *resp.Raw.Credentials.Port) + cmd.Printf("URI: %s\n", *resp.Uri) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().Bool(hidePasswordFlag, false, "Hide password in output") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(cmd, instanceIdFlag), + HidePassword: flags.FlagToBoolValue(cmd, hidePasswordFlag), + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiCreateCredentialsRequest { + req := apiClient.CreateCredentials(ctx, model.ProjectId, model.InstanceId) + return req +} diff --git a/internal/cmd/opensearch/credentials/create/create_test.go b/internal/cmd/opensearch/credentials/create/create_test.go new file mode 100644 index 000000000..429d8c474 --- /dev/null +++ b/internal/cmd/opensearch/credentials/create/create_test.go @@ -0,0 +1,189 @@ +package create + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiCreateCredentialsRequest)) opensearch.ApiCreateCredentialsRequest { + request := testClient.CreateCredentials(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiCreateCredentialsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/credentials/credentials.go b/internal/cmd/opensearch/credentials/credentials.go new file mode 100644 index 000000000..966119715 --- /dev/null +++ b/internal/cmd/opensearch/credentials/credentials.go @@ -0,0 +1,31 @@ +package credentials + +import ( + "stackit/internal/cmd/opensearch/credentials/create" + "stackit/internal/cmd/opensearch/credentials/delete" + "stackit/internal/cmd/opensearch/credentials/describe" + "stackit/internal/cmd/opensearch/credentials/list" + "stackit/internal/pkg/args" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "credentials", + Short: "Provides functionality for OpenSearch credentials", + Long: "Provides functionality for OpenSearch credentials", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd) + return cmd +} + +func addSubcommands(cmd *cobra.Command) { + cmd.AddCommand(create.NewCmd()) + cmd.AddCommand(delete.NewCmd()) + cmd.AddCommand(describe.NewCmd()) + cmd.AddCommand(list.NewCmd()) +} diff --git a/internal/cmd/opensearch/credentials/delete/delete.go b/internal/cmd/opensearch/credentials/delete/delete.go new file mode 100644 index 000000000..505c6bd17 --- /dev/null +++ b/internal/cmd/opensearch/credentials/delete/delete.go @@ -0,0 +1,115 @@ +package delete + +import ( + "context" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/confirm" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + credentialsIdArg = "CREDENTIAL_ID" //nolint:gosec // linter false positive + + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + CredentialsId string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", credentialsIdArg), + Short: "Delete credentials of an OpenSearch instance", + Long: "Delete credentials of an OpenSearch instance", + Args: args.SingleArg(credentialsIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete credentials with ID "xxx" of OpenSearch instance with ID "yyy"`, + "$ stackit opensearch credentials delete xxx --instance-id yyy"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + instanceLabel = model.InstanceId + } + + credentialLabel, err := opensearchUtils.GetCredentialUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + if err != nil { + credentialLabel = model.CredentialsId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete credential %s of instance %s? (This cannot be undone)", credentialLabel, instanceLabel) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete OpenSearch credentials: %w", err) + } + + cmd.Printf("Deleted credential %s of instance %s\n", credentialLabel, instanceLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + credentialsId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(cmd, instanceIdFlag), + CredentialsId: credentialsId, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiDeleteCredentialsRequest { + req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + return req +} diff --git a/internal/cmd/opensearch/credentials/delete/delete_test.go b/internal/cmd/opensearch/credentials/delete/delete_test.go new file mode 100644 index 000000000..41f627317 --- /dev/null +++ b/internal/cmd/opensearch/credentials/delete/delete_test.go @@ -0,0 +1,242 @@ +package delete + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testCredentialsId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testCredentialsId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + CredentialsId: testCredentialsId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiDeleteCredentialsRequest)) opensearch.ApiDeleteCredentialsRequest { + request := testClient.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "credentail id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "credential id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiDeleteCredentialsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/credentials/describe/describe.go b/internal/cmd/opensearch/credentials/describe/describe.go new file mode 100644 index 000000000..8fb7b3295 --- /dev/null +++ b/internal/cmd/opensearch/credentials/describe/describe.go @@ -0,0 +1,127 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + "stackit/internal/pkg/tables" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + credentialsIdArg = "CREDENTIAL_ID" //nolint:gosec // linter false positive + + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + CredentialsId string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", credentialsIdArg), + Short: "Get details of credentials of an OpenSearch instance", + Long: "Get details of credentials of an OpenSearch instance. The password will be shown in plain text in the output.", + Args: args.SingleArg(credentialsIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of credentials of an OpenSearch instance with ID "xxx" from instance with ID "yyy"`, + "$ stackit opensearch credentials describe xxx --instance-id yyy"), + examples.NewExample( + `Get details of credentials of an OpenSearch instance with ID "xxx" from instance with ID "yyy" in a table format`, + "$ stackit opensearch credentials describe xxx --instance-id yyy --output-format pretty"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe OpenSearch credentials: %w", err) + } + + return outputResult(cmd, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + credentialsId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(cmd, instanceIdFlag), + CredentialsId: credentialsId, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiGetCredentialsRequest { + req := apiClient.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + return req +} + +func outputResult(cmd *cobra.Command, outputFormat string, credential *opensearch.CredentialsResponse) error { + switch outputFormat { + case globalflags.PrettyOutputFormat: + table := tables.NewTable() + table.AddRow("ID", *credential.Id) + table.AddSeparator() + table.AddRow("USERNAME", *credential.Raw.Credentials.Username) + table.AddSeparator() + table.AddRow("PASSWORD", *credential.Raw.Credentials.Password) + table.AddSeparator() + table.AddRow("URI", *credential.Raw.Credentials.Uri) + err := table.Display(cmd) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + default: + details, err := json.MarshalIndent(credential, "", " ") + if err != nil { + return fmt.Errorf("marshal OpenSearch credentials: %w", err) + } + cmd.Println(string(details)) + + return nil + } +} diff --git a/internal/cmd/opensearch/credentials/describe/describe_test.go b/internal/cmd/opensearch/credentials/describe/describe_test.go new file mode 100644 index 000000000..52eec1fae --- /dev/null +++ b/internal/cmd/opensearch/credentials/describe/describe_test.go @@ -0,0 +1,242 @@ +package describe + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testCredentialsId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testCredentialsId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + CredentialsId: testCredentialsId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiGetCredentialsRequest)) opensearch.ApiGetCredentialsRequest { + request := testClient.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "credentail id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "credential id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiGetCredentialsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/credentials/list/list.go b/internal/cmd/opensearch/credentials/list/list.go new file mode 100644 index 000000000..f3d0c008a --- /dev/null +++ b/internal/cmd/opensearch/credentials/list/list.go @@ -0,0 +1,147 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + "stackit/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + instanceIdFlag = "instance-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + Limit *int64 +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all credentials' IDs for an OpenSearch instance", + Long: "List all credentials' IDs for an OpenSearch instance", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all credentials' IDs for an OpenSearch instance`, + "$ stackit opensearch credentials list --instance-id xxx"), + examples.NewExample( + `List all credentials' IDs for an OpenSearch instance in JSON format`, + "$ stackit opensearch credentials list --instance-id xxx --output-format json"), + examples.NewExample( + `List up to 10 credentials' IDs for an OpenSearch instance`, + "$ stackit opensearch credentials list --instance-id xxx --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list OpenSearch credentialss: %w", err) + } + credentials := *resp.CredentialsList + if len(credentials) == 0 { + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + instanceLabel = model.InstanceId + } + cmd.Printf("No credentials found for instance %s\n", instanceLabel) + return nil + } + + // Truncate output + if model.Limit != nil && len(credentials) > int(*model.Limit) { + credentials = credentials[:*model.Limit] + } + return outputResult(cmd, model.OutputFormat, credentials) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(cmd, instanceIdFlag), + Limit: limit, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiListCredentialsRequest { + req := apiClient.ListCredentials(ctx, model.ProjectId, model.InstanceId) + return req +} + +func outputResult(cmd *cobra.Command, outputFormat string, credentials []opensearch.CredentialsListItem) error { + switch outputFormat { + case globalflags.JSONOutputFormat: + details, err := json.MarshalIndent(credentials, "", " ") + if err != nil { + return fmt.Errorf("marshal OpenSearch credentials list: %w", err) + } + cmd.Println(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID") + for i := range credentials { + c := credentials[i] + table.AddRow(*c.Id) + } + err := table.Display(cmd) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/opensearch/credentials/list/list_test.go b/internal/cmd/opensearch/credentials/list/list_test.go new file mode 100644 index 000000000..a9ce8bf6d --- /dev/null +++ b/internal/cmd/opensearch/credentials/list/list_test.go @@ -0,0 +1,206 @@ +package list + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiListCredentialsRequest)) opensearch.ApiListCredentialsRequest { + request := testClient.ListCredentials(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiListCredentialsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go new file mode 100644 index 000000000..18428a540 --- /dev/null +++ b/internal/cmd/opensearch/instance/create/create.go @@ -0,0 +1,253 @@ +package create + +import ( + "context" + "errors" + "fmt" + "strings" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/confirm" + cliErr "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/projectname" + "stackit/internal/pkg/services/opensearch/client" + opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + "stackit/internal/pkg/spinner" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" +) + +const ( + instanceNameFlag = "name" + enableMonitoringFlag = "enable-monitoring" + graphiteFlag = "graphite" + metricsFrequencyFlag = "metrics-frequency" + metricsPrefixFlag = "metrics-prefix" + monitoringInstanceIdFlag = "monitoring-instance-id" + pluginFlag = "plugin" + sgwAclFlag = "acl" + syslogFlag = "syslog" + planIdFlag = "plan-id" + planNameFlag = "plan-name" + versionFlag = "version" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + PlanName string + Version string + + InstanceName *string + EnableMonitoring *bool + Graphite *string + MetricsFrequency *int64 + MetricsPrefix *string + MonitoringInstanceId *string + Plugin *[]string + SgwAcl *[]string + Syslog *[]string + PlanId *string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create an OpenSearch instance", + Long: "Create an OpenSearch instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create an OpenSearch instance with name "my-instance" and specify plan by name and version`, + "$ stackit opensearch instance create --name my-instance --plan-name stackit-qa-opensearch-1.2.10-replica --version 2"), + examples.NewExample( + `Create an OpenSearch instance with name "my-instance" and specify plan by ID`, + "$ stackit opensearch instance create --name my-instance --plan-id xxx"), + examples.NewExample( + `Create an OpenSearch instance with name "my-instance" and specify IP range which is allowed to access it`, + "$ stackit opensearch instance create --name my-instance --plan-id xxx --acl 192.168.1.0/24"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + // Service name and operation needed for error handling + service := strings.Split(cmd.Parent().Parent().Use, " ")[0] + operation := cmd.Use + model, err := parseInput(cmd, service, operation) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, cmd) + if err != nil { + projectLabel = model.ProjectId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create an OpenSearch instance for project %s?", projectLabel) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, service, model, apiClient) + if err != nil { + var dsaInvalidPlanError *cliErr.DSAInvalidPlanError + if !errors.As(err, &dsaInvalidPlanError) { + return fmt.Errorf("build OpenSearch instance creation request: %w", err) + } + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create OpenSearch instance: %w", err) + } + instanceId := *resp.InstanceId + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Creating instance") + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for OpenSearch instance creation: %w", err) + } + s.Stop() + } + + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + cmd.Printf("%s instance for project %s. Instance ID: %s\n", operationState, projectLabel, instanceId) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") + cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") + cmd.Flags().String(graphiteFlag, "", "Graphite host") + cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") + cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") + cmd.Flags().StringSlice(pluginFlag, []string{}, "Plugin") + cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") + cmd.Flags().StringSlice(syslogFlag, []string{}, "Syslog") + cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") + cmd.Flags().String(planNameFlag, "", "Plan name") + cmd.Flags().String(versionFlag, "", "Instance OpenSearch version") + + err := flags.MarkFlagsRequired(cmd, instanceNameFlag) + cobra.CheckErr(err) +} + +func parseInput(cmd *cobra.Command, service, operation string) (*inputModel, error) { + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + planId := flags.FlagToStringPointer(cmd, planIdFlag) + planName := flags.FlagToStringValue(cmd, planNameFlag) + version := flags.FlagToStringValue(cmd, versionFlag) + + if planId == nil && (planName == "" || version == "") { + return nil, &cliErr.DSAInputPlanError{ + Service: service, + Operation: operation, + } + } + if planId != nil && (planName != "" || version != "") { + return nil, &cliErr.DSAInputPlanError{ + Service: service, + Operation: operation, + } + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceName: flags.FlagToStringPointer(cmd, instanceNameFlag), + EnableMonitoring: flags.FlagToBoolPointer(cmd, enableMonitoringFlag), + MonitoringInstanceId: flags.FlagToStringPointer(cmd, monitoringInstanceIdFlag), + Graphite: flags.FlagToStringPointer(cmd, graphiteFlag), + MetricsFrequency: flags.FlagToInt64Pointer(cmd, metricsFrequencyFlag), + MetricsPrefix: flags.FlagToStringPointer(cmd, metricsPrefixFlag), + Plugin: flags.FlagToStringSlicePointer(cmd, pluginFlag), + SgwAcl: flags.FlagToStringSlicePointer(cmd, sgwAclFlag), + Syslog: flags.FlagToStringSlicePointer(cmd, syslogFlag), + PlanId: planId, + PlanName: planName, + Version: version, + }, nil +} + +type OpenSearchClient interface { + CreateInstance(ctx context.Context, projectId string) opensearch.ApiCreateInstanceRequest + ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) +} + +func buildRequest(ctx context.Context, service string, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiCreateInstanceRequest, error) { + req := apiClient.CreateInstance(ctx, model.ProjectId) + + var planId *string + var err error + + offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + if err != nil { + return req, fmt.Errorf("get OpenSearch offerings: %w", err) + } + + if model.PlanId == nil { + planId, err = opensearchUtils.LoadPlanId(service, model.PlanName, model.Version, offerings) + if err != nil { + var dsaInvalidPlanError *cliErr.DSAInvalidPlanError + if !errors.As(err, &dsaInvalidPlanError) { + return req, fmt.Errorf("load plan ID: %w", err) + } + return req, err + } + } else { + err := opensearchUtils.ValidatePlanId(service, *model.PlanId, offerings) + if err != nil { + return req, err + } + planId = model.PlanId + } + + var sgwAcl *string + if model.SgwAcl != nil { + sgwAcl = utils.Ptr(strings.Join(*model.SgwAcl, ",")) + } + + req = req.CreateInstancePayload(opensearch.CreateInstancePayload{ + InstanceName: model.InstanceName, + Parameters: &opensearch.InstanceParameters{ + EnableMonitoring: model.EnableMonitoring, + Graphite: model.Graphite, + MonitoringInstanceId: model.MonitoringInstanceId, + MetricsFrequency: model.MetricsFrequency, + MetricsPrefix: model.MetricsPrefix, + Plugins: model.Plugin, + SgwAcl: sgwAcl, + Syslog: model.Syslog, + }, + PlanId: planId, + }) + return req, nil +} diff --git a/internal/cmd/opensearch/instance/create/create_test.go b/internal/cmd/opensearch/instance/create/create_test.go new file mode 100644 index 000000000..178e8daf4 --- /dev/null +++ b/internal/cmd/opensearch/instance/create/create_test.go @@ -0,0 +1,484 @@ +package create + +import ( + "context" + "fmt" + "testing" + + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} + +type openSearchClientMocked struct { + returnError bool + listOfferingsResp *opensearch.ListOfferingsResponse +} + +func (c *openSearchClientMocked) CreateInstance(ctx context.Context, projectId string) opensearch.ApiCreateInstanceRequest { + return testClient.CreateInstance(ctx, projectId) +} + +func (c *openSearchClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*opensearch.ListOfferingsResponse, error) { + if c.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return c.listOfferingsResp, nil +} + +var testProjectId = uuid.NewString() +var testPlanId = uuid.NewString() +var testMonitoringInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceNameFlag: "example-name", + enableMonitoringFlag: "true", + graphiteFlag: "example-graphite", + metricsFrequencyFlag: "100", + metricsPrefixFlag: "example-prefix", + monitoringInstanceIdFlag: testMonitoringInstanceId, + pluginFlag: "example-plugin", + sgwAclFlag: "198.51.100.14/24", + syslogFlag: "example-syslog", + planIdFlag: testPlanId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceName: utils.Ptr("example-name"), + EnableMonitoring: utils.Ptr(true), + Graphite: utils.Ptr("example-graphite"), + MetricsFrequency: utils.Ptr(int64(100)), + MetricsPrefix: utils.Ptr("example-prefix"), + MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + Plugin: utils.Ptr([]string{"example-plugin"}), + SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), + Syslog: utils.Ptr([]string{"example-syslog"}), + PlanId: utils.Ptr(testPlanId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiCreateInstanceRequest)) opensearch.ApiCreateInstanceRequest { + request := testClient.CreateInstance(testCtx, testProjectId) + request = request.CreateInstancePayload(opensearch.CreateInstancePayload{ + InstanceName: utils.Ptr("example-name"), + Parameters: &opensearch.InstanceParameters{ + EnableMonitoring: utils.Ptr(true), + Graphite: utils.Ptr("example-graphite"), + MetricsFrequency: utils.Ptr(int64(100)), + MetricsPrefix: utils.Ptr("example-prefix"), + MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + Plugins: utils.Ptr([]string{"example-plugin"}), + SgwAcl: utils.Ptr("198.51.100.14/24"), + Syslog: utils.Ptr([]string{"example-syslog"}), + }, + PlanId: utils.Ptr(testPlanId), + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + sgwAclValues []string + pluginValues []string + syslogValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with plan name and version", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[planNameFlag] = "plan-name" + flagValues[versionFlag] = "6" + delete(flagValues, planIdFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.PlanId = nil + model.PlanName = "plan-name" + model.Version = "6" + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "required fields only", + flagValues: map[string]string{ + projectIdFlag: testProjectId, + instanceNameFlag: "example-name", + planIdFlag: testPlanId, + }, + isValid: true, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceName: utils.Ptr("example-name"), + PlanId: utils.Ptr(testPlanId), + }, + }, + { + description: "zero values", + flagValues: map[string]string{ + projectIdFlag: testProjectId, + planIdFlag: testPlanId, + instanceNameFlag: "", + enableMonitoringFlag: "false", + graphiteFlag: "", + metricsFrequencyFlag: "0", + metricsPrefixFlag: "", + }, + isValid: true, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + PlanId: utils.Ptr(testPlanId), + InstanceName: utils.Ptr(""), + EnableMonitoring: utils.Ptr(false), + Graphite: utils.Ptr(""), + MetricsFrequency: utils.Ptr(int64(0)), + MetricsPrefix: utils.Ptr(""), + }, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid with plan ID, plan name and version", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[planNameFlag] = "plan-name" + flagValues[versionFlag] = "6" + }), + isValid: false, + }, + { + description: "invalid with plan ID and plan name", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[planNameFlag] = "plan-name" + }), + isValid: false, + }, + { + description: "invalid with plan name only", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[planNameFlag] = "plan-name" + delete(flagValues, planIdFlag) + }), + isValid: false, + }, + { + description: "repeated acl flags", + flagValues: fixtureFlagValues(), + sgwAclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SgwAcl = utils.Ptr( + append(*model.SgwAcl, "198.51.100.14/24", "198.51.100.14/32"), + ) + }), + }, + { + description: "repeated acl flag with list value", + flagValues: fixtureFlagValues(), + sgwAclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SgwAcl = utils.Ptr( + append(*model.SgwAcl, "198.51.100.14/24", "198.51.100.14/32"), + ) + }), + }, + { + description: "repeated plugin flags", + flagValues: fixtureFlagValues(), + pluginValues: []string{"example-plugin-1", "example-plugin-2"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Plugin = utils.Ptr( + append(*model.Plugin, "example-plugin-1", "example-plugin-2"), + ) + }), + }, + { + description: "repeated syslog flags", + flagValues: fixtureFlagValues(), + syslogValues: []string{"example-syslog-1", "example-syslog-2"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Syslog = utils.Ptr( + append(*model.Syslog, "example-syslog-1", "example-syslog-2"), + ) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + for _, value := range tt.sgwAclValues { + err := cmd.Flags().Set(sgwAclFlag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) + } + } + + for _, value := range tt.pluginValues { + err := cmd.Flags().Set(pluginFlag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", pluginFlag, value, err) + } + } + + for _, value := range tt.syslogValues { + err := cmd.Flags().Set(syslogFlag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, "opensearch", "create") + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiCreateInstanceRequest + getOfferingsFails bool + getOfferingsResp *opensearch.ListOfferingsResponse + isValid bool + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("example-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + }, + { + description: "use plan name and version", + model: fixtureInputModel( + func(model *inputModel) { + model.PlanId = nil + model.PlanName = "example-plan-name" + model.Version = "example-version" + }, + ), + expectedRequest: fixtureRequest(), + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("example-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + }, + { + description: "get offering fails", + model: fixtureInputModel( + func(model *inputModel) { + model.PlanId = nil + model.PlanName = "example-plan-name" + model.Version = "example-version" + }, + ), + getOfferingsFails: true, + isValid: false, + }, + { + description: "plan name not found", + model: fixtureInputModel( + func(model *inputModel) { + model.PlanId = nil + model.PlanName = "example-plan-name" + model.Version = "example-version" + }, + ), + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("other-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + isValid: false, + }, + { + description: "required fields only", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + PlanId: utils.Ptr(testPlanId), + }, + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("example-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + expectedRequest: testClient.CreateInstance(testCtx, testProjectId). + CreateInstancePayload(opensearch.CreateInstancePayload{PlanId: utils.Ptr(testPlanId), Parameters: &opensearch.InstanceParameters{}}), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &openSearchClientMocked{ + returnError: tt.getOfferingsFails, + listOfferingsResp: tt.getOfferingsResp, + } + request, err := buildRequest(testCtx, "opensearch", tt.model, client) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/instance/delete/delete.go b/internal/cmd/opensearch/instance/delete/delete.go new file mode 100644 index 000000000..a3cda12cb --- /dev/null +++ b/internal/cmd/opensearch/instance/delete/delete.go @@ -0,0 +1,114 @@ +package delete + +import ( + "context" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/confirm" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + "stackit/internal/pkg/spinner" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", instanceIdArg), + Short: "Delete an OpenSearch instance", + Long: "Delete an OpenSearch instance", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete an OpenSearch instance with ID "xxx"`, + "$ stackit opensearch instance delete xxx"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + instanceLabel = model.InstanceId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete instance %s? (This cannot be undone)", instanceLabel) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete OpenSearch instance: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Deleting instance") + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for OpenSearch instance deletion: %w", err) + } + s.Stop() + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + cmd.Printf("%s instance %s\n", operationState, instanceLabel) + return nil + }, + } + return cmd +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiDeleteInstanceRequest { + req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + return req +} diff --git a/internal/cmd/opensearch/instance/delete/delete_test.go b/internal/cmd/opensearch/instance/delete/delete_test.go new file mode 100644 index 000000000..b02a77a87 --- /dev/null +++ b/internal/cmd/opensearch/instance/delete/delete_test.go @@ -0,0 +1,215 @@ +package delete + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiDeleteInstanceRequest)) opensearch.ApiDeleteInstanceRequest { + request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiDeleteInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/instance/describe/describe.go b/internal/cmd/opensearch/instance/describe/describe.go new file mode 100644 index 000000000..d6bd6c7d3 --- /dev/null +++ b/internal/cmd/opensearch/instance/describe/describe.go @@ -0,0 +1,113 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + "stackit/internal/pkg/tables" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", instanceIdArg), + Short: "Get details of an OpenSearch instance", + Long: "Get details of an OpenSearch instance", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of an OpenSearch instance with ID "xxx"`, + "$ stackit opensearch instance describe xxx"), + examples.NewExample( + `Get details of an OpenSearch instance with ID "xxx" in a table format`, + "$ stackit opensearch instance describe xxx --output-format pretty"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read OpenSearch instance: %w", err) + } + + return outputResult(cmd, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiGetInstanceRequest { + req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + return req +} + +func outputResult(cmd *cobra.Command, outputFormat string, instance *opensearch.Instance) error { + switch outputFormat { + case globalflags.PrettyOutputFormat: + table := tables.NewTable() + table.AddRow("ID", *instance.InstanceId) + table.AddSeparator() + table.AddRow("NAME", *instance.Name) + table.AddSeparator() + table.AddRow("LAST OPERATION TYPE", *instance.LastOperation.Type) + table.AddSeparator() + table.AddRow("LAST OPERATION STATE", *instance.LastOperation.State) + err := table.Display(cmd) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + default: + details, err := json.MarshalIndent(instance, "", " ") + if err != nil { + return fmt.Errorf("marshal OpenSearch instance: %w", err) + } + cmd.Println(string(details)) + + return nil + } +} diff --git a/internal/cmd/opensearch/instance/describe/describe_test.go b/internal/cmd/opensearch/instance/describe/describe_test.go new file mode 100644 index 000000000..13a077eb0 --- /dev/null +++ b/internal/cmd/opensearch/instance/describe/describe_test.go @@ -0,0 +1,215 @@ +package describe + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiGetInstanceRequest)) opensearch.ApiGetInstanceRequest { + request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiGetInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/instance/instance.go b/internal/cmd/opensearch/instance/instance.go new file mode 100644 index 000000000..62f356352 --- /dev/null +++ b/internal/cmd/opensearch/instance/instance.go @@ -0,0 +1,33 @@ +package instance + +import ( + "stackit/internal/cmd/opensearch/instance/create" + "stackit/internal/cmd/opensearch/instance/delete" + "stackit/internal/cmd/opensearch/instance/describe" + "stackit/internal/cmd/opensearch/instance/list" + "stackit/internal/cmd/opensearch/instance/update" + "stackit/internal/pkg/args" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "instance", + Short: "Provides functionality for OpenSearch instances", + Long: "Provides functionality for OpenSearch instances", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd) + return cmd +} + +func addSubcommands(cmd *cobra.Command) { + cmd.AddCommand(create.NewCmd()) + cmd.AddCommand(delete.NewCmd()) + cmd.AddCommand(describe.NewCmd()) + cmd.AddCommand(list.NewCmd()) + cmd.AddCommand(update.NewCmd()) +} diff --git a/internal/cmd/opensearch/instance/list/list.go b/internal/cmd/opensearch/instance/list/list.go new file mode 100644 index 000000000..6aa442ef7 --- /dev/null +++ b/internal/cmd/opensearch/instance/list/list.go @@ -0,0 +1,142 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/projectname" + "stackit/internal/pkg/services/opensearch/client" + "stackit/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all OpenSearch instances", + Long: "List all OpenSearch instances", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all OpenSearch instances`, + "$ stackit opensearch instance list"), + examples.NewExample( + `List all OpenSearch instances in JSON format`, + "$ stackit opensearch instance list --output-format json"), + examples.NewExample( + `List up to 10 OpenSearch instances`, + "$ stackit opensearch instance list --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get OpenSearch instances: %w", err) + } + instances := *resp.Instances + if len(instances) == 0 { + projectLabel, err := projectname.GetProjectName(ctx, cmd) + if err != nil { + projectLabel = model.ProjectId + } + cmd.Printf("No instances found for project %s\n", projectLabel) + return nil + } + + // Truncate output + if model.Limit != nil && len(instances) > int(*model.Limit) { + instances = instances[:*model.Limit] + } + + return outputResult(cmd, model.OutputFormat, instances) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + Limit: flags.FlagToInt64Pointer(cmd, limitFlag), + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiListInstancesRequest { + req := apiClient.ListInstances(ctx, model.ProjectId) + return req +} + +func outputResult(cmd *cobra.Command, outputFormat string, instances []opensearch.Instance) error { + switch outputFormat { + case globalflags.JSONOutputFormat: + details, err := json.MarshalIndent(instances, "", " ") + if err != nil { + return fmt.Errorf("marshal OpenSearch instance list: %w", err) + } + cmd.Println(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "NAME", "LAST OPERATION TYPE", "LAST OPERATION STATE") + for i := range instances { + instance := instances[i] + table.AddRow(*instance.InstanceId, *instance.Name, *instance.LastOperation.Type, *instance.LastOperation.State) + } + err := table.Display(cmd) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/opensearch/instance/list/list_test.go b/internal/cmd/opensearch/instance/list/list_test.go new file mode 100644 index 000000000..a16f48d1d --- /dev/null +++ b/internal/cmd/opensearch/instance/list/list_test.go @@ -0,0 +1,185 @@ +package list + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiListInstancesRequest)) opensearch.ApiListInstancesRequest { + request := testClient.ListInstances(testCtx, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiListInstancesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go new file mode 100644 index 000000000..c4a0ccc3d --- /dev/null +++ b/internal/cmd/opensearch/instance/update/update.go @@ -0,0 +1,264 @@ +package update + +import ( + "context" + "errors" + "fmt" + "strings" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/confirm" + cliErr "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/services/opensearch/client" + opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + "stackit/internal/pkg/spinner" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" +) + +const ( + instanceIdArg = "INSTANCE_ID" + + instanceNameFlag = "name" + enableMonitoringFlag = "enable-monitoring" + graphiteFlag = "graphite" + metricsFrequencyFlag = "metrics-frequency" + metricsPrefixFlag = "metrics-prefix" + monitoringInstanceIdFlag = "monitoring-instance-id" + pluginFlag = "plugin" + sgwAclFlag = "acl" + syslogFlag = "syslog" + planIdFlag = "plan-id" + planNameFlag = "plan-name" + versionFlag = "version" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + PlanName string + Version string + + EnableMonitoring *bool + Graphite *string + MetricsFrequency *int64 + MetricsPrefix *string + MonitoringInstanceId *string + Plugin *[]string + SgwAcl *[]string + Syslog *[]string + PlanId *string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", instanceIdArg), + Short: "Updates an OpenSearch instance", + Long: "Updates an OpenSearch instance", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the plan of an OpenSearch instance with ID "xxx"`, + "$ stackit opensearch instance update xxx --plan-id xxx"), + examples.NewExample( + `Update the range of IPs allowed to access an OpenSearch instance with ID "xxx"`, + "$ stackit opensearch instance update xxx --acl 192.168.1.0/24"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + // Service name and operation needed for error handling + service := strings.Split(cmd.Parent().Parent().Use, " ")[0] + operation := cmd.Use + model, err := parseInput(cmd, args, service, operation) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + instanceLabel = model.InstanceId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update instance %s?", instanceLabel) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, service, model, apiClient) + if err != nil { + var dsaInvalidPlanError *cliErr.DSAInvalidPlanError + if !errors.As(err, &dsaInvalidPlanError) { + return fmt.Errorf("build OpenSearch instance update request: %w", err) + } + return err + } + err = req.Execute() + if err != nil { + return fmt.Errorf("update OpenSearch instance: %w", err) + } + instanceId := model.InstanceId + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Updating instance") + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for OpenSearch instance update: %w", err) + } + s.Stop() + } + + operationState := "Updated" + if model.Async { + operationState = "Triggered update of" + } + cmd.Printf("%s instance %s\n", operationState, instanceLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") + cmd.Flags().String(graphiteFlag, "", "Graphite host") + cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") + cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") + cmd.Flags().StringSlice(pluginFlag, []string{}, "Plugin") + cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") + cmd.Flags().StringSlice(syslogFlag, []string{}, "Syslog") + cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") + cmd.Flags().String(planNameFlag, "", "Plan name") + cmd.Flags().String(versionFlag, "", "Instance OpenSearch version") + + cmd.MarkFlagsMutuallyExclusive(planIdFlag, planNameFlag) + cmd.MarkFlagsMutuallyExclusive(planIdFlag, versionFlag) + cmd.MarkFlagsRequiredTogether(planNameFlag, versionFlag) +} + +func parseInput(cmd *cobra.Command, inputArgs []string, service, operation string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + enableMonitoring := flags.FlagToBoolPointer(cmd, enableMonitoringFlag) + monitoringInstanceId := flags.FlagToStringPointer(cmd, monitoringInstanceIdFlag) + graphite := flags.FlagToStringPointer(cmd, graphiteFlag) + metricsFrequency := flags.FlagToInt64Pointer(cmd, metricsFrequencyFlag) + metricsPrefix := flags.FlagToStringPointer(cmd, metricsPrefixFlag) + plugin := flags.FlagToStringSlicePointer(cmd, pluginFlag) + sgwAcl := flags.FlagToStringSlicePointer(cmd, sgwAclFlag) + syslog := flags.FlagToStringSlicePointer(cmd, syslogFlag) + planId := flags.FlagToStringPointer(cmd, planIdFlag) + planName := flags.FlagToStringValue(cmd, planNameFlag) + version := flags.FlagToStringValue(cmd, versionFlag) + + if planId != nil && (planName != "" || version != "") { + return nil, &cliErr.DSAInputPlanError{ + Service: service, + Operation: operation, + } + } + + if enableMonitoring == nil && monitoringInstanceId == nil && graphite == nil && + metricsFrequency == nil && metricsPrefix == nil && plugin == nil && + sgwAcl == nil && syslog == nil && planId == nil && + planName == "" && version == "" { + return nil, &cliErr.EmptyUpdateError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + EnableMonitoring: enableMonitoring, + MonitoringInstanceId: monitoringInstanceId, + Graphite: graphite, + MetricsFrequency: metricsFrequency, + MetricsPrefix: metricsPrefix, + Plugin: plugin, + SgwAcl: sgwAcl, + Syslog: syslog, + PlanId: planId, + PlanName: planName, + Version: version, + }, nil +} + +type OpenSearchClient interface { + PartialUpdateInstance(ctx context.Context, projectId, instanceId string) opensearch.ApiPartialUpdateInstanceRequest + ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) +} + +func buildRequest(ctx context.Context, service string, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiPartialUpdateInstanceRequest, error) { + req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) + + var planId *string + var err error + + offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + if err != nil { + return req, fmt.Errorf("get OpenSearch offerings: %w", err) + } + + if model.PlanId == nil && model.PlanName != "" && model.Version != "" { + planId, err = opensearchUtils.LoadPlanId(service, model.PlanName, model.Version, offerings) + if err != nil { + var dsaInvalidPlanError *cliErr.DSAInvalidPlanError + if !errors.As(err, &dsaInvalidPlanError) { + return req, fmt.Errorf("load plan ID: %w", err) + } + return req, err + } + } else { + // planId is not required for update operation + if model.PlanId != nil { + err := opensearchUtils.ValidatePlanId(service, *model.PlanId, offerings) + if err != nil { + return req, err + } + } + planId = model.PlanId + } + + var sgwAcl *string + if model.SgwAcl != nil { + sgwAcl = utils.Ptr(strings.Join(*model.SgwAcl, ",")) + } + + req = req.PartialUpdateInstancePayload(opensearch.PartialUpdateInstancePayload{ + Parameters: &opensearch.InstanceParameters{ + EnableMonitoring: model.EnableMonitoring, + Graphite: model.Graphite, + MonitoringInstanceId: model.MonitoringInstanceId, + MetricsFrequency: model.MetricsFrequency, + MetricsPrefix: model.MetricsPrefix, + Plugins: model.Plugin, + SgwAcl: sgwAcl, + Syslog: model.Syslog, + }, + PlanId: planId, + }) + return req, nil +} diff --git a/internal/cmd/opensearch/instance/update/update_test.go b/internal/cmd/opensearch/instance/update/update_test.go new file mode 100644 index 000000000..5154554cf --- /dev/null +++ b/internal/cmd/opensearch/instance/update/update_test.go @@ -0,0 +1,485 @@ +package update + +import ( + "context" + "fmt" + "testing" + + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} + +type openSearchClientMocked struct { + returnError bool + listOfferingsResp *opensearch.ListOfferingsResponse +} + +func (c *openSearchClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId string) opensearch.ApiPartialUpdateInstanceRequest { + return testClient.PartialUpdateInstance(ctx, projectId, instanceId) +} + +func (c *openSearchClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*opensearch.ListOfferingsResponse, error) { + if c.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return c.listOfferingsResp, nil +} + +var ( + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testPlanId = uuid.NewString() + testMonitoringInstanceId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + enableMonitoringFlag: "true", + graphiteFlag: "example-graphite", + metricsFrequencyFlag: "100", + metricsPrefixFlag: "example-prefix", + monitoringInstanceIdFlag: testMonitoringInstanceId, + pluginFlag: "example-plugin", + sgwAclFlag: "198.51.100.14/24", + syslogFlag: "example-syslog", + planIdFlag: testPlanId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + EnableMonitoring: utils.Ptr(true), + Graphite: utils.Ptr("example-graphite"), + MetricsFrequency: utils.Ptr(int64(100)), + MetricsPrefix: utils.Ptr("example-prefix"), + MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + Plugin: utils.Ptr([]string{"example-plugin"}), + SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), + Syslog: utils.Ptr([]string{"example-syslog"}), + PlanId: utils.Ptr(testPlanId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiPartialUpdateInstanceRequest)) opensearch.ApiPartialUpdateInstanceRequest { + request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) + request = request.PartialUpdateInstancePayload(opensearch.PartialUpdateInstancePayload{ + Parameters: &opensearch.InstanceParameters{ + EnableMonitoring: utils.Ptr(true), + Graphite: utils.Ptr("example-graphite"), + MetricsFrequency: utils.Ptr(int64(100)), + MetricsPrefix: utils.Ptr("example-prefix"), + MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + Plugins: utils.Ptr([]string{"example-plugin"}), + SgwAcl: utils.Ptr("198.51.100.14/24"), + Syslog: utils.Ptr([]string{"example-syslog"}), + }, + PlanId: utils.Ptr(testPlanId), + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + sgwAclValues []string + pluginValues []string + syslogValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "required flags only (no values to update)", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + }, + isValid: false, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + }, + }, + { + description: "zero values", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + planIdFlag: testPlanId, + enableMonitoringFlag: "false", + graphiteFlag: "", + metricsFrequencyFlag: "0", + metricsPrefixFlag: "", + }, + isValid: true, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + PlanId: utils.Ptr(testPlanId), + EnableMonitoring: utils.Ptr(false), + Graphite: utils.Ptr(""), + MetricsFrequency: utils.Ptr(int64(0)), + MetricsPrefix: utils.Ptr(""), + }, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "repeated acl flags", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + sgwAclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SgwAcl = utils.Ptr( + append(*model.SgwAcl, "198.51.100.14/24", "198.51.100.14/32"), + ) + }), + }, + { + description: "repeated acl flag with list value", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + sgwAclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SgwAcl = utils.Ptr( + append(*model.SgwAcl, "198.51.100.14/24", "198.51.100.14/32"), + ) + }), + }, + { + description: "repeated plugin flags", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + pluginValues: []string{"example-plugin-1", "example-plugin-2"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Plugin = utils.Ptr( + append(*model.Plugin, "example-plugin-1", "example-plugin-2"), + ) + }), + }, + { + description: "repeated syslog flags", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + syslogValues: []string{"example-syslog-1", "example-syslog-2"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Syslog = utils.Ptr( + append(*model.Syslog, "example-syslog-1", "example-syslog-2"), + ) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + for _, value := range tt.sgwAclValues { + err := cmd.Flags().Set(sgwAclFlag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) + } + } + + for _, value := range tt.pluginValues { + err := cmd.Flags().Set(pluginFlag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", pluginFlag, value, err) + } + } + + for _, value := range tt.syslogValues { + err := cmd.Flags().Set(syslogFlag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues, "opensearch", "update") + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiPartialUpdateInstanceRequest + getOfferingsFails bool + listOfferingsResp *opensearch.ListOfferingsResponse + isValid bool + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + listOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("example-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + }, + { + description: "use plan name and version", + model: fixtureInputModel( + func(model *inputModel) { + model.PlanId = nil + model.PlanName = "example-plan-name" + model.Version = "example-version" + }, + ), + expectedRequest: fixtureRequest(), + listOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("example-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + }, + { + description: "get offering fails", + model: fixtureInputModel( + func(model *inputModel) { + model.PlanId = nil + model.PlanName = "example-plan-name" + model.Version = "example-version" + }, + ), + getOfferingsFails: true, + isValid: false, + }, + { + description: "plan name not found", + model: fixtureInputModel( + func(model *inputModel) { + model.PlanId = nil + model.PlanName = "example-plan-name" + model.Version = "example-version" + }, + ), + listOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: &[]opensearch.Offering{ + { + Version: utils.Ptr("example-version"), + Plans: &[]opensearch.Plan{ + { + Name: utils.Ptr("other-plan-name"), + Id: utils.Ptr(testPlanId), + }, + }, + }, + }, + }, + isValid: false, + }, + { + description: "required fields only", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + }, + expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). + PartialUpdateInstancePayload(opensearch.PartialUpdateInstancePayload{Parameters: &opensearch.InstanceParameters{}}), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &openSearchClientMocked{ + returnError: tt.getOfferingsFails, + listOfferingsResp: tt.listOfferingsResp, + } + request, err := buildRequest(testCtx, "opensearch", tt.model, client) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/opensearch/opensearch.go b/internal/cmd/opensearch/opensearch.go new file mode 100644 index 000000000..42239c71e --- /dev/null +++ b/internal/cmd/opensearch/opensearch.go @@ -0,0 +1,29 @@ +package opensearch + +import ( + "stackit/internal/cmd/opensearch/credentials" + "stackit/internal/cmd/opensearch/instance" + "stackit/internal/cmd/opensearch/plans" + "stackit/internal/pkg/args" + "stackit/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "opensearch", + Short: "Provides functionality for OpenSearch", + Long: "Provides functionality for OpenSearch", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd) + return cmd +} + +func addSubcommands(cmd *cobra.Command) { + cmd.AddCommand(instance.NewCmd()) + cmd.AddCommand(plans.NewCmd()) + cmd.AddCommand(credentials.NewCmd()) +} diff --git a/internal/cmd/opensearch/plans/plans.go b/internal/cmd/opensearch/plans/plans.go new file mode 100644 index 000000000..54b852595 --- /dev/null +++ b/internal/cmd/opensearch/plans/plans.go @@ -0,0 +1,147 @@ +package plans + +import ( + "context" + "encoding/json" + "fmt" + + "stackit/internal/pkg/args" + "stackit/internal/pkg/errors" + "stackit/internal/pkg/examples" + "stackit/internal/pkg/flags" + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/projectname" + "stackit/internal/pkg/services/opensearch/client" + "stackit/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "plans", + Short: "List all OpenSearch service plans", + Long: "List all OpenSearch service plans", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all OpenSearch service plans`, + "$ stackit opensearch plans"), + examples.NewExample( + `List all OpenSearch service plans in JSON format`, + "$ stackit opensearch plans --output-format json"), + examples.NewExample( + `List up to 10 OpenSearch service plans`, + "$ stackit opensearch plans --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get OpenSearch service plans: %w", err) + } + plans := *resp.Offerings + if len(plans) == 0 { + projectLabel, err := projectname.GetProjectName(ctx, cmd) + if err != nil { + projectLabel = model.ProjectId + } + cmd.Printf("No plans found for project %s\n", projectLabel) + return nil + } + + // Truncate output + if model.Limit != nil && len(plans) > int(*model.Limit) { + plans = plans[:*model.Limit] + } + + return outputResult(cmd, model.OutputFormat, plans) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiListOfferingsRequest { + req := apiClient.ListOfferings(ctx, model.ProjectId) + return req +} + +func outputResult(cmd *cobra.Command, outputFormat string, plans []opensearch.Offering) error { + switch outputFormat { + case globalflags.JSONOutputFormat: + details, err := json.MarshalIndent(plans, "", " ") + if err != nil { + return fmt.Errorf("marshal OpenSearch plans: %w", err) + } + cmd.Println(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("OFFERING NAME", "ID", "NAME", "DESCRIPTION") + for i := range plans { + o := plans[i] + for j := range *o.Plans { + p := (*o.Plans)[j] + table.AddRow(*o.Name, *p.Id, *p.Name, *p.Description) + } + table.AddSeparator() + } + table.EnableAutoMergeOnColumns(1) + err := table.Display(cmd) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/opensearch/plans/plans_test.go b/internal/cmd/opensearch/plans/plans_test.go new file mode 100644 index 000000000..8361216f8 --- /dev/null +++ b/internal/cmd/opensearch/plans/plans_test.go @@ -0,0 +1,185 @@ +package plans + +import ( + "context" + "testing" + + "stackit/internal/pkg/globalflags" + "stackit/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &opensearch.APIClient{} +var testProjectId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *opensearch.ApiListOfferingsRequest)) opensearch.ApiListOfferingsRequest { + request := testClient.ListOfferings(testCtx, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest opensearch.ApiListOfferingsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index cd0cf8de8..41999861b 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -11,6 +11,7 @@ import ( "stackit/internal/cmd/curl" "stackit/internal/cmd/dns" "stackit/internal/cmd/mongodbflex" + "stackit/internal/cmd/opensearch" "stackit/internal/cmd/organization" "stackit/internal/cmd/project" serviceaccount "stackit/internal/cmd/service-account" @@ -77,12 +78,13 @@ func configureFlags(cmd *cobra.Command) error { func addSubcommands(cmd *cobra.Command) { cmd.AddCommand(auth.NewCmd()) - cmd.AddCommand(curl.NewCmd()) cmd.AddCommand(config.NewCmd()) - cmd.AddCommand(organization.NewCmd()) - cmd.AddCommand(project.NewCmd()) + cmd.AddCommand(curl.NewCmd()) cmd.AddCommand(dns.NewCmd()) cmd.AddCommand(mongodbflex.NewCmd()) + cmd.AddCommand(opensearch.NewCmd()) + cmd.AddCommand(organization.NewCmd()) + cmd.AddCommand(project.NewCmd()) cmd.AddCommand(serviceaccount.NewCmd()) cmd.AddCommand(ske.NewCmd()) } diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 2f1221371..c98ea35d7 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -22,6 +22,7 @@ const ( ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" SKECustomEndpointKey = "ske_custom_endpoint" ResourceManagerEndpointKey = "resource_manager_custom_endpoint" + OpenSearchCustomEndpointKey = "opensearch_custom_endpoint" AsyncDefault = "false" SessionTimeLimitDefault = "2h" @@ -46,6 +47,7 @@ var ConfigKeys = []string{ ServiceAccountCustomEndpointKey, SKECustomEndpointKey, ResourceManagerEndpointKey, + OpenSearchCustomEndpointKey, } func InitConfig() { @@ -114,5 +116,6 @@ func setConfigDefaults() { viper.SetDefault(MongoDBFlexCustomEndpointKey, "") viper.SetDefault(ServiceAccountCustomEndpointKey, "") viper.SetDefault(SKECustomEndpointKey, "") + viper.SetDefault(OpenSearchCustomEndpointKey, "") viper.SetDefault(ResourceManagerEndpointKey, "") } diff --git a/internal/pkg/services/opensearch/client/client.go b/internal/pkg/services/opensearch/client/client.go new file mode 100644 index 000000000..cf338d121 --- /dev/null +++ b/internal/pkg/services/opensearch/client/client.go @@ -0,0 +1,37 @@ +package client + +import ( + "stackit/internal/pkg/auth" + "stackit/internal/pkg/config" + "stackit/internal/pkg/errors" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +func ConfigureClient(cmd *cobra.Command) (*opensearch.APIClient, error) { + var err error + var apiClient *opensearch.APIClient + var cfgOptions []sdkConfig.ConfigurationOption + + authCfgOption, err := auth.AuthenticationConfig(cmd, auth.AuthorizeUser) + if err != nil { + return nil, &errors.AuthError{} + } + cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) + + customEndpoint := viper.GetString(config.OpenSearchCustomEndpointKey) + + if customEndpoint != "" { + cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) + } + + apiClient, err = opensearch.NewAPIClient(cfgOptions...) + if err != nil { + return nil, &errors.AuthError{} + } + + return apiClient, nil +} diff --git a/internal/pkg/services/opensearch/utils/utils.go b/internal/pkg/services/opensearch/utils/utils.go new file mode 100644 index 000000000..255afe6b2 --- /dev/null +++ b/internal/pkg/services/opensearch/utils/utils.go @@ -0,0 +1,82 @@ +package utils + +import ( + "context" + "fmt" + "stackit/internal/pkg/errors" + "strings" + + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +func ValidatePlanId(service, planId string, offerings *opensearch.ListOfferingsResponse) error { + for _, offer := range *offerings.Offerings { + for _, plan := range *offer.Plans { + if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + return nil + } + } + } + + return &errors.DSAInvalidPlanError{ + Service: service, + Details: fmt.Sprintf("You provided plan ID '%s', which is invalid.", planId), + } +} + +func LoadPlanId(service, planName, version string, offerings *opensearch.ListOfferingsResponse) (*string, error) { + availableVersions := "" + availablePlanNames := "" + isValidVersion := false + for _, offer := range *offerings.Offerings { + if !strings.EqualFold(*offer.Version, version) { + availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) + continue + } + isValidVersion = true + + for _, plan := range *offer.Plans { + if plan.Name == nil { + continue + } + if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { + return plan.Id, nil + } + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) + } + } + + if !isValidVersion { + details := fmt.Sprintf("You provided version '%s', which is invalid. Available versions are: %s", version, availableVersions) + return nil, &errors.DSAInvalidPlanError{ + Service: service, + Details: details, + } + } + details := fmt.Sprintf("You provided plan_name '%s' for version %s, which is invalid. Available plan names for that version are: %s", planName, version, availablePlanNames) + return nil, &errors.DSAInvalidPlanError{ + Service: service, + Details: details, + } +} + +type OpenSearchClient interface { + GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*opensearch.Instance, error) + GetCredentialsExecute(ctx context.Context, projectId, instanceId, credentialsId string) (*opensearch.CredentialsResponse, error) +} + +func GetInstanceName(ctx context.Context, apiClient OpenSearchClient, projectId, instanceId string) (string, error) { + resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) + if err != nil { + return "", fmt.Errorf("get OpenSearch instance: %w", err) + } + return *resp.Name, nil +} + +func GetCredentialUsername(ctx context.Context, apiClient OpenSearchClient, projectId, instanceId, credentialsId string) (string, error) { + resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) + if err != nil { + return "", fmt.Errorf("get OpenSearch credentials: %w", err) + } + return *resp.Raw.Credentials.Username, nil +} diff --git a/internal/pkg/services/opensearch/utils/utils_test.go b/internal/pkg/services/opensearch/utils/utils_test.go new file mode 100644 index 000000000..b0c2e85a6 --- /dev/null +++ b/internal/pkg/services/opensearch/utils/utils_test.go @@ -0,0 +1,144 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + "stackit/internal/pkg/utils" + + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" +) + +var ( + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testCredentialsId = uuid.NewString() +) + +const ( + testInstanceName = "instance" + testCredentialsUsername = "username" +) + +type openSearchClientMocked struct { + getInstanceFails bool + getInstanceResp *opensearch.Instance + getCredentialsFails bool + getCredentialsResp *opensearch.CredentialsResponse +} + +func (m *openSearchClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*opensearch.Instance, error) { + if m.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return m.getInstanceResp, nil +} + +func (m *openSearchClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*opensearch.CredentialsResponse, error) { + if m.getCredentialsFails { + return nil, fmt.Errorf("could not get user") + } + return m.getCredentialsResp, nil +} + +func TestGetInstanceName(t *testing.T) { + tests := []struct { + description string + getInstanceFails bool + getInstanceResp *opensearch.Instance + isValid bool + expectedOutput string + }{ + { + description: "base", + getInstanceResp: &opensearch.Instance{ + Name: utils.Ptr(testInstanceName), + }, + isValid: true, + expectedOutput: testInstanceName, + }, + { + description: "get instance fails", + getInstanceFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &openSearchClientMocked{ + getInstanceFails: tt.getInstanceFails, + getInstanceResp: tt.getInstanceResp, + } + + output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} + +func TestCredentialUsername(t *testing.T) { + tests := []struct { + description string + getCredentialsFails bool + getCredentialsResp *opensearch.CredentialsResponse + isValid bool + expectedOutput string + }{ + { + description: "base", + getCredentialsResp: &opensearch.CredentialsResponse{ + Raw: &opensearch.RawCredentials{ + Credentials: &opensearch.Credentials{ + Username: utils.Ptr(testCredentialsUsername), + }, + }, + }, + isValid: true, + expectedOutput: testCredentialsUsername, + }, + { + description: "get credentials fails", + getCredentialsFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &openSearchClientMocked{ + getCredentialsFails: tt.getCredentialsFails, + getCredentialsResp: tt.getCredentialsResp, + } + + output, err := GetCredentialUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} From 18076ef67c613f10671fb95d37376d4ad92dd9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Tue, 30 Jan 2024 16:28:18 +0000 Subject: [PATCH 02/11] Update docs --- docs/stackit.md | 7 +++ docs/stackit_config_set.md | 1 + docs/stackit_config_unset.md | 1 + docs/stackit_opensearch.md | 34 +++++++++++ docs/stackit_opensearch_credentials.md | 35 ++++++++++++ docs/stackit_opensearch_credentials_create.md | 43 ++++++++++++++ docs/stackit_opensearch_credentials_delete.md | 39 +++++++++++++ ...stackit_opensearch_credentials_describe.md | 42 ++++++++++++++ docs/stackit_opensearch_credentials_list.md | 46 +++++++++++++++ docs/stackit_opensearch_instance.md | 36 ++++++++++++ docs/stackit_opensearch_instance_create.md | 56 +++++++++++++++++++ docs/stackit_opensearch_instance_delete.md | 38 +++++++++++++ docs/stackit_opensearch_instance_describe.md | 41 ++++++++++++++ docs/stackit_opensearch_instance_list.md | 45 +++++++++++++++ docs/stackit_opensearch_instance_update.md | 52 +++++++++++++++++ docs/stackit_opensearch_plans.md | 45 +++++++++++++++ 16 files changed, 561 insertions(+) create mode 100644 docs/stackit_opensearch.md create mode 100644 docs/stackit_opensearch_credentials.md create mode 100644 docs/stackit_opensearch_credentials_create.md create mode 100644 docs/stackit_opensearch_credentials_delete.md create mode 100644 docs/stackit_opensearch_credentials_describe.md create mode 100644 docs/stackit_opensearch_credentials_list.md create mode 100644 docs/stackit_opensearch_instance.md create mode 100644 docs/stackit_opensearch_instance_create.md create mode 100644 docs/stackit_opensearch_instance_delete.md create mode 100644 docs/stackit_opensearch_instance_describe.md create mode 100644 docs/stackit_opensearch_instance_list.md create mode 100644 docs/stackit_opensearch_instance_update.md create mode 100644 docs/stackit_opensearch_plans.md diff --git a/docs/stackit.md b/docs/stackit.md index 04061303a..e47559a28 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -2,6 +2,12 @@ Manage STACKIT resources using the command line +### Synopsis + +Manage STACKIT resources using the command line. +This CLI is in a BETA state. +More services and functionality will be supported soon. Your feedback is appreciated! + ``` stackit [flags] ``` @@ -24,6 +30,7 @@ stackit [flags] * [stackit curl](./stackit_curl.md) - Execute an authenticated HTTP request to an endpoint * [stackit dns](./stackit_dns.md) - Provides functionality for DNS * [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex +* [stackit opensearch](./stackit_opensearch.md) - Provides functionality for OpenSearch * [stackit organization](./stackit_organization.md) - Provides functionality regarding organizations * [stackit project](./stackit_project.md) - Provides functionality regarding projects * [stackit service-account](./stackit_service-account.md) - Provides functionality for service accounts diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 318fce5d7..38cbe15fc 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -33,6 +33,7 @@ stackit config set [flags] -h, --help Help for "stackit config set" --membership-custom-endpoint string Membership custom endpoint --mongodbflex-custom-endpoint string MongoDB Flex custom endpoint + --opensearch-custom-endpoint string OpenSearch custom endpoint --resource-manager-custom-endpoint string Resource manager custom endpoint --service-account-custom-endpoint string Service Account custom endpoint --session-time-limit string Maximum time before authentication is required again. Can't be larger than 24h. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect) diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 8989dc9d3..d902f3dae 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -31,6 +31,7 @@ stackit config unset [flags] -h, --help Help for "stackit config unset" --membership-custom-endpoint Membership custom endpoint --mongodbflex-custom-endpoint MongoDB Flex custom endpoint + --opensearch-custom-endpoint OpenSearch custom endpoint --output-format Output format --project-id Project ID --resource-manager-custom-endpoint Resource Manager custom endpoint diff --git a/docs/stackit_opensearch.md b/docs/stackit_opensearch.md new file mode 100644 index 000000000..e178f9648 --- /dev/null +++ b/docs/stackit_opensearch.md @@ -0,0 +1,34 @@ +## stackit opensearch + +Provides functionality for OpenSearch + +### Synopsis + +Provides functionality for OpenSearch + +``` +stackit opensearch [flags] +``` + +### Options + +``` + -h, --help Help for "stackit opensearch" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit opensearch credentials](./stackit_opensearch_credentials.md) - Provides functionality for OpenSearch credentials +* [stackit opensearch instance](./stackit_opensearch_instance.md) - Provides functionality for OpenSearch instances +* [stackit opensearch plans](./stackit_opensearch_plans.md) - List all OpenSearch service plans + diff --git a/docs/stackit_opensearch_credentials.md b/docs/stackit_opensearch_credentials.md new file mode 100644 index 000000000..e8fde1943 --- /dev/null +++ b/docs/stackit_opensearch_credentials.md @@ -0,0 +1,35 @@ +## stackit opensearch credentials + +Provides functionality for OpenSearch credentials + +### Synopsis + +Provides functionality for OpenSearch credentials + +``` +stackit opensearch credentials [flags] +``` + +### Options + +``` + -h, --help Help for "stackit opensearch credentials" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch](./stackit_opensearch.md) - Provides functionality for OpenSearch +* [stackit opensearch credentials create](./stackit_opensearch_credentials_create.md) - Create credentials for an OpenSearch instance +* [stackit opensearch credentials delete](./stackit_opensearch_credentials_delete.md) - Delete credentials of an OpenSearch instance +* [stackit opensearch credentials describe](./stackit_opensearch_credentials_describe.md) - Get details of credentials of an OpenSearch instance +* [stackit opensearch credentials list](./stackit_opensearch_credentials_list.md) - List all credentials' IDs for an OpenSearch instance + diff --git a/docs/stackit_opensearch_credentials_create.md b/docs/stackit_opensearch_credentials_create.md new file mode 100644 index 000000000..e6a4bc9b0 --- /dev/null +++ b/docs/stackit_opensearch_credentials_create.md @@ -0,0 +1,43 @@ +## stackit opensearch credentials create + +Create credentials for an OpenSearch instance + +### Synopsis + +Create credentials (username and password) for an OpenSearch instance + +``` +stackit opensearch credentials create [flags] +``` + +### Examples + +``` + Create credentials for an OpenSearch instance + $ stackit opensearch credentials create --instance-id xxx + + Create credentials for an OpenSearch instance and hide the password in the output + $ stackit opensearch credentials create --instance-id xxx --hide-password +``` + +### Options + +``` + -h, --help Help for "stackit opensearch credentials create" + --hide-password Hide password in output + --instance-id string Instance ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch credentials](./stackit_opensearch_credentials.md) - Provides functionality for OpenSearch credentials + diff --git a/docs/stackit_opensearch_credentials_delete.md b/docs/stackit_opensearch_credentials_delete.md new file mode 100644 index 000000000..3380c62ea --- /dev/null +++ b/docs/stackit_opensearch_credentials_delete.md @@ -0,0 +1,39 @@ +## stackit opensearch credentials delete + +Delete credentials of an OpenSearch instance + +### Synopsis + +Delete credentials of an OpenSearch instance + +``` +stackit opensearch credentials delete CREDENTIAL_ID [flags] +``` + +### Examples + +``` + Delete credentials with ID "xxx" of OpenSearch instance with ID "yyy" + $ stackit opensearch credentials delete xxx --instance-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit opensearch credentials delete" + --instance-id string Instance ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch credentials](./stackit_opensearch_credentials.md) - Provides functionality for OpenSearch credentials + diff --git a/docs/stackit_opensearch_credentials_describe.md b/docs/stackit_opensearch_credentials_describe.md new file mode 100644 index 000000000..ddc514fda --- /dev/null +++ b/docs/stackit_opensearch_credentials_describe.md @@ -0,0 +1,42 @@ +## stackit opensearch credentials describe + +Get details of credentials of an OpenSearch instance + +### Synopsis + +Get details of credentials of an OpenSearch instance. The password will be shown in plain text in the output. + +``` +stackit opensearch credentials describe CREDENTIAL_ID [flags] +``` + +### Examples + +``` + Get details of credentials of an OpenSearch instance with ID "xxx" from instance with ID "yyy" + $ stackit opensearch credentials describe xxx --instance-id yyy + + Get details of credentials of an OpenSearch instance with ID "xxx" from instance with ID "yyy" in a table format + $ stackit opensearch credentials describe xxx --instance-id yyy --output-format pretty +``` + +### Options + +``` + -h, --help Help for "stackit opensearch credentials describe" + --instance-id string Instance ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch credentials](./stackit_opensearch_credentials.md) - Provides functionality for OpenSearch credentials + diff --git a/docs/stackit_opensearch_credentials_list.md b/docs/stackit_opensearch_credentials_list.md new file mode 100644 index 000000000..14955ccb0 --- /dev/null +++ b/docs/stackit_opensearch_credentials_list.md @@ -0,0 +1,46 @@ +## stackit opensearch credentials list + +List all credentials' IDs for an OpenSearch instance + +### Synopsis + +List all credentials' IDs for an OpenSearch instance + +``` +stackit opensearch credentials list [flags] +``` + +### Examples + +``` + List all credentials' IDs for an OpenSearch instance + $ stackit opensearch credentials list --instance-id xxx + + List all credentials' IDs for an OpenSearch instance in JSON format + $ stackit opensearch credentials list --instance-id xxx --output-format json + + List up to 10 credentials' IDs for an OpenSearch instance + $ stackit opensearch credentials list --instance-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit opensearch credentials list" + --instance-id string Instance ID + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch credentials](./stackit_opensearch_credentials.md) - Provides functionality for OpenSearch credentials + diff --git a/docs/stackit_opensearch_instance.md b/docs/stackit_opensearch_instance.md new file mode 100644 index 000000000..2d0a70f4c --- /dev/null +++ b/docs/stackit_opensearch_instance.md @@ -0,0 +1,36 @@ +## stackit opensearch instance + +Provides functionality for OpenSearch instances + +### Synopsis + +Provides functionality for OpenSearch instances + +``` +stackit opensearch instance [flags] +``` + +### Options + +``` + -h, --help Help for "stackit opensearch instance" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch](./stackit_opensearch.md) - Provides functionality for OpenSearch +* [stackit opensearch instance create](./stackit_opensearch_instance_create.md) - Create an OpenSearch instance +* [stackit opensearch instance delete](./stackit_opensearch_instance_delete.md) - Delete an OpenSearch instance +* [stackit opensearch instance describe](./stackit_opensearch_instance_describe.md) - Get details of an OpenSearch instance +* [stackit opensearch instance list](./stackit_opensearch_instance_list.md) - List all OpenSearch instances +* [stackit opensearch instance update](./stackit_opensearch_instance_update.md) - Updates an OpenSearch instance + diff --git a/docs/stackit_opensearch_instance_create.md b/docs/stackit_opensearch_instance_create.md new file mode 100644 index 000000000..456ed3908 --- /dev/null +++ b/docs/stackit_opensearch_instance_create.md @@ -0,0 +1,56 @@ +## stackit opensearch instance create + +Create an OpenSearch instance + +### Synopsis + +Create an OpenSearch instance. + +``` +stackit opensearch instance create [flags] +``` + +### Examples + +``` + Create an OpenSearch instance with name "my-instance" and specify plan by name and version + $ stackit opensearch instance create --name my-instance --plan-name stackit-qa-opensearch-1.2.10-replica --version 2 + + Create an OpenSearch instance with name "my-instance" and specify plan by ID + $ stackit opensearch instance create --name my-instance --plan-id xxx + + Create an OpenSearch instance with name "my-instance" and specify IP range which is allowed to access it + $ stackit opensearch instance create --name my-instance --plan-id xxx --acl 192.168.1.0/24 +``` + +### Options + +``` + --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) + --enable-monitoring Enable monitoring + --graphite string Graphite host + -h, --help Help for "stackit opensearch instance create" + --metrics-frequency int Metrics frequency + --metrics-prefix string Metrics prefix + --monitoring-instance-id string Monitoring instance ID + -n, --name string Instance name + --plan-id string Plan ID + --plan-name string Plan name + --plugin strings Plugin + --syslog strings Syslog + --version string Instance OpenSearch version +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch instance](./stackit_opensearch_instance.md) - Provides functionality for OpenSearch instances + diff --git a/docs/stackit_opensearch_instance_delete.md b/docs/stackit_opensearch_instance_delete.md new file mode 100644 index 000000000..1a02f0f4d --- /dev/null +++ b/docs/stackit_opensearch_instance_delete.md @@ -0,0 +1,38 @@ +## stackit opensearch instance delete + +Delete an OpenSearch instance + +### Synopsis + +Delete an OpenSearch instance + +``` +stackit opensearch instance delete INSTANCE_ID [flags] +``` + +### Examples + +``` + Delete an OpenSearch instance with ID "xxx" + $ stackit opensearch instance delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit opensearch instance delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch instance](./stackit_opensearch_instance.md) - Provides functionality for OpenSearch instances + diff --git a/docs/stackit_opensearch_instance_describe.md b/docs/stackit_opensearch_instance_describe.md new file mode 100644 index 000000000..00ce1d810 --- /dev/null +++ b/docs/stackit_opensearch_instance_describe.md @@ -0,0 +1,41 @@ +## stackit opensearch instance describe + +Get details of an OpenSearch instance + +### Synopsis + +Get details of an OpenSearch instance + +``` +stackit opensearch instance describe INSTANCE_ID [flags] +``` + +### Examples + +``` + Get details of an OpenSearch instance with ID "xxx" + $ stackit opensearch instance describe xxx + + Get details of an OpenSearch instance with ID "xxx" in a table format + $ stackit opensearch instance describe xxx --output-format pretty +``` + +### Options + +``` + -h, --help Help for "stackit opensearch instance describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch instance](./stackit_opensearch_instance.md) - Provides functionality for OpenSearch instances + diff --git a/docs/stackit_opensearch_instance_list.md b/docs/stackit_opensearch_instance_list.md new file mode 100644 index 000000000..650ce8120 --- /dev/null +++ b/docs/stackit_opensearch_instance_list.md @@ -0,0 +1,45 @@ +## stackit opensearch instance list + +List all OpenSearch instances + +### Synopsis + +List all OpenSearch instances + +``` +stackit opensearch instance list [flags] +``` + +### Examples + +``` + List all OpenSearch instances + $ stackit opensearch instance list + + List all OpenSearch instances in JSON format + $ stackit opensearch instance list --output-format json + + List up to 10 OpenSearch instances + $ stackit opensearch instance list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit opensearch instance list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch instance](./stackit_opensearch_instance.md) - Provides functionality for OpenSearch instances + diff --git a/docs/stackit_opensearch_instance_update.md b/docs/stackit_opensearch_instance_update.md new file mode 100644 index 000000000..dafc24840 --- /dev/null +++ b/docs/stackit_opensearch_instance_update.md @@ -0,0 +1,52 @@ +## stackit opensearch instance update + +Updates an OpenSearch instance + +### Synopsis + +Updates an OpenSearch instance + +``` +stackit opensearch instance update INSTANCE_ID [flags] +``` + +### Examples + +``` + Update the plan of an OpenSearch instance with ID "xxx" + $ stackit opensearch instance update xxx --plan-id xxx + + Update the range of IPs allowed to access an OpenSearch instance with ID "xxx" + $ stackit opensearch instance update xxx --acl 192.168.1.0/24 +``` + +### Options + +``` + --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) + --enable-monitoring Enable monitoring + --graphite string Graphite host + -h, --help Help for "stackit opensearch instance update" + --metrics-frequency int Metrics frequency + --metrics-prefix string Metrics prefix + --monitoring-instance-id string Monitoring instance ID + --plan-id string Plan ID + --plan-name string Plan name + --plugin strings Plugin + --syslog strings Syslog + --version string Instance OpenSearch version +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch instance](./stackit_opensearch_instance.md) - Provides functionality for OpenSearch instances + diff --git a/docs/stackit_opensearch_plans.md b/docs/stackit_opensearch_plans.md new file mode 100644 index 000000000..f9687bcfa --- /dev/null +++ b/docs/stackit_opensearch_plans.md @@ -0,0 +1,45 @@ +## stackit opensearch plans + +List all OpenSearch service plans + +### Synopsis + +List all OpenSearch service plans + +``` +stackit opensearch plans [flags] +``` + +### Examples + +``` + List all OpenSearch service plans + $ stackit opensearch plans + + List all OpenSearch service plans in JSON format + $ stackit opensearch plans --output-format json + + List up to 10 OpenSearch service plans + $ stackit opensearch plans --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit opensearch plans" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit opensearch](./stackit_opensearch.md) - Provides functionality for OpenSearch + From 1990cbbd653efe15adb8881d540dbde7b9588f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Wed, 31 Jan 2024 18:11:54 +0000 Subject: [PATCH 03/11] Adjustments after review --- .github/workflows/release.yaml | 4 ++++ .goreleaser.yaml | 16 +++++++++++++++- internal/cmd/config/set/set.go | 6 +++--- .../cmd/opensearch/instance/create/create.go | 2 +- internal/pkg/services/opensearch/utils/utils.go | 6 +++--- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 189a6dc5f..aa6080152 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,6 +17,8 @@ permissions: jobs: goreleaser: runs-on: ubuntu-latest + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} steps: - uses: actions/checkout@v4 with: @@ -32,6 +34,8 @@ jobs: with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: Install Snapcraft + uses: samuelmeuli/action-snapcraft@v2 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d2974cb53..3e5f1a7ef 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -78,4 +78,18 @@ brews: # If set to auto, the release will not be uploaded to the homebrew tap repo # if the tag has a prerelease indicator (e.g. v0.0.1-alpha1) # Not setting it for now so we can publish prerelease tags - # skip_upload: auto \ No newline at end of file + # skip_upload: auto + +snapcrafts: + # IDs of the builds for which to create packages for + builds: + - linux-builds + # The name of the snap + name: stackit-cli + # The canonical title of the application, displayed in the software + # centre graphical frontends + title: STACKIT CLI + publish: true + summary: A command-line interface to manage STACKIT resources. + description: A command-line interface to manage STACKIT resources. + license: Apache-2.0 \ No newline at end of file diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 5e6c3fca5..b099b4f62 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -23,7 +23,7 @@ const ( serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" - OpenSearchCustomEndpointFlag = "opensearch-custom-endpoint" + openSearchCustomEndpointFlag = "opensearch-custom-endpoint" ) type inputModel struct { @@ -87,7 +87,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account custom endpoint") cmd.Flags().String(skeCustomEndpointFlag, "", "SKE custom endpoint") cmd.Flags().String(resourceManagerCustomEndpointFlag, "", "Resource manager custom endpoint") - cmd.Flags().String(OpenSearchCustomEndpointFlag, "", "OpenSearch custom endpoint") + cmd.Flags().String(openSearchCustomEndpointFlag, "", "OpenSearch custom endpoint") err := viper.BindPFlag(config.DNSCustomEndpointKey, cmd.Flags().Lookup(dnsCustomEndpointFlag)) cobra.CheckErr(err) @@ -101,7 +101,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.ResourceManagerEndpointKey, cmd.Flags().Lookup(skeCustomEndpointFlag)) cobra.CheckErr(err) - err = viper.BindPFlag(config.OpenSearchCustomEndpointKey, cmd.Flags().Lookup(OpenSearchCustomEndpointFlag)) + err = viper.BindPFlag(config.OpenSearchCustomEndpointKey, cmd.Flags().Lookup(openSearchCustomEndpointFlag)) cobra.CheckErr(err) } diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index 18428a540..13aa0edc1 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -64,7 +64,7 @@ func NewCmd() *cobra.Command { Example: examples.Build( examples.NewExample( `Create an OpenSearch instance with name "my-instance" and specify plan by name and version`, - "$ stackit opensearch instance create --name my-instance --plan-name stackit-qa-opensearch-1.2.10-replica --version 2"), + "$ stackit opensearch instance create --name my-instance --plan-name stackit-opensearch-1.2.10-replica --version 2"), examples.NewExample( `Create an OpenSearch instance with name "my-instance" and specify plan by ID`, "$ stackit opensearch instance create --name my-instance --plan-id xxx"), diff --git a/internal/pkg/services/opensearch/utils/utils.go b/internal/pkg/services/opensearch/utils/utils.go index 255afe6b2..9d080e421 100644 --- a/internal/pkg/services/opensearch/utils/utils.go +++ b/internal/pkg/services/opensearch/utils/utils.go @@ -20,7 +20,7 @@ func ValidatePlanId(service, planId string, offerings *opensearch.ListOfferingsR return &errors.DSAInvalidPlanError{ Service: service, - Details: fmt.Sprintf("You provided plan ID '%s', which is invalid.", planId), + Details: fmt.Sprintf("You provided plan ID %q, which is invalid.", planId), } } @@ -47,13 +47,13 @@ func LoadPlanId(service, planName, version string, offerings *opensearch.ListOff } if !isValidVersion { - details := fmt.Sprintf("You provided version '%s', which is invalid. Available versions are: %s", version, availableVersions) + details := fmt.Sprintf("You provided version %q, which is invalid. Available versions are: %s", version, availableVersions) return nil, &errors.DSAInvalidPlanError{ Service: service, Details: details, } } - details := fmt.Sprintf("You provided plan_name '%s' for version %s, which is invalid. Available plan names for that version are: %s", planName, version, availablePlanNames) + details := fmt.Sprintf("You provided plan_name %q for version %s, which is invalid. Available plan names for that version are: %s", planName, version, availablePlanNames) return nil, &errors.DSAInvalidPlanError{ Service: service, Details: details, From 279261e3d09069dabe72586859f3888ced822b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Wed, 31 Jan 2024 18:16:13 +0000 Subject: [PATCH 04/11] Explicitly set service name instead pf getting it from parent --- internal/cmd/mongodbflex/instance/create/create.go | 3 +-- internal/cmd/mongodbflex/instance/update/update.go | 4 +--- internal/cmd/opensearch/instance/create/create.go | 2 +- internal/cmd/opensearch/instance/update/update.go | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/cmd/mongodbflex/instance/create/create.go b/internal/cmd/mongodbflex/instance/create/create.go index 1ca36afb2..b725e764f 100644 --- a/internal/cmd/mongodbflex/instance/create/create.go +++ b/internal/cmd/mongodbflex/instance/create/create.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "stackit/internal/pkg/args" "stackit/internal/pkg/confirm" @@ -77,7 +76,7 @@ func NewCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() // Service name and operation needed for error handling - service := strings.Split(cmd.Parent().Parent().Use, " ")[0] + service := "mongodbflex" operation := cmd.Use model, err := parseInput(cmd, service, operation) if err != nil { diff --git a/internal/cmd/mongodbflex/instance/update/update.go b/internal/cmd/mongodbflex/instance/update/update.go index 21024f5e3..9d5386c2b 100644 --- a/internal/cmd/mongodbflex/instance/update/update.go +++ b/internal/cmd/mongodbflex/instance/update/update.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "stackit/internal/pkg/args" "stackit/internal/pkg/confirm" @@ -72,8 +71,7 @@ func NewCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() // Service name and operation needed for error handling - service := strings.Split(cmd.Parent().Parent().Use, " ")[0] - operation := cmd.Use + service := "mongodbflex" model, err := parseInput(cmd, args, service, operation) if err != nil { return err diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index 13aa0edc1..bf442e16e 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -75,7 +75,7 @@ func NewCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() // Service name and operation needed for error handling - service := strings.Split(cmd.Parent().Parent().Use, " ")[0] + service := "opensearch" operation := cmd.Use model, err := parseInput(cmd, service, operation) if err != nil { diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go index c4a0ccc3d..5f0e59e37 100644 --- a/internal/cmd/opensearch/instance/update/update.go +++ b/internal/cmd/opensearch/instance/update/update.go @@ -73,7 +73,7 @@ func NewCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() // Service name and operation needed for error handling - service := strings.Split(cmd.Parent().Parent().Use, " ")[0] + service := "opensearch" operation := cmd.Use model, err := parseInput(cmd, args, service, operation) if err != nil { From 0bb774e1cde37c4a97b8b38d3ec83b94b30f333f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Wed, 31 Jan 2024 18:21:54 +0000 Subject: [PATCH 05/11] Restore line accidentally deleted --- internal/cmd/mongodbflex/instance/update/update.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cmd/mongodbflex/instance/update/update.go b/internal/cmd/mongodbflex/instance/update/update.go index 9d5386c2b..be9c6db27 100644 --- a/internal/cmd/mongodbflex/instance/update/update.go +++ b/internal/cmd/mongodbflex/instance/update/update.go @@ -72,6 +72,7 @@ func NewCmd() *cobra.Command { ctx := context.Background() // Service name and operation needed for error handling service := "mongodbflex" + operation := cmd.Use model, err := parseInput(cmd, args, service, operation) if err != nil { return err From 63fc029dc1836242a2ced3c60fcacde992ed1bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Thu, 1 Feb 2024 11:54:46 +0000 Subject: [PATCH 06/11] Remove changes belonging to other branch --- .github/workflows/release.yaml | 6 +----- .goreleaser.yaml | 15 +-------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aa6080152..b41ef611c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,8 +17,6 @@ permissions: jobs: goreleaser: runs-on: ubuntu-latest - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} steps: - uses: actions/checkout@v4 with: @@ -34,12 +32,10 @@ jobs: with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Install Snapcraft - uses: samuelmeuli/action-snapcraft@v2 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: args: release --clean env: GITHUB_TOKEN: ${{ secrets.CLI_RELEASE }} - GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} \ No newline at end of file + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3e5f1a7ef..9fa80b77c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -79,17 +79,4 @@ brews: # if the tag has a prerelease indicator (e.g. v0.0.1-alpha1) # Not setting it for now so we can publish prerelease tags # skip_upload: auto - -snapcrafts: - # IDs of the builds for which to create packages for - builds: - - linux-builds - # The name of the snap - name: stackit-cli - # The canonical title of the application, displayed in the software - # centre graphical frontends - title: STACKIT CLI - publish: true - summary: A command-line interface to manage STACKIT resources. - description: A command-line interface to manage STACKIT resources. - license: Apache-2.0 \ No newline at end of file + \ No newline at end of file From b84f6a0938f46a48e19bb5b7e8e88150ce6dae33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Fri, 2 Feb 2024 17:02:09 +0000 Subject: [PATCH 07/11] Update module name in imports --- .../opensearch/credentials/create/create.go | 16 +++++++------- .../credentials/create/create_test.go | 2 +- .../cmd/opensearch/credentials/credentials.go | 12 +++++----- .../opensearch/credentials/delete/delete.go | 18 +++++++-------- .../credentials/delete/delete_test.go | 2 +- .../credentials/describe/describe.go | 16 +++++++------- .../credentials/describe/describe_test.go | 2 +- .../cmd/opensearch/credentials/list/list.go | 16 +++++++------- .../opensearch/credentials/list/list_test.go | 4 ++-- .../cmd/opensearch/instance/create/create.go | 22 +++++++++---------- .../opensearch/instance/create/create_test.go | 4 ++-- .../cmd/opensearch/instance/delete/delete.go | 18 +++++++-------- .../opensearch/instance/delete/delete_test.go | 2 +- .../opensearch/instance/describe/describe.go | 14 ++++++------ .../instance/describe/describe_test.go | 2 +- internal/cmd/opensearch/instance/instance.go | 14 ++++++------ internal/cmd/opensearch/instance/list/list.go | 16 +++++++------- .../cmd/opensearch/instance/list/list_test.go | 4 ++-- .../cmd/opensearch/instance/update/update.go | 20 ++++++++--------- .../opensearch/instance/update/update_test.go | 4 ++-- internal/cmd/opensearch/opensearch.go | 10 ++++----- internal/cmd/opensearch/plans/plans.go | 16 +++++++------- internal/cmd/opensearch/plans/plans_test.go | 4 ++-- .../pkg/services/opensearch/client/client.go | 6 ++--- .../pkg/services/opensearch/utils/utils.go | 3 ++- .../services/opensearch/utils/utils_test.go | 2 +- 26 files changed, 125 insertions(+), 124 deletions(-) diff --git a/internal/cmd/opensearch/credentials/create/create.go b/internal/cmd/opensearch/credentials/create/create.go index 14ab59c72..87cafa68d 100644 --- a/internal/cmd/opensearch/credentials/create/create.go +++ b/internal/cmd/opensearch/credentials/create/create.go @@ -4,14 +4,14 @@ import ( "context" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/confirm" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - opensearchUtils "stackit/internal/pkg/services/opensearch/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/credentials/create/create_test.go b/internal/cmd/opensearch/credentials/create/create_test.go index 429d8c474..5ba51a1f2 100644 --- a/internal/cmd/opensearch/credentials/create/create_test.go +++ b/internal/cmd/opensearch/credentials/create/create_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/credentials/credentials.go b/internal/cmd/opensearch/credentials/credentials.go index 966119715..84cdd555d 100644 --- a/internal/cmd/opensearch/credentials/credentials.go +++ b/internal/cmd/opensearch/credentials/credentials.go @@ -1,12 +1,12 @@ package credentials import ( - "stackit/internal/cmd/opensearch/credentials/create" - "stackit/internal/cmd/opensearch/credentials/delete" - "stackit/internal/cmd/opensearch/credentials/describe" - "stackit/internal/cmd/opensearch/credentials/list" - "stackit/internal/pkg/args" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) diff --git a/internal/cmd/opensearch/credentials/delete/delete.go b/internal/cmd/opensearch/credentials/delete/delete.go index 505c6bd17..e1edafa65 100644 --- a/internal/cmd/opensearch/credentials/delete/delete.go +++ b/internal/cmd/opensearch/credentials/delete/delete.go @@ -4,15 +4,15 @@ import ( "context" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/confirm" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - opensearchUtils "stackit/internal/pkg/services/opensearch/utils" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/credentials/delete/delete_test.go b/internal/cmd/opensearch/credentials/delete/delete_test.go index 41f627317..af37bcc2d 100644 --- a/internal/cmd/opensearch/credentials/delete/delete_test.go +++ b/internal/cmd/opensearch/credentials/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/credentials/describe/describe.go b/internal/cmd/opensearch/credentials/describe/describe.go index 8fb7b3295..177967cb4 100644 --- a/internal/cmd/opensearch/credentials/describe/describe.go +++ b/internal/cmd/opensearch/credentials/describe/describe.go @@ -5,14 +5,14 @@ import ( "encoding/json" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - "stackit/internal/pkg/tables" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/credentials/describe/describe_test.go b/internal/cmd/opensearch/credentials/describe/describe_test.go index 52eec1fae..5bdb935c6 100644 --- a/internal/cmd/opensearch/credentials/describe/describe_test.go +++ b/internal/cmd/opensearch/credentials/describe/describe_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/credentials/list/list.go b/internal/cmd/opensearch/credentials/list/list.go index f3d0c008a..2d75eccf7 100644 --- a/internal/cmd/opensearch/credentials/list/list.go +++ b/internal/cmd/opensearch/credentials/list/list.go @@ -5,14 +5,14 @@ import ( "encoding/json" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - opensearchUtils "stackit/internal/pkg/services/opensearch/utils" - "stackit/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/credentials/list/list_test.go b/internal/cmd/opensearch/credentials/list/list_test.go index a9ce8bf6d..4c503e18f 100644 --- a/internal/cmd/opensearch/credentials/list/list_test.go +++ b/internal/cmd/opensearch/credentials/list/list_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index bf442e16e..d9316ad19 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -6,17 +6,17 @@ import ( "fmt" "strings" - "stackit/internal/pkg/args" - "stackit/internal/pkg/confirm" - cliErr "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/projectname" - "stackit/internal/pkg/services/opensearch/client" - opensearchUtils "stackit/internal/pkg/services/opensearch/utils" - "stackit/internal/pkg/spinner" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/instance/create/create_test.go b/internal/cmd/opensearch/instance/create/create_test.go index 178e8daf4..881f896eb 100644 --- a/internal/cmd/opensearch/instance/create/create_test.go +++ b/internal/cmd/opensearch/instance/create/create_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/instance/delete/delete.go b/internal/cmd/opensearch/instance/delete/delete.go index a3cda12cb..e63769c7d 100644 --- a/internal/cmd/opensearch/instance/delete/delete.go +++ b/internal/cmd/opensearch/instance/delete/delete.go @@ -4,15 +4,15 @@ import ( "context" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/confirm" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - opensearchUtils "stackit/internal/pkg/services/opensearch/utils" - "stackit/internal/pkg/spinner" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/instance/delete/delete_test.go b/internal/cmd/opensearch/instance/delete/delete_test.go index b02a77a87..3f6042eb9 100644 --- a/internal/cmd/opensearch/instance/delete/delete_test.go +++ b/internal/cmd/opensearch/instance/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/instance/describe/describe.go b/internal/cmd/opensearch/instance/describe/describe.go index d6bd6c7d3..d63c62fc1 100644 --- a/internal/cmd/opensearch/instance/describe/describe.go +++ b/internal/cmd/opensearch/instance/describe/describe.go @@ -5,13 +5,13 @@ import ( "encoding/json" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - "stackit/internal/pkg/tables" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/instance/describe/describe_test.go b/internal/cmd/opensearch/instance/describe/describe_test.go index 13a077eb0..6881ec419 100644 --- a/internal/cmd/opensearch/instance/describe/describe_test.go +++ b/internal/cmd/opensearch/instance/describe/describe_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/instance/instance.go b/internal/cmd/opensearch/instance/instance.go index 62f356352..e72c032bf 100644 --- a/internal/cmd/opensearch/instance/instance.go +++ b/internal/cmd/opensearch/instance/instance.go @@ -1,13 +1,13 @@ package instance import ( - "stackit/internal/cmd/opensearch/instance/create" - "stackit/internal/cmd/opensearch/instance/delete" - "stackit/internal/cmd/opensearch/instance/describe" - "stackit/internal/cmd/opensearch/instance/list" - "stackit/internal/cmd/opensearch/instance/update" - "stackit/internal/pkg/args" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) diff --git a/internal/cmd/opensearch/instance/list/list.go b/internal/cmd/opensearch/instance/list/list.go index 6aa442ef7..f305dac11 100644 --- a/internal/cmd/opensearch/instance/list/list.go +++ b/internal/cmd/opensearch/instance/list/list.go @@ -5,14 +5,14 @@ import ( "encoding/json" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/projectname" - "stackit/internal/pkg/services/opensearch/client" - "stackit/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/instance/list/list_test.go b/internal/cmd/opensearch/instance/list/list_test.go index a16f48d1d..17f77beb8 100644 --- a/internal/cmd/opensearch/instance/list/list_test.go +++ b/internal/cmd/opensearch/instance/list/list_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go index 5f0e59e37..1fddac58b 100644 --- a/internal/cmd/opensearch/instance/update/update.go +++ b/internal/cmd/opensearch/instance/update/update.go @@ -6,16 +6,16 @@ import ( "fmt" "strings" - "stackit/internal/pkg/args" - "stackit/internal/pkg/confirm" - cliErr "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/services/opensearch/client" - opensearchUtils "stackit/internal/pkg/services/opensearch/utils" - "stackit/internal/pkg/spinner" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/instance/update/update_test.go b/internal/cmd/opensearch/instance/update/update_test.go index 5154554cf..39ab6b130 100644 --- a/internal/cmd/opensearch/instance/update/update_test.go +++ b/internal/cmd/opensearch/instance/update/update_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/cmd/opensearch/opensearch.go b/internal/cmd/opensearch/opensearch.go index 42239c71e..93494fbc7 100644 --- a/internal/cmd/opensearch/opensearch.go +++ b/internal/cmd/opensearch/opensearch.go @@ -1,11 +1,11 @@ package opensearch import ( - "stackit/internal/cmd/opensearch/credentials" - "stackit/internal/cmd/opensearch/instance" - "stackit/internal/cmd/opensearch/plans" - "stackit/internal/pkg/args" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/plans" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) diff --git a/internal/cmd/opensearch/plans/plans.go b/internal/cmd/opensearch/plans/plans.go index 54b852595..5f71edbc3 100644 --- a/internal/cmd/opensearch/plans/plans.go +++ b/internal/cmd/opensearch/plans/plans.go @@ -5,14 +5,14 @@ import ( "encoding/json" "fmt" - "stackit/internal/pkg/args" - "stackit/internal/pkg/errors" - "stackit/internal/pkg/examples" - "stackit/internal/pkg/flags" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/projectname" - "stackit/internal/pkg/services/opensearch/client" - "stackit/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" diff --git a/internal/cmd/opensearch/plans/plans_test.go b/internal/cmd/opensearch/plans/plans_test.go index 8361216f8..d0f656b7f 100644 --- a/internal/cmd/opensearch/plans/plans_test.go +++ b/internal/cmd/opensearch/plans/plans_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "stackit/internal/pkg/globalflags" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" diff --git a/internal/pkg/services/opensearch/client/client.go b/internal/pkg/services/opensearch/client/client.go index cf338d121..6f9852f56 100644 --- a/internal/pkg/services/opensearch/client/client.go +++ b/internal/pkg/services/opensearch/client/client.go @@ -1,9 +1,9 @@ package client import ( - "stackit/internal/pkg/auth" - "stackit/internal/pkg/config" - "stackit/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/internal/pkg/services/opensearch/utils/utils.go b/internal/pkg/services/opensearch/utils/utils.go index 9d080e421..9287532c6 100644 --- a/internal/pkg/services/opensearch/utils/utils.go +++ b/internal/pkg/services/opensearch/utils/utils.go @@ -3,9 +3,10 @@ package utils import ( "context" "fmt" - "stackit/internal/pkg/errors" "strings" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" ) diff --git a/internal/pkg/services/opensearch/utils/utils_test.go b/internal/pkg/services/opensearch/utils/utils_test.go index b0c2e85a6..f46e2f9cf 100644 --- a/internal/pkg/services/opensearch/utils/utils_test.go +++ b/internal/pkg/services/opensearch/utils/utils_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - "stackit/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/uuid" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" From e01f88a58b4e32db6511055c0a88eada8c518e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Fri, 2 Feb 2024 17:15:57 +0000 Subject: [PATCH 08/11] Adjustments after review --- .../cmd/opensearch/credentials/delete/delete.go | 2 +- .../cmd/opensearch/instance/create/create.go | 17 +++++++++-------- .../opensearch/instance/create/create_test.go | 4 ++-- .../cmd/opensearch/instance/update/update.go | 17 +++++++++-------- .../opensearch/instance/update/update_test.go | 4 ++-- internal/cmd/root.go | 1 + internal/pkg/services/opensearch/utils/utils.go | 6 ++++-- .../pkg/services/opensearch/utils/utils_test.go | 4 ++-- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/internal/cmd/opensearch/credentials/delete/delete.go b/internal/cmd/opensearch/credentials/delete/delete.go index e1edafa65..e7f08ebf0 100644 --- a/internal/cmd/opensearch/credentials/delete/delete.go +++ b/internal/cmd/opensearch/credentials/delete/delete.go @@ -59,7 +59,7 @@ func NewCmd() *cobra.Command { instanceLabel = model.InstanceId } - credentialLabel, err := opensearchUtils.GetCredentialUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialLabel, err := opensearchUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) if err != nil { credentialLabel = model.CredentialsId } diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index d9316ad19..5c0731785 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -74,10 +74,7 @@ func NewCmd() *cobra.Command { ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - // Service name and operation needed for error handling - service := "opensearch" - operation := cmd.Use - model, err := parseInput(cmd, service, operation) + model, err := parseInput(cmd) if err != nil { return err } @@ -102,7 +99,7 @@ func NewCmd() *cobra.Command { } // Call API - req, err := buildRequest(ctx, service, model, apiClient) + req, err := buildRequest(ctx, model, apiClient) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -157,7 +154,10 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(cmd *cobra.Command, service, operation string) (*inputModel, error) { +func parseInput(cmd *cobra.Command) (*inputModel, error) { + service := "opensearch" + operation := cmd.Use + globalFlags := globalflags.Parse(cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -202,7 +202,8 @@ type OpenSearchClient interface { ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) } -func buildRequest(ctx context.Context, service string, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiCreateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiCreateInstanceRequest, error) { + service := "opensearch" req := apiClient.CreateInstance(ctx, model.ProjectId) var planId *string @@ -214,7 +215,7 @@ func buildRequest(ctx context.Context, service string, model *inputModel, apiCli } if model.PlanId == nil { - planId, err = opensearchUtils.LoadPlanId(service, model.PlanName, model.Version, offerings) + planId, err = opensearchUtils.LoadPlanId(model.PlanName, model.Version, offerings) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { diff --git a/internal/cmd/opensearch/instance/create/create_test.go b/internal/cmd/opensearch/instance/create/create_test.go index 881f896eb..6a6679242 100644 --- a/internal/cmd/opensearch/instance/create/create_test.go +++ b/internal/cmd/opensearch/instance/create/create_test.go @@ -326,7 +326,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(cmd, "opensearch", "create") + model, err := parseInput(cmd) if err != nil { if !tt.isValid { return @@ -464,7 +464,7 @@ func TestBuildRequest(t *testing.T) { returnError: tt.getOfferingsFails, listOfferingsResp: tt.getOfferingsResp, } - request, err := buildRequest(testCtx, "opensearch", tt.model, client) + request, err := buildRequest(testCtx, tt.model, client) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go index 1fddac58b..61f0a03a8 100644 --- a/internal/cmd/opensearch/instance/update/update.go +++ b/internal/cmd/opensearch/instance/update/update.go @@ -72,10 +72,7 @@ func NewCmd() *cobra.Command { ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - // Service name and operation needed for error handling - service := "opensearch" - operation := cmd.Use - model, err := parseInput(cmd, args, service, operation) + model, err := parseInput(cmd, args) if err != nil { return err } @@ -100,7 +97,7 @@ func NewCmd() *cobra.Command { } // Call API - req, err := buildRequest(ctx, service, model, apiClient) + req, err := buildRequest(ctx, model, apiClient) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -155,7 +152,9 @@ func configureFlags(cmd *cobra.Command) { cmd.MarkFlagsRequiredTogether(planNameFlag, versionFlag) } -func parseInput(cmd *cobra.Command, inputArgs []string, service, operation string) (*inputModel, error) { +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + service := "opensearch" + operation := cmd.Use instanceId := inputArgs[0] globalFlags := globalflags.Parse(cmd) @@ -211,7 +210,9 @@ type OpenSearchClient interface { ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) } -func buildRequest(ctx context.Context, service string, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiPartialUpdateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiPartialUpdateInstanceRequest, error) { + service := "opensearch" + req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) var planId *string @@ -223,7 +224,7 @@ func buildRequest(ctx context.Context, service string, model *inputModel, apiCli } if model.PlanId == nil && model.PlanName != "" && model.Version != "" { - planId, err = opensearchUtils.LoadPlanId(service, model.PlanName, model.Version, offerings) + planId, err = opensearchUtils.LoadPlanId(model.PlanName, model.Version, offerings) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { diff --git a/internal/cmd/opensearch/instance/update/update_test.go b/internal/cmd/opensearch/instance/update/update_test.go index 39ab6b130..5141e905e 100644 --- a/internal/cmd/opensearch/instance/update/update_test.go +++ b/internal/cmd/opensearch/instance/update/update_test.go @@ -340,7 +340,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(cmd, tt.argValues, "opensearch", "update") + model, err := parseInput(cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -465,7 +465,7 @@ func TestBuildRequest(t *testing.T) { returnError: tt.getOfferingsFails, listOfferingsResp: tt.listOfferingsResp, } - request, err := buildRequest(testCtx, "opensearch", tt.model, client) + request, err := buildRequest(testCtx, tt.model, client) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 4dab9d315..7c004d117 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/curl" "github.com/stackitcloud/stackit-cli/internal/cmd/dns" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex" + "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch" "github.com/stackitcloud/stackit-cli/internal/cmd/organization" "github.com/stackitcloud/stackit-cli/internal/cmd/project" serviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/service-account" diff --git a/internal/pkg/services/opensearch/utils/utils.go b/internal/pkg/services/opensearch/utils/utils.go index 9287532c6..6d75de4e8 100644 --- a/internal/pkg/services/opensearch/utils/utils.go +++ b/internal/pkg/services/opensearch/utils/utils.go @@ -25,7 +25,9 @@ func ValidatePlanId(service, planId string, offerings *opensearch.ListOfferingsR } } -func LoadPlanId(service, planName, version string, offerings *opensearch.ListOfferingsResponse) (*string, error) { +func LoadPlanId(planName, version string, offerings *opensearch.ListOfferingsResponse) (*string, error) { + service := "opensearch" + availableVersions := "" availablePlanNames := "" isValidVersion := false @@ -74,7 +76,7 @@ func GetInstanceName(ctx context.Context, apiClient OpenSearchClient, projectId, return *resp.Name, nil } -func GetCredentialUsername(ctx context.Context, apiClient OpenSearchClient, projectId, instanceId, credentialsId string) (string, error) { +func GetCredentialsUsername(ctx context.Context, apiClient OpenSearchClient, projectId, instanceId, credentialsId string) (string, error) { resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) if err != nil { return "", fmt.Errorf("get OpenSearch credentials: %w", err) diff --git a/internal/pkg/services/opensearch/utils/utils_test.go b/internal/pkg/services/opensearch/utils/utils_test.go index f46e2f9cf..8ebe3391b 100644 --- a/internal/pkg/services/opensearch/utils/utils_test.go +++ b/internal/pkg/services/opensearch/utils/utils_test.go @@ -91,7 +91,7 @@ func TestGetInstanceName(t *testing.T) { } } -func TestCredentialUsername(t *testing.T) { +func TestGetCredentialsUsername(t *testing.T) { tests := []struct { description string getCredentialsFails bool @@ -125,7 +125,7 @@ func TestCredentialUsername(t *testing.T) { getCredentialsResp: tt.getCredentialsResp, } - output, err := GetCredentialUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) if tt.isValid && err != nil { t.Errorf("failed on valid input") From 6d2d1b83c505da58536dcd542ad4c7dfe1d6f37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Mon, 5 Feb 2024 09:42:26 +0000 Subject: [PATCH 09/11] Make OpenSearch client interface private and credentials plural --- docs/stackit_opensearch_credentials_delete.md | 2 +- docs/stackit_opensearch_credentials_describe.md | 2 +- docs/stackit_opensearch_instance_create.md | 2 +- internal/cmd/opensearch/credentials/create/create.go | 4 ++-- internal/cmd/opensearch/credentials/delete/delete.go | 10 +++++----- .../cmd/opensearch/credentials/describe/describe.go | 2 +- internal/cmd/opensearch/instance/create/create.go | 4 ++-- internal/cmd/opensearch/instance/update/update.go | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/stackit_opensearch_credentials_delete.md b/docs/stackit_opensearch_credentials_delete.md index 3380c62ea..edc0a3c7e 100644 --- a/docs/stackit_opensearch_credentials_delete.md +++ b/docs/stackit_opensearch_credentials_delete.md @@ -7,7 +7,7 @@ Delete credentials of an OpenSearch instance Delete credentials of an OpenSearch instance ``` -stackit opensearch credentials delete CREDENTIAL_ID [flags] +stackit opensearch credentials delete CREDENTIALS_ID [flags] ``` ### Examples diff --git a/docs/stackit_opensearch_credentials_describe.md b/docs/stackit_opensearch_credentials_describe.md index ddc514fda..93e44c81b 100644 --- a/docs/stackit_opensearch_credentials_describe.md +++ b/docs/stackit_opensearch_credentials_describe.md @@ -7,7 +7,7 @@ Get details of credentials of an OpenSearch instance Get details of credentials of an OpenSearch instance. The password will be shown in plain text in the output. ``` -stackit opensearch credentials describe CREDENTIAL_ID [flags] +stackit opensearch credentials describe CREDENTIALS_ID [flags] ``` ### Examples diff --git a/docs/stackit_opensearch_instance_create.md b/docs/stackit_opensearch_instance_create.md index 456ed3908..c66c80d8c 100644 --- a/docs/stackit_opensearch_instance_create.md +++ b/docs/stackit_opensearch_instance_create.md @@ -14,7 +14,7 @@ stackit opensearch instance create [flags] ``` Create an OpenSearch instance with name "my-instance" and specify plan by name and version - $ stackit opensearch instance create --name my-instance --plan-name stackit-qa-opensearch-1.2.10-replica --version 2 + $ stackit opensearch instance create --name my-instance --plan-name stackit-opensearch-1.2.10-replica --version 2 Create an OpenSearch instance with name "my-instance" and specify plan by ID $ stackit opensearch instance create --name my-instance --plan-id xxx diff --git a/internal/cmd/opensearch/credentials/create/create.go b/internal/cmd/opensearch/credentials/create/create.go index 87cafa68d..2ae98a4f7 100644 --- a/internal/cmd/opensearch/credentials/create/create.go +++ b/internal/cmd/opensearch/credentials/create/create.go @@ -61,7 +61,7 @@ func NewCmd() *cobra.Command { } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a credential for instance %s?", instanceLabel) + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %s?", instanceLabel) err = confirm.PromptForConfirmation(cmd, prompt) if err != nil { return err @@ -75,7 +75,7 @@ func NewCmd() *cobra.Command { return fmt.Errorf("create OpenSearch credentials: %w", err) } - cmd.Printf("Created credential for instance %s. Credential ID: %s\n\n", instanceLabel, *resp.Id) + cmd.Printf("Created credentials for instance %s. Credentials ID: %s\n\n", instanceLabel, *resp.Id) cmd.Printf("Username: %s\n", *resp.Raw.Credentials.Username) if model.HidePassword { cmd.Printf("Password: \n") diff --git a/internal/cmd/opensearch/credentials/delete/delete.go b/internal/cmd/opensearch/credentials/delete/delete.go index e7f08ebf0..676c6752a 100644 --- a/internal/cmd/opensearch/credentials/delete/delete.go +++ b/internal/cmd/opensearch/credentials/delete/delete.go @@ -19,7 +19,7 @@ import ( ) const ( - credentialsIdArg = "CREDENTIAL_ID" //nolint:gosec // linter false positive + credentialsIdArg = "CREDENTIALS_ID" //nolint:gosec // linter false positive instanceIdFlag = "instance-id" ) @@ -59,13 +59,13 @@ func NewCmd() *cobra.Command { instanceLabel = model.InstanceId } - credentialLabel, err := opensearchUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialsLabel, err := opensearchUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) if err != nil { - credentialLabel = model.CredentialsId + credentialsLabel = model.CredentialsId } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credential %s of instance %s? (This cannot be undone)", credentialLabel, instanceLabel) + prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %s? (This cannot be undone)", credentialsLabel, instanceLabel) err = confirm.PromptForConfirmation(cmd, prompt) if err != nil { return err @@ -79,7 +79,7 @@ func NewCmd() *cobra.Command { return fmt.Errorf("delete OpenSearch credentials: %w", err) } - cmd.Printf("Deleted credential %s of instance %s\n", credentialLabel, instanceLabel) + cmd.Printf("Deleted credentials %s of instance %s\n", credentialsLabel, instanceLabel) return nil }, } diff --git a/internal/cmd/opensearch/credentials/describe/describe.go b/internal/cmd/opensearch/credentials/describe/describe.go index 177967cb4..75782debc 100644 --- a/internal/cmd/opensearch/credentials/describe/describe.go +++ b/internal/cmd/opensearch/credentials/describe/describe.go @@ -19,7 +19,7 @@ import ( ) const ( - credentialsIdArg = "CREDENTIAL_ID" //nolint:gosec // linter false positive + credentialsIdArg = "CREDENTIALS_ID" //nolint:gosec // linter false positive instanceIdFlag = "instance-id" ) diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index 5c0731785..1ed3694ea 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -197,12 +197,12 @@ func parseInput(cmd *cobra.Command) (*inputModel, error) { }, nil } -type OpenSearchClient interface { +type openSearchClient interface { CreateInstance(ctx context.Context, projectId string) opensearch.ApiCreateInstanceRequest ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) } -func buildRequest(ctx context.Context, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiCreateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchClient) (opensearch.ApiCreateInstanceRequest, error) { service := "opensearch" req := apiClient.CreateInstance(ctx, model.ProjectId) diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go index 61f0a03a8..af7bc5bb3 100644 --- a/internal/cmd/opensearch/instance/update/update.go +++ b/internal/cmd/opensearch/instance/update/update.go @@ -205,12 +205,12 @@ func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { }, nil } -type OpenSearchClient interface { +type openSearchClient interface { PartialUpdateInstance(ctx context.Context, projectId, instanceId string) opensearch.ApiPartialUpdateInstanceRequest ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) } -func buildRequest(ctx context.Context, model *inputModel, apiClient OpenSearchClient) (opensearch.ApiPartialUpdateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchClient) (opensearch.ApiPartialUpdateInstanceRequest, error) { service := "opensearch" req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) From c68605faa0d59f769d210e7298bf92cd0a470053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Mon, 5 Feb 2024 09:46:32 +0000 Subject: [PATCH 10/11] Rename last usages of credential to credentials --- .../cmd/opensearch/credentials/delete/delete_test.go | 4 ++-- .../cmd/opensearch/credentials/describe/describe.go | 12 ++++++------ .../opensearch/credentials/describe/describe_test.go | 4 ++-- internal/cmd/ske/credentials/describe/describe.go | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/cmd/opensearch/credentials/delete/delete_test.go b/internal/cmd/opensearch/credentials/delete/delete_test.go index af37bcc2d..ad1a9a41f 100644 --- a/internal/cmd/opensearch/credentials/delete/delete_test.go +++ b/internal/cmd/opensearch/credentials/delete/delete_test.go @@ -147,13 +147,13 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "credentail id invalid 1", + description: "credentials id invalid 1", argValues: []string{""}, flagValues: fixtureFlagValues(), isValid: false, }, { - description: "credential id invalid 2", + description: "credentials id invalid 2", argValues: []string{"invalid-uuid"}, flagValues: fixtureFlagValues(), isValid: false, diff --git a/internal/cmd/opensearch/credentials/describe/describe.go b/internal/cmd/opensearch/credentials/describe/describe.go index 75782debc..0ff4ee7bd 100644 --- a/internal/cmd/opensearch/credentials/describe/describe.go +++ b/internal/cmd/opensearch/credentials/describe/describe.go @@ -98,17 +98,17 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch. return req } -func outputResult(cmd *cobra.Command, outputFormat string, credential *opensearch.CredentialsResponse) error { +func outputResult(cmd *cobra.Command, outputFormat string, credentials *opensearch.CredentialsResponse) error { switch outputFormat { case globalflags.PrettyOutputFormat: table := tables.NewTable() - table.AddRow("ID", *credential.Id) + table.AddRow("ID", *credentials.Id) table.AddSeparator() - table.AddRow("USERNAME", *credential.Raw.Credentials.Username) + table.AddRow("USERNAME", *credentials.Raw.Credentials.Username) table.AddSeparator() - table.AddRow("PASSWORD", *credential.Raw.Credentials.Password) + table.AddRow("PASSWORD", *credentials.Raw.Credentials.Password) table.AddSeparator() - table.AddRow("URI", *credential.Raw.Credentials.Uri) + table.AddRow("URI", *credentials.Raw.Credentials.Uri) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) @@ -116,7 +116,7 @@ func outputResult(cmd *cobra.Command, outputFormat string, credential *opensearc return nil default: - details, err := json.MarshalIndent(credential, "", " ") + details, err := json.MarshalIndent(credentials, "", " ") if err != nil { return fmt.Errorf("marshal OpenSearch credentials: %w", err) } diff --git a/internal/cmd/opensearch/credentials/describe/describe_test.go b/internal/cmd/opensearch/credentials/describe/describe_test.go index 5bdb935c6..e72ed1694 100644 --- a/internal/cmd/opensearch/credentials/describe/describe_test.go +++ b/internal/cmd/opensearch/credentials/describe/describe_test.go @@ -147,13 +147,13 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "credentail id invalid 1", + description: "credentials id invalid 1", argValues: []string{""}, flagValues: fixtureFlagValues(), isValid: false, }, { - description: "credential id invalid 2", + description: "credentials id invalid 2", argValues: []string{"invalid-uuid"}, flagValues: fixtureFlagValues(), isValid: false, diff --git a/internal/cmd/ske/credentials/describe/describe.go b/internal/cmd/ske/credentials/describe/describe.go index 85a985dcf..65aceb7fd 100644 --- a/internal/cmd/ske/credentials/describe/describe.go +++ b/internal/cmd/ske/credentials/describe/describe.go @@ -94,13 +94,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClie return req } -func outputResult(cmd *cobra.Command, outputFormat string, credential *ske.Credentials) error { +func outputResult(cmd *cobra.Command, outputFormat string, credentials *ske.Credentials) error { switch outputFormat { case globalflags.PrettyOutputFormat: table := tables.NewTable() - table.AddRow("SERVER", *credential.Server) + table.AddRow("SERVER", *credentials.Server) table.AddSeparator() - table.AddRow("TOKEN", *credential.Token) + table.AddRow("TOKEN", *credentials.Token) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) @@ -108,7 +108,7 @@ func outputResult(cmd *cobra.Command, outputFormat string, credential *ske.Crede return nil default: - details, err := json.MarshalIndent(credential, "", " ") + details, err := json.MarshalIndent(credentials, "", " ") if err != nil { return fmt.Errorf("marshal SKE credentials: %w", err) } From 666c9875c98b80a7b742267e161fed607723c73a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Mon, 5 Feb 2024 11:44:16 +0000 Subject: [PATCH 11/11] Remove restrictions from configureFlags to show custom errors in parseInput --- internal/cmd/opensearch/instance/update/update.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go index af7bc5bb3..579b330cd 100644 --- a/internal/cmd/opensearch/instance/update/update.go +++ b/internal/cmd/opensearch/instance/update/update.go @@ -146,10 +146,6 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") cmd.Flags().String(planNameFlag, "", "Plan name") cmd.Flags().String(versionFlag, "", "Instance OpenSearch version") - - cmd.MarkFlagsMutuallyExclusive(planIdFlag, planNameFlag) - cmd.MarkFlagsMutuallyExclusive(planIdFlag, versionFlag) - cmd.MarkFlagsRequiredTogether(planNameFlag, versionFlag) } func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) {