diff --git a/docs/stackit_mongodbflex.md b/docs/stackit_mongodbflex.md index 2d741af26..c42180c7b 100644 --- a/docs/stackit_mongodbflex.md +++ b/docs/stackit_mongodbflex.md @@ -31,7 +31,9 @@ stackit mongodbflex [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups +* [stackit mongodbflex flavor](./stackit_mongodbflex_flavor.md) - Provides functionality for MongoDB Flex flavors * [stackit mongodbflex instance](./stackit_mongodbflex_instance.md) - Provides functionality for MongoDB Flex instances -* [stackit mongodbflex options](./stackit_mongodbflex_options.md) - Lists MongoDB Flex options +* [stackit mongodbflex storage](./stackit_mongodbflex_storage.md) - Provides functionality for MongoDB Flex storages for a certain flavor * [stackit mongodbflex user](./stackit_mongodbflex_user.md) - Provides functionality for MongoDB Flex users +* [stackit mongodbflex version](./stackit_mongodbflex_version.md) - Provides functionality for MongoDB Flex versions diff --git a/docs/stackit_mongodbflex_flavor.md b/docs/stackit_mongodbflex_flavor.md new file mode 100644 index 000000000..8e3e3e926 --- /dev/null +++ b/docs/stackit_mongodbflex_flavor.md @@ -0,0 +1,34 @@ +## stackit mongodbflex flavor + +Provides functionality for MongoDB Flex flavors + +### Synopsis + +Provides functionality for MongoDB Flex flavors. + +``` +stackit mongodbflex flavor [flags] +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex flavor" +``` + +### 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, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex +* [stackit mongodbflex flavor list](./stackit_mongodbflex_flavor_list.md) - Lists MongoDB Flex flavors + diff --git a/docs/stackit_mongodbflex_flavor_list.md b/docs/stackit_mongodbflex_flavor_list.md new file mode 100644 index 000000000..732c0327e --- /dev/null +++ b/docs/stackit_mongodbflex_flavor_list.md @@ -0,0 +1,41 @@ +## stackit mongodbflex flavor list + +Lists MongoDB Flex flavors + +### Synopsis + +Lists MongoDB Flex flavors. + +``` +stackit mongodbflex flavor list [flags] +``` + +### Examples + +``` + List MongoDB Flex flavor + $ stackit mongodbflex flavor list +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex flavor 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, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex flavor](./stackit_mongodbflex_flavor.md) - Provides functionality for MongoDB Flex flavors + diff --git a/docs/stackit_mongodbflex_instance_create.md b/docs/stackit_mongodbflex_instance_create.md index 13b67ea65..e6c9d81e7 100644 --- a/docs/stackit_mongodbflex_instance_create.md +++ b/docs/stackit_mongodbflex_instance_create.md @@ -13,30 +13,25 @@ stackit mongodbflex instance create [flags] ### Examples ``` - Create a MongoDB Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by CPU and RAM. Other parameters are set to default values - $ stackit mongodbflex instance create --name my-instance --cpu 1 --ram 4 --acl 0.0.0.0/0 + Create a MongoDB Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access). + $ stackit mongodbflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 --type Replica --storage-size 20 --version 8.0 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-mongodb - Create a MongoDB Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by ID. Other parameters are set to default values - $ stackit mongodbflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 - - Create a MongoDB Flex instance with name "my-instance", allow access to a specific range of IP addresses, specify flavor by CPU and RAM and set storage size to 20 GB. Other parameters are set to default values - $ stackit mongodbflex instance create --name my-instance --cpu 1 --ram 4 --acl 1.2.3.0/24 --storage-size 20 + Create a MongoDB Flex instance with name "my-instance", allow access to a specific range of IP addresses. + $ stackit mongodbflex instance create --name my-instance --flavor-id xxx --acl 1.2.3.0/24 --type Replica --storage-size 20 --version 8.0 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-mongodb ``` ### Options ``` --acl strings The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc. (default []) - --backup-schedule string Backup schedule (default "0 0/6 * * *") - --cpu int32 Number of CPUs - --flavor-id string ID of the flavor + --backup-schedule string Backup schedule. This flag will be required after 2027-03-07. (default "0 0/6 * * *") + --flavor-id string ID of the flavor. This flag will be required after 2027-03-07. -h, --help Help for "stackit mongodbflex instance create" -n, --name string Instance name - --ram int32 Amount of RAM (in GB) - --storage-class string Storage class (default "premium-perf2-mongodb") - --storage-size int Storage size (in GB) (default 10) + --storage-class string Storage class. This flag will be required after 2027-03-07. (default "premium-perf2-mongodb") + --storage-size int Storage size (in GB). This flag will be required after 2027-03-07. (default 10) --type string Instance type, (one of: [Replica, Sharded, Single]) (default "Replica") - --version string MongoDB version. Defaults to the latest version available + --version string MongoDB version. Defaults to the latest version available. This flag will be required after 2027-03-07. ``` ### Options inherited from parent commands diff --git a/docs/stackit_mongodbflex_instance_update.md b/docs/stackit_mongodbflex_instance_update.md index 44921ddd8..159593f7d 100644 --- a/docs/stackit_mongodbflex_instance_update.md +++ b/docs/stackit_mongodbflex_instance_update.md @@ -17,7 +17,7 @@ stackit mongodbflex instance update INSTANCE_ID [flags] $ stackit mongodbflex instance update xxx --name my-new-name Update the version of a MongoDB Flex instance - $ stackit mongodbflex instance update xxx --version 6.0 + $ stackit mongodbflex instance update xxx --version 8.0 ``` ### Options @@ -25,11 +25,9 @@ stackit mongodbflex instance update INSTANCE_ID [flags] ``` --acl strings Lists of IP networks in CIDR notation which are allowed to access this instance (default []) --backup-schedule string Backup schedule - --cpu int32 Number of CPUs --flavor-id string ID of the flavor -h, --help Help for "stackit mongodbflex instance update" -n, --name string Instance name - --ram int32 Amount of RAM (in GB) --storage-class string Storage class --storage-size int Storage size (in GB) --type string Instance type, (one of: [Replica, Sharded, Single]) diff --git a/docs/stackit_mongodbflex_options.md b/docs/stackit_mongodbflex_options.md deleted file mode 100644 index 31ad5fa7f..000000000 --- a/docs/stackit_mongodbflex_options.md +++ /dev/null @@ -1,51 +0,0 @@ -## stackit mongodbflex options - -Lists MongoDB Flex options - -### Synopsis - -Lists MongoDB Flex options (flavors, versions and storages for a given flavor) -Pass one or more flags to filter what categories are shown. - -``` -stackit mongodbflex options [flags] -``` - -### Examples - -``` - List MongoDB Flex flavors options - $ stackit mongodbflex options --flavors - - List MongoDB Flex available versions - $ stackit mongodbflex options --versions - - List MongoDB Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit mongodbflex options --flavors" - $ stackit mongodbflex options --storages --flavor-id -``` - -### Options - -``` - --flavor-id string The flavor ID to show storages for. Only relevant when "--storages" is passed - --flavors Lists supported flavors - -h, --help Help for "stackit mongodbflex options" - --storages Lists supported storages for a given flavor - --versions Lists supported versions -``` - -### 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, none, yaml]) - -p, --project-id string Project ID - --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") -``` - -### SEE ALSO - -* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex - diff --git a/docs/stackit_mongodbflex_storage.md b/docs/stackit_mongodbflex_storage.md new file mode 100644 index 000000000..d66631768 --- /dev/null +++ b/docs/stackit_mongodbflex_storage.md @@ -0,0 +1,34 @@ +## stackit mongodbflex storage + +Provides functionality for MongoDB Flex storages for a certain flavor + +### Synopsis + +Provides functionality for MongoDB Flex storages for a certain flavor. + +``` +stackit mongodbflex storage [flags] +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex storage" +``` + +### 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, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex +* [stackit mongodbflex storage list](./stackit_mongodbflex_storage_list.md) - Lists MongoDB Flex storages for a certain flavor + diff --git a/docs/stackit_mongodbflex_storage_list.md b/docs/stackit_mongodbflex_storage_list.md new file mode 100644 index 000000000..b308ce96c --- /dev/null +++ b/docs/stackit_mongodbflex_storage_list.md @@ -0,0 +1,48 @@ +## stackit mongodbflex storage list + +Lists MongoDB Flex storages for a certain flavor + +### Synopsis + +Lists MongoDB Flex storages for a certain flavor. + +``` +stackit mongodbflex storage list [flags] +``` + +### Examples + +``` + List MongoDB Flex storages for flavor with ID "xxx" + $ stackit mongodbflex storage list --flavor-id xxx + + List MongoDB Flex storages for flavor with ID "xxx" in JSON format + $ stackit mongodbflex storage list --flavor-id xxx --output-format json + + List up to 10 storages for flavor with ID "xxx" + $ stackit mongodbflex storage list --flavor-id xxx --limit 10 +``` + +### Options + +``` + --flavor-id string Flavor ID + -h, --help Help for "stackit mongodbflex storage 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, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex storage](./stackit_mongodbflex_storage.md) - Provides functionality for MongoDB Flex storages for a certain flavor + diff --git a/docs/stackit_mongodbflex_version.md b/docs/stackit_mongodbflex_version.md new file mode 100644 index 000000000..5494a5889 --- /dev/null +++ b/docs/stackit_mongodbflex_version.md @@ -0,0 +1,34 @@ +## stackit mongodbflex version + +Provides functionality for MongoDB Flex versions + +### Synopsis + +Provides functionality for MongoDB Flex versions. + +``` +stackit mongodbflex version [flags] +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex 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, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex +* [stackit mongodbflex version list](./stackit_mongodbflex_version_list.md) - Lists MongoDB Flex versions + diff --git a/docs/stackit_mongodbflex_version_list.md b/docs/stackit_mongodbflex_version_list.md new file mode 100644 index 000000000..9508c1250 --- /dev/null +++ b/docs/stackit_mongodbflex_version_list.md @@ -0,0 +1,40 @@ +## stackit mongodbflex version list + +Lists MongoDB Flex versions + +### Synopsis + +Lists MongoDB Flex versions. + +``` +stackit mongodbflex version list [flags] +``` + +### Examples + +``` + List MongoDB Flex version options + $ stackit mongodbflex version list +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex version 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, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex version](./stackit_mongodbflex_version.md) - Provides functionality for MongoDB Flex versions + diff --git a/internal/cmd/mongodbflex/flavor/flavor.go b/internal/cmd/mongodbflex/flavor/flavor.go new file mode 100644 index 000000000..c34da77e6 --- /dev/null +++ b/internal/cmd/mongodbflex/flavor/flavor.go @@ -0,0 +1,26 @@ +package flavor + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/flavor/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "flavor", + Short: "Provides functionality for MongoDB Flex flavors", + Long: "Provides functionality for MongoDB Flex flavors.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/mongodbflex/flavor/list/list.go b/internal/cmd/mongodbflex/flavor/list/list.go new file mode 100644 index 000000000..372d89491 --- /dev/null +++ b/internal/cmd/mongodbflex/flavor/list/list.go @@ -0,0 +1,124 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + 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/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists MongoDB Flex flavors", + Long: "Lists MongoDB Flex flavors.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List MongoDB Flex flavor`, + "$ stackit mongodbflex flavor list"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + flavors, err := buildRequest(ctx, model, apiClient.DefaultAPI).Execute() + if err != nil { + return fmt.Errorf("get MongoDB Flex flavors: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, flavors.Flavors) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient mongodbflex.DefaultAPI) mongodbflex.ApiListFlavorsRequest { + return apiClient.ListFlavors(ctx, model.ProjectId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, flavors []mongodbflex.InstanceFlavor) error { + return p.OutputResult(outputFormat, flavors, func() error { + if len(flavors) == 0 { + p.Outputf("No MongoDB flavors found.") + return nil + } + + table := tables.NewTable() + table.SetTitle("Flavors") + table.SetHeader("ID", "CPU", "MEMORY", "DESCRIPTION", "VALID INSTANCE TYPES") + for _, f := range flavors { + table.AddRow( + utils.PtrString(f.Id), + utils.PtrString(f.Cpu), + utils.PtrString(f.Memory), + utils.PtrString(f.Description), + f.Categories, + ) + table.AddSeparator() + } + + return table.Display(p) + }) +} diff --git a/internal/cmd/mongodbflex/flavor/list/list_test.go b/internal/cmd/mongodbflex/flavor/list/list_test.go new file mode 100644 index 000000000..2ac82d80d --- /dev/null +++ b/internal/cmd/mongodbflex/flavor/list/list_test.go @@ -0,0 +1,165 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiListFlavorsRequest)) mongodbflex.ApiListFlavorsRequest { + request := testClient.DefaultAPI.ListFlavors(testCtx, testProjectId, testRegion) + 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", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiListFlavorsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + flavors []mongodbflex.InstanceFlavor + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "flavors slice is nil", + args: args{ + flavors: nil, + }, + wantErr: false, + }, + { + name: "flavors slice is empty", + args: args{ + flavors: []mongodbflex.InstanceFlavor{}, + }, + wantErr: false, + }, + { + name: "empty flavor in flavors slice", + args: args{ + flavors: []mongodbflex.InstanceFlavor{{}}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.flavors); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/instance/create/create.go b/internal/cmd/mongodbflex/instance/create/create.go index 8d0237887..002ff1074 100644 --- a/internal/cmd/mongodbflex/instance/create/create.go +++ b/internal/cmd/mongodbflex/instance/create/create.go @@ -2,7 +2,6 @@ package create import ( "context" - "errors" "fmt" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -29,16 +28,18 @@ const ( aclFlag = "acl" backupScheduleFlag = "backup-schedule" flavorIdFlag = "flavor-id" - cpuFlag = "cpu" - ramFlag = "ram" storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" + defaultType = "Replica" + + cpuFlag = "cpu" // Deprecated: Will be removed after 2027-03-07. + ramFlag = "ram" // Deprecated: Will be removed after 2027-03-07. + + defaultBackupSchedule = "0 0/6 * * *" // Deprecated: Will be removed after 2027-03-07. + defaultStorageClass = "premium-perf2-mongodb" // Deprecated: Will be removed after 2027-03-07. + defaultStorageSize = 10 // Deprecated: Will be removed after 2027-03-07. - defaultBackupSchedule = "0 0/6 * * *" - defaultStorageClass = "premium-perf2-mongodb" - defaultStorageSize = 10 - defaultType = "Replica" ) var typeFlag = flags.StringEnumFlag( @@ -53,14 +54,14 @@ type inputModel struct { InstanceName string ACL []string - BackupSchedule string - FlavorId string - CPU *int32 - RAM *int32 + BackupSchedule *string + FlavorId *string StorageClass *string StorageSize *int64 - Version string + Version *string Type *string + CPU *int32 // Deprecated: Will be removed after 2027-03-07. + RAM *int32 // Deprecated: Will be removed after 2027-03-07. } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -71,14 +72,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a MongoDB Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by CPU and RAM. Other parameters are set to default values`, - `$ stackit mongodbflex instance create --name my-instance --cpu 1 --ram 4 --acl 0.0.0.0/0`), - examples.NewExample( - `Create a MongoDB Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by ID. Other parameters are set to default values`, - `$ stackit mongodbflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0`), + `Create a MongoDB Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access).`, + `$ stackit mongodbflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 --type Replica --storage-size 20 --version 8.0 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-mongodb`), examples.NewExample( - `Create a MongoDB Flex instance with name "my-instance", allow access to a specific range of IP addresses, specify flavor by CPU and RAM and set storage size to 20 GB. Other parameters are set to default values`, - `$ stackit mongodbflex instance create --name my-instance --cpu 1 --ram 4 --acl 1.2.3.0/24 --storage-size 20`), + `Create a MongoDB Flex instance with name "my-instance", allow access to a specific range of IP addresses.`, + `$ stackit mongodbflex instance create --name my-instance --flavor-id xxx --acl 1.2.3.0/24 --type Replica --storage-size 20 --version 8.0 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-mongodb`), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -100,19 +98,51 @@ func NewCmd(params *types.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - prompt := fmt.Sprintf("Are you sure you want to create a MongoDB Flex instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) + // load flavor id - remove after 2027-03-07 + if model.FlavorId == nil { + // transform the model.FlavorId field from "*string" to "string" once this is removed + params.Printer.Warn("The --%s flag is not set, determining flavor ID by CPU und RAM. This behavior is deprecated, the --%s flag will be required after 2027-03-07.\n", flavorIdFlag, flavorIdFlag) + } + model.FlavorId, err = getFlavorId(ctx, model, apiClient.DefaultAPI) if err != nil { - return err + params.Printer.Debug(print.ErrorLevel, "determining flavor id: %v", err) + } + + // remove after 2027-03-07 + if model.BackupSchedule == nil { + // transform the model.BackupSchedule field from "*string" to "string" once this is removed + params.Printer.Warn("The --%s flag is not set. Using the default value \"%s\". This behavior is deprecated, the --%s flag will be required after 2027-03-07.\n", backupScheduleFlag, defaultBackupSchedule, backupScheduleFlag) + model.BackupSchedule = utils.Ptr(defaultBackupSchedule) } - // Fill in version, if needed - if model.Version == "" { - version, err := mongodbflexUtils.GetLatestMongoDBVersion(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + // Fill in version, if needed - remove after 2027-03-07 + if model.Version == nil { + params.Printer.Warn("The --%s flag is not set. Using the latest version as a default. This behavior is deprecated, the --%s flag will be required after 2027-03-07.\n", versionFlag, versionFlag) + // transform the model.Version field from "*string" to "string" once this is removed + + version, err := mongodbflexUtils.GetLatestMongoDBVersion(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) //nolint:staticcheck // SA1019 - deprecated but still supported until 2027-03-07 if err != nil { return fmt.Errorf("get latest MongoDB version: %w", err) } - model.Version = version + model.Version = utils.Ptr(version) + } + + // remove after 2027-03-07 + if model.StorageSize == nil { + params.Printer.Warn("The --%s flag is not set. Using the default value (%d). This behavior is deprecated, the --%s flag will be required after 2027-03-07.\n", storageSizeFlag, defaultStorageSize, storageSizeFlag) + model.StorageSize = utils.Ptr(int64(defaultStorageSize)) + } + + // remove after 2027-03-07 + if model.StorageClass == nil { + params.Printer.Warn("The --%s flag is not set. Using the default value (%s). This behavior is deprecated, the --%s flag will be required after 2027-03-07.\n", storageClassFlag, defaultStorageClass, storageClassFlag) + model.StorageClass = utils.Ptr(defaultStorageClass) + } + + prompt := fmt.Sprintf("Are you sure you want to create a MongoDB Flex instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -147,17 +177,29 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc.") - cmd.Flags().String(backupScheduleFlag, defaultBackupSchedule, "Backup schedule") - cmd.Flags().String(flavorIdFlag, "", "ID of the flavor") + cmd.Flags().String(backupScheduleFlag, defaultBackupSchedule, "Backup schedule. This flag will be required after 2027-03-07.") + cmd.Flags().String(flavorIdFlag, "", "ID of the flavor. This flag will be required after 2027-03-07.") + cmd.Flags().String(storageClassFlag, defaultStorageClass, "Storage class. This flag will be required after 2027-03-07.") + cmd.Flags().Int64(storageSizeFlag, defaultStorageSize, "Storage size (in GB). This flag will be required after 2027-03-07.") + cmd.Flags().String(versionFlag, "", "MongoDB version. Defaults to the latest version available. This flag will be required after 2027-03-07.") + typeFlag.Register(cmd.Flags()) + + // remove after 2027-03-07 cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") - cmd.Flags().String(storageClassFlag, defaultStorageClass, "Storage class") - cmd.Flags().Int64(storageSizeFlag, defaultStorageSize, "Storage size (in GB)") - cmd.Flags().String(versionFlag, "", "MongoDB version. Defaults to the latest version available") - typeFlag.Register(cmd.Flags()) + // after 2027-03-07: add backupScheduleFlag, storageClassFlag, storageSizeFlag, versionFlag, flavorIdFlag, replicasFlag err := flags.MarkFlagsRequired(cmd, instanceNameFlag, aclFlag) cobra.CheckErr(err) + + // remove after 2027-03-07 + err = cmd.Flags().MarkDeprecated(cpuFlag, fmt.Sprintf("Will be removed after 2027-03-07. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + err = cmd.Flags().MarkDeprecated(ramFlag, fmt.Sprintf("Will be removed after 2027-03-07. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + cmd.MarkFlagsRequiredTogether(cpuFlag, ramFlag) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, cpuFlag) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, ramFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -166,83 +208,86 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return nil, &cliErr.ProjectIdError{} } - storageSize := flags.FlagWithDefaultToInt64Value(p, cmd, storageSizeFlag) - - flavorId := flags.FlagToStringValue(p, cmd, flavorIdFlag) + flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) cpu := flags.FlagToInt32Pointer(p, cmd, cpuFlag) ram := flags.FlagToInt32Pointer(p, cmd, ramFlag) - if flavorId == "" && (cpu == nil || ram == nil) { + // remove after 2027-03-07: flavor id flag will be required then + if flavorId == nil && (cpu == nil || ram == nil) { return nil, &cliErr.DatabaseInputFlavorError{ Cmd: cmd, } } - if flavorId != "" && (cpu != nil || ram != nil) { + // remove after 2027-03-07: flavor id flag will be required then + if flavorId != nil && (cpu != nil || ram != nil) { return nil, &cliErr.DatabaseInputFlavorError{ Cmd: cmd, } } + // remove after 2027-03-07: storage size flag will be required then (no pointer anymore) + var storageSize *int64 + if cmd.Flags().Changed(storageSizeFlag) { + storageSize = flags.FlagToInt64Pointer(p, cmd, storageSizeFlag) + } + model := inputModel{ GlobalFlagModel: globalFlags, InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), ACL: flags.FlagToStringSliceValue(p, cmd, aclFlag), - BackupSchedule: flags.FlagWithDefaultToStringValue(p, cmd, backupScheduleFlag), + BackupSchedule: flags.FlagToStringPointer(p, cmd, backupScheduleFlag), FlavorId: flavorId, - CPU: cpu, - RAM: ram, - StorageClass: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, storageClassFlag)), - StorageSize: &storageSize, - Version: flags.FlagToStringValue(p, cmd, versionFlag), + StorageClass: flags.FlagToStringPointer(p, cmd, storageClassFlag), + StorageSize: storageSize, + Version: flags.FlagToStringPointer(p, cmd, versionFlag), Type: typeFlag.Ptr(), + + // remove after 2027-03-07: deprecated fields + CPU: cpu, + RAM: ram, } p.DebugInputModel(model) return &model, nil } -type MongoDBFlexClient interface { - CreateInstance(ctx context.Context, projectId, region string) mongodbflex.ApiCreateInstanceRequest - ListFlavors(ctx context.Context, projectId, region string) mongodbflex.ApiListFlavorsRequest - ListStorages(ctx context.Context, projectId, flavorId, region string) mongodbflex.ApiListStoragesRequest -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexClient) (mongodbflex.ApiCreateInstanceRequest, error) { - req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) +// Deprecated: remove after 2027-03-07 +func getFlavorId(ctx context.Context, model *inputModel, apiClient mongodbflex.DefaultAPI) (*string, error) { + if model == nil { + return nil, fmt.Errorf("model is nil") + } - var flavorId string - var err error + if model.FlavorId != nil { + return model.FlavorId, nil + } + // Load all flavors flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { - return req, fmt.Errorf("get MongoDB Flex flavors: %w", err) + return nil, fmt.Errorf("loading flavors: %w", err) } - if model.FlavorId == "" { - foundFlavorId, err := mongodbflexUtils.LoadFlavorId(*model.CPU, *model.RAM, &flavors.Flavors) - if err != nil { - var dsaInvalidPlanError *cliErr.DSAInvalidPlanError - if !errors.As(err, &dsaInvalidPlanError) { - return req, fmt.Errorf("load flavor ID: %w", err) - } - return req, err - } - flavorId = *foundFlavorId - } else { - err := mongodbflexUtils.ValidateFlavorId(model.FlavorId, flavors.Flavors) - if err != nil { - return req, err + for _, flavor := range flavors.Flavors { + if *flavor.Cpu == *model.CPU && *flavor.Memory == *model.RAM { + return flavor.Id, nil } - flavorId = model.FlavorId } - storages, err := apiClient.ListStorages(ctx, model.ProjectId, flavorId, model.Region).Execute() - if err != nil { - return req, fmt.Errorf("get MongoDB Flex storages: %w", err) - } - err = mongodbflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, flavorId) - if err != nil { - return req, err + return nil, fmt.Errorf("no matching flavor found") +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient mongodbflex.DefaultAPI) (mongodbflex.ApiCreateInstanceRequest, error) { + req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) + + // remove after 2027-03-07 + if model.BackupSchedule == nil { + return mongodbflex.ApiCreateInstanceRequest{}, fmt.Errorf("backup schedule is nil") + } else if model.StorageSize == nil { + return mongodbflex.ApiCreateInstanceRequest{}, fmt.Errorf("storage size is nil") + } else if model.Version == nil { + return mongodbflex.ApiCreateInstanceRequest{}, fmt.Errorf("version is nil") + } else if model.StorageClass == nil { + return mongodbflex.ApiCreateInstanceRequest{}, fmt.Errorf("storage class is nil") } replicas, err := mongodbflexUtils.GetInstanceReplicas(*model.Type) @@ -253,18 +298,19 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC req = req.CreateInstancePayload(mongodbflex.CreateInstancePayload{ Name: model.InstanceName, Acl: mongodbflex.ACL{Items: model.ACL}, - BackupSchedule: model.BackupSchedule, - FlavorId: flavorId, + BackupSchedule: *model.BackupSchedule, + FlavorId: utils.PtrString(model.FlavorId), Replicas: replicas, Storage: mongodbflex.Storage{ Class: model.StorageClass, Size: model.StorageSize, }, - Version: model.Version, + Version: *model.Version, Options: map[string]string{ "type": *model.Type, }, }) + return req, nil } diff --git a/internal/cmd/mongodbflex/instance/create/create_test.go b/internal/cmd/mongodbflex/instance/create/create_test.go index 4f1b88082..5a873fadc 100644 --- a/internal/cmd/mongodbflex/instance/create/create_test.go +++ b/internal/cmd/mongodbflex/instance/create/create_test.go @@ -2,7 +2,6 @@ package create import ( "context" - "fmt" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -22,34 +21,12 @@ const ( type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} - -type mockSettings struct { - listFlavorsFails bool - listFlavorsResp *mongodbflex.ListFlavorsResponse - listStoragesResp *mongodbflex.ListStoragesResponse -} - -var testProjectId = uuid.NewString() -var testFlavorId = uuid.NewString() - -func newAPICLientMock(settings mockSettings) mongodbflex.DefaultAPI { - return mongodbflex.DefaultAPIServiceMock{ - ListStoragesExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListStoragesRequest) (*mongodbflex.ListStoragesResponse, error) { - if settings.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return settings.listStoragesResp, nil - }), - ListFlavorsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListFlavorsRequest) (*mongodbflex.ListFlavorsResponse, error) { - if settings.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") - } - return settings.listFlavorsResp, nil - }), - } -} +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} + testFlavorId = uuid.NewString() + testProjectId = uuid.NewString() +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -79,11 +56,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { }, InstanceName: "example-name", ACL: []string{"0.0.0.0/0"}, - BackupSchedule: "0 0/6 * * *", - FlavorId: testFlavorId, + BackupSchedule: utils.Ptr("0 0/6 * * *"), + FlavorId: utils.Ptr(testFlavorId), StorageClass: utils.Ptr("premium-perf4-mongodb"), StorageSize: utils.Ptr(int64(10)), - Version: "6.0", + Version: utils.Ptr("6.0"), Type: utils.Ptr("Replica"), } for _, mod := range mods { @@ -144,8 +121,10 @@ func TestParseInput(t *testing.T) { delete(flagValues, backupScheduleFlag) delete(flagValues, typeFlag.Name()) }), - isValid: true, - expectedModel: fixtureInputModel(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.BackupSchedule = nil + }), }, { description: "use CPU and RAM", @@ -156,7 +135,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.FlavorId = "" + model.FlavorId = nil model.CPU = utils.Ptr(int32(2)) model.RAM = utils.Ptr(int32(4)) }), @@ -216,7 +195,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Version = "" + model.Version = nil }), }, { @@ -260,239 +239,50 @@ func TestParseInput(t *testing.T) { func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest mongodbflex.ApiCreateInstanceRequest - mockClientSettings mockSettings - isValid bool + description string + model *inputModel + expectedRequest mongodbflex.ApiCreateInstanceRequest + isValid bool }{ { description: "base with flavor ID", model: fixtureInputModel(), isValid: true, expectedRequest: fixtureRequest(), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - }, - { - description: "with CPU and RAM", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = "" - model.CPU = utils.Ptr(int32(2)) - model.RAM = utils.Ptr(int32(4)) - }, - ), - isValid: true, - expectedRequest: fixtureRequest(), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int32(1)), - Memory: utils.Ptr(int32(8)), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - }, - { - description: "single instance type", - model: fixtureInputModel(func(model *inputModel) { model.Type = utils.Ptr("Single") }), - isValid: true, - expectedRequest: fixtureRequest().CreateInstancePayload(fixturePayload(func(payload *mongodbflex.CreateInstancePayload) { - payload.Options = map[string]string{"type": "Single"} - payload.Replicas = int32(1) - })), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, }, { - description: "sharded instance type", - model: fixtureInputModel(func(model *inputModel) { model.Type = utils.Ptr("Sharded") }), - isValid: true, - expectedRequest: fixtureRequest().CreateInstancePayload(fixturePayload(func(payload *mongodbflex.CreateInstancePayload) { - payload.Options = map[string]string{"type": "Sharded"} - payload.Replicas = int32(9) - })), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - }, - { - description: "get flavors fails", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = "" - model.CPU = utils.Ptr(int32(2)) - model.RAM = utils.Ptr(int32(4)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsFails: true, - }, - isValid: false, - }, - { - description: "flavor id not found", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = "" - model.CPU = utils.Ptr(int32(5)) - model.RAM = utils.Ptr(int32(9)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int32(1)), - Memory: utils.Ptr(int32(8)), - }, - }, - }, - }, + description: "storage class missing", + model: fixtureInputModel(func(model *inputModel) { + model.StorageClass = nil + }), isValid: false, }, { - description: "get storages fails", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = "" - model.CPU = utils.Ptr(int32(2)) - model.RAM = utils.Ptr(int32(4)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsFails: true, - }, + description: "storage size missing", + model: fixtureInputModel(func(model *inputModel) { + model.StorageSize = nil + }), isValid: false, }, { - description: "invalid storage class", - model: fixtureInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("non-existing-class") - }, - ), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, + description: "backup schedule missing", + model: fixtureInputModel(func(model *inputModel) { + model.BackupSchedule = nil + }), isValid: false, }, { - description: "invalid storage size", - model: fixtureInputModel( - func(model *inputModel) { - model.StorageSize = utils.Ptr(int64(9)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, + description: "version missing", + model: fixtureInputModel(func(model *inputModel) { + model.Version = nil + }), isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request, err := buildRequest(testCtx, tt.model, newAPICLientMock(tt.mockClientSettings)) + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) if err != nil { if !tt.isValid { return @@ -502,8 +292,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), - cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mongodbflex/instance/describe/describe.go b/internal/cmd/mongodbflex/instance/describe/describe.go index 1e6dd2652..13144382f 100644 --- a/internal/cmd/mongodbflex/instance/describe/describe.go +++ b/internal/cmd/mongodbflex/instance/describe/describe.go @@ -13,7 +13,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" - mongodbflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -96,15 +95,6 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I if instance == nil { return fmt.Errorf("instance is nil") } - var instanceType string - if instance.HasReplicas() { - var err error - instanceType, err = mongodbflexUtils.GetInstanceType(*instance.Replicas) - if err != nil { - // Should never happen - instanceType = "" - } - } table := tables.NewTable() table.AddRow("ID", utils.PtrString(instance.Id)) @@ -113,6 +103,10 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I table.AddSeparator() table.AddRow("STATUS", utils.PtrString(instance.Status)) table.AddSeparator() + if instance.HasFlavor() { + table.AddRow("FLAVOR ID", utils.PtrString(instance.Flavor.Id)) + table.AddSeparator() + } if instance.HasStorage() { table.AddRow("STORAGE SIZE (GB)", utils.PtrString(instance.Storage.Size)) table.AddSeparator() @@ -124,22 +118,6 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I table.AddRow("ACL", acls) table.AddSeparator() } - if instance.HasFlavor() && instance.Flavor.HasDescription() { - table.AddRow("FLAVOR DESCRIPTION", *instance.Flavor.Description) - table.AddSeparator() - } - table.AddRow("TYPE", instanceType) - table.AddSeparator() - if instance.HasReplicas() { - table.AddRow("REPLICAS", *instance.Replicas) - table.AddSeparator() - } - if instance.HasFlavor() { - table.AddRow("CPU", utils.PtrString(instance.Flavor.Cpu)) - table.AddSeparator() - table.AddRow("RAM (GB)", utils.PtrString(instance.Flavor.Memory)) - table.AddSeparator() - } table.AddRow("BACKUP SCHEDULE (UTC)", utils.PtrString(instance.BackupSchedule)) table.AddSeparator() err := table.Display(p) diff --git a/internal/cmd/mongodbflex/instance/update/update.go b/internal/cmd/mongodbflex/instance/update/update.go index 22ba42eda..8f852eb6e 100644 --- a/internal/cmd/mongodbflex/instance/update/update.go +++ b/internal/cmd/mongodbflex/instance/update/update.go @@ -30,11 +30,12 @@ const ( aclFlag = "acl" backupScheduleFlag = "backup-schedule" flavorIdFlag = "flavor-id" - cpuFlag = "cpu" - ramFlag = "ram" storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" + + cpuFlag = "cpu" // Deprecated: Will be removed after 2027-03-07. Flavor id should be used instead. + ramFlag = "ram" // Deprecated: Will be removed after 2027-03-07. Flavor id should be used instead. ) var typeFlag = flags.StringEnumFlag( @@ -51,12 +52,12 @@ type inputModel struct { ACL *[]string BackupSchedule *string FlavorId *string - CPU *int32 - RAM *int32 StorageClass *string StorageSize *int64 Version *string Type *string + CPU *int32 // Deprecated: Will be removed after 2027-03-07. + RAM *int32 // Deprecated: Will be removed after 2027-03-07. } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -70,7 +71,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { "$ stackit mongodbflex instance update xxx --name my-new-name"), examples.NewExample( `Update the version of a MongoDB Flex instance`, - "$ stackit mongodbflex instance update xxx --version 6.0"), + "$ stackit mongodbflex instance update xxx --version 8.0"), ), Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), RunE: func(cmd *cobra.Command, args []string) error { @@ -133,12 +134,21 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "Lists of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().String(backupScheduleFlag, "", "Backup schedule") cmd.Flags().String(flavorIdFlag, "", "ID of the flavor") - cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") cmd.Flags().String(storageClassFlag, "", "Storage class") cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB)") cmd.Flags().String(versionFlag, "", "Version") + cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") // Deprecated: Will be removed after 2027-03-07. + cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") // Deprecated: Will be removed after 2027-03-07. typeFlag.Register(cmd.Flags()) + + // Deprecated: Will be removed after 2027-03-07. + err := cmd.Flags().MarkDeprecated(cpuFlag, fmt.Sprintf("Will be removed after 2027-03-07. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + err = cmd.Flags().MarkDeprecated(ramFlag, fmt.Sprintf("Will be removed after 2027-03-07. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + cmd.MarkFlagsRequiredTogether(cpuFlag, ramFlag) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, cpuFlag) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, ramFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -179,52 +189,63 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ACL: acl, BackupSchedule: backupSchedule, FlavorId: flavorId, - CPU: cpu, - RAM: ram, StorageClass: storageClass, StorageSize: storageSize, Version: version, Type: instanceType, + + // deprecated fields + CPU: cpu, + RAM: ram, } p.DebugInputModel(model) return &model, nil } -type MongoDBFlexClient interface { - PartialUpdateInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiPartialUpdateInstanceRequest - GetInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiGetInstanceRequest - ListFlavors(ctx context.Context, projectId, region string) mongodbflex.ApiListFlavorsRequest - ListStorages(ctx context.Context, projectId, flavorId, region string) mongodbflex.ApiListStoragesRequest -} +func buildRequest(ctx context.Context, model *inputModel, apiClient mongodbflex.DefaultAPI) (mongodbflex.ApiPartialUpdateInstanceRequest, error) { + var flavorId *string + var err error -func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexClient) (mongodbflex.ApiPartialUpdateInstanceRequest, error) { req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId, model.Region) - var flavorId *string - var err error + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() + if err != nil { + return req, fmt.Errorf("get MongoDB Flex instance: %w", err) + } flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex flavors: %w", err) } + // if cpu/ram flags are used instead of the flavor id flag if model.FlavorId == nil && (model.RAM != nil || model.CPU != nil) { ram := model.RAM cpu := model.CPU + + // if only one of the cpu/ram flags is set if model.RAM == nil || model.CPU == nil { - currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() - if err != nil { - return req, fmt.Errorf("get MongoDB Flex instance: %w", err) + var currentFlavor *mongodbflex.InstanceFlavor + for _, f := range flavors.Flavors { + if f.Id == currentInstance.Item.Flavor.Id { + currentFlavor = &f + } + } + + if currentFlavor == nil { + return req, fmt.Errorf("flavor %s not found", currentInstance.Item.Flavor.GetId()) } + if model.RAM == nil { - ram = currentInstance.Item.Flavor.Memory + ram = currentFlavor.Memory } if model.CPU == nil { - cpu = currentInstance.Item.Flavor.Cpu + cpu = currentFlavor.Cpu } } - flavorId, err = mongodbflexUtils.LoadFlavorId(*cpu, *ram, &flavors.Flavors) + + flavorId, err = mongodbflexUtils.LoadFlavorId(*cpu, *ram, &flavors.Flavors) //nolint:staticcheck // SA1019 - deprecated but still supported until 2027-03-07 if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -233,33 +254,9 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC return req, err } } else if model.FlavorId != nil { - err := mongodbflexUtils.ValidateFlavorId(*model.FlavorId, flavors.Flavors) - if err != nil { - return req, err - } flavorId = model.FlavorId } - var storages *mongodbflex.ListStoragesResponse - if model.StorageClass != nil || model.StorageSize != nil { - validationFlavorId := flavorId - if validationFlavorId == nil { - currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() - if err != nil { - return req, fmt.Errorf("get MongoDB Flex instance: %w", err) - } - validationFlavorId = currentInstance.Item.Flavor.Id - } - storages, err = apiClient.ListStorages(ctx, model.ProjectId, *validationFlavorId, model.Region).Execute() - if err != nil { - return req, fmt.Errorf("get MongoDB Flex storages: %w", err) - } - err = mongodbflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *validationFlavorId) - if err != nil { - return req, err - } - } - var payloadAcl *mongodbflex.ACL if model.ACL != nil { payloadAcl = &mongodbflex.ACL{Items: *model.ACL} diff --git a/internal/cmd/mongodbflex/instance/update/update_test.go b/internal/cmd/mongodbflex/instance/update/update_test.go index 5b34c94f0..539c16809 100644 --- a/internal/cmd/mongodbflex/instance/update/update_test.go +++ b/internal/cmd/mongodbflex/instance/update/update_test.go @@ -21,16 +21,20 @@ const ( type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testFlavorId = uuid.NewString() +) type mockClientSettings struct { - listFlavorsFails bool - listFlavorsResp *mongodbflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *mongodbflex.ListStoragesResponse - getInstanceFails bool - getInstanceResp *mongodbflex.InstanceResponse + listFlavorsFails bool + listFlavorsResp *mongodbflex.ListFlavorsResponse + getInstanceFails bool + getInstanceResp *mongodbflex.InstanceResponse } func newAPIClientMock(c mockClientSettings) mongodbflex.DefaultAPI { @@ -41,12 +45,6 @@ func newAPIClientMock(c mockClientSettings) mongodbflex.DefaultAPI { } return c.getInstanceResp, nil }), - ListStoragesExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListStoragesRequest) (*mongodbflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil - }), ListFlavorsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListFlavorsRequest) (*mongodbflex.ListFlavorsResponse, error) { if c.listFlavorsFails { return nil, fmt.Errorf("list flavors failed") @@ -56,10 +54,6 @@ func newAPIClientMock(c mockClientSettings) mongodbflex.DefaultAPI { } } -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() -var testFlavorId = uuid.NewString() - func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testInstanceId, @@ -348,16 +342,11 @@ func TestParseInput(t *testing.T) { func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest mongodbflex.ApiPartialUpdateInstanceRequest - getInstanceFails bool - getInstanceResp *mongodbflex.InstanceResponse - listFlavorsFails bool - listFlavorsResp *mongodbflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *mongodbflex.ListStoragesResponse - isValid bool + description string + model *inputModel + expectedRequest mongodbflex.ApiPartialUpdateInstanceRequest + mockClientSettings mockClientSettings + isValid bool }{ { description: "no values", @@ -371,12 +360,14 @@ func TestBuildRequest(t *testing.T) { model.FlavorId = utils.Ptr(testFlavorId) }), isValid: true, - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), + mockClientSettings: mockClientSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, }, @@ -392,12 +383,14 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int32(4)) }), isValid: true, - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), + mockClientSettings: mockClientSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, }, @@ -412,20 +405,15 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("class") }), isValid: true, - getInstanceResp: &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Flavor: &mongodbflex.Flavor{ - Id: utils.Ptr(testFlavorId), + mockClientSettings: mockClientSettings{ + getInstanceResp: &mongodbflex.InstanceResponse{ + Item: &mongodbflex.Instance{ + Flavor: &mongodbflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, }, }, }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{ Storage: &mongodbflex.Storage{ @@ -440,20 +428,15 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(10)) }), isValid: true, - getInstanceResp: &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Flavor: &mongodbflex.Flavor{ - Id: utils.Ptr(testFlavorId), + mockClientSettings: mockClientSettings{ + getInstanceResp: &mongodbflex.InstanceResponse{ + Item: &mongodbflex.Instance{ + Flavor: &mongodbflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, }, }, }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{ Storage: &mongodbflex.Storage{ @@ -470,8 +453,10 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int32(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockClientSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "flavor id not found", @@ -481,17 +466,19 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int32(9)) }, ), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: []mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int32(2)), - Memory: utils.Ptr(int32(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int32(1)), - Memory: utils.Ptr(int32(8)), + mockClientSettings: mockClientSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, + { + Id: utils.Ptr("other-flavor"), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), + }, }, }, }, @@ -499,13 +486,11 @@ func TestBuildRequest(t *testing.T) { }, { description: "get instance fails", - model: fixtureRequiredInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("class") - }, - ), - getInstanceFails: true, - isValid: false, + model: fixtureRequiredInputModel(), + mockClientSettings: mockClientSettings{ + getInstanceFails: true, + }, + isValid: false, }, { description: "get storages fails", @@ -516,52 +501,8 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int32(4)) }, ), - listFlavorsFails: true, - isValid: false, - }, - { - description: "invalid storage class", - model: fixtureRequiredInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("non-existing-class") - }, - ), - getInstanceResp: &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Flavor: &mongodbflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - isValid: false, - }, - { - description: "invalid storage size", - model: fixtureRequiredInputModel( - func(model *inputModel) { - model.StorageSize = utils.Ptr(int64(9)) - }, - ), - getInstanceResp: &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Flavor: &mongodbflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, + mockClientSettings: mockClientSettings{ + listFlavorsFails: true, }, isValid: false, }, @@ -569,15 +510,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - settings := mockClientSettings{ - getInstanceFails: tt.getInstanceFails, - getInstanceResp: tt.getInstanceResp, - listFlavorsFails: tt.listFlavorsFails, - listFlavorsResp: tt.listFlavorsResp, - listStoragesFails: tt.listStoragesFails, - listStoragesResp: tt.listStoragesResp, - } - request, err := buildRequest(testCtx, tt.model, newAPIClientMock(settings)) + request, err := buildRequest(testCtx, tt.model, newAPIClientMock(tt.mockClientSettings)) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/mongodbflex/mongodbflex.go b/internal/cmd/mongodbflex/mongodbflex.go index 3376477a3..54ef69b43 100644 --- a/internal/cmd/mongodbflex/mongodbflex.go +++ b/internal/cmd/mongodbflex/mongodbflex.go @@ -2,9 +2,12 @@ package mongodbflex import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/flavor" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/options" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/storage" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/user" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/version" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -27,6 +30,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(user.NewCmd(params)) - cmd.AddCommand(options.NewCmd(params)) + cmd.AddCommand(options.NewCmd(params)) //nolint:staticcheck // Command is deprecated but must be kept for backward compatibility cmd.AddCommand(backup.NewCmd(params)) + cmd.AddCommand(version.NewCmd(params)) + cmd.AddCommand(flavor.NewCmd(params)) + cmd.AddCommand(storage.NewCmd(params)) } diff --git a/internal/cmd/mongodbflex/options/options.go b/internal/cmd/mongodbflex/options/options.go index 1941ffbc1..edbbaaf7d 100644 --- a/internal/cmd/mongodbflex/options/options.go +++ b/internal/cmd/mongodbflex/options/options.go @@ -20,12 +20,17 @@ import ( ) const ( - flavorsFlag = "flavors" + // Deprecated: Will be removed after 2027-03-07. + flavorsFlag = "flavors" + // Deprecated: Will be removed after 2027-03-07. versionsFlag = "versions" + // Deprecated: Will be removed after 2027-03-07. storagesFlag = "storages" + // Deprecated: Will be removed after 2027-03-07. flavorIdFlag = "flavor-id" ) +// Deprecated: Will be removed after 2027-03-07. type inputModel struct { *globalflags.GlobalFlagModel @@ -35,17 +40,20 @@ type inputModel struct { FlavorId *string } +// Deprecated: Will be removed after 2027-03-07. type options struct { Flavors []mongodbflex.InstanceFlavor `json:"flavors,omitempty"` Versions []string `json:"versions,omitempty"` Storages *flavorStorages `json:"flavorStorages,omitempty"` } +// Deprecated: Will be removed after 2027-03-07. type flavorStorages struct { FlavorId string `json:"flavorId"` Storages *mongodbflex.ListStoragesResponse `json:"storages"` } +// Deprecated: Will be removed after 2027-03-07. func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", @@ -63,6 +71,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { `List MongoDB Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit mongodbflex options --flavors"`, "$ stackit mongodbflex options --storages --flavor-id "), ), + Deprecated: `Command "stackit mongodbflex options" command is deprecated and will be removed after 2027-03-07. Please use "stackit mongodbflex version list", "stackit mongodbflex flavor list" and "stackit mongodbflex storage list" commands instead.`, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() model, err := parseInput(params.Printer, cmd, args) @@ -89,6 +98,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return cmd } +// Deprecated: Will be removed after 2027-03-07. func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(flavorsFlag, false, "Lists supported flavors") cmd.Flags().Bool(versionsFlag, false, "Lists supported versions") @@ -96,6 +106,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(flavorIdFlag, "", `The flavor ID to show storages for. Only relevant when "--storages" is passed`) } +// Deprecated: Will be removed after 2027-03-07. func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) flavors := flags.FlagToBoolValue(p, cmd, flavorsFlag) @@ -128,12 +139,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } +// Deprecated: Will be removed after 2027-03-07. type mongoDBFlexOptionsClient interface { ListFlavors(ctx context.Context, projectId, region string) mongodbflex.ApiListFlavorsRequest ListVersions(ctx context.Context, projectId, region string) mongodbflex.ApiListVersionsRequest ListStorages(ctx context.Context, projectId, flavorId, region string) mongodbflex.ApiListStoragesRequest } +// Deprecated: Will be removed after 2027-03-07. func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputModel, apiClient mongoDBFlexOptionsClient) error { var flavors *mongodbflex.ListFlavorsResponse var versions *mongodbflex.ListVersionsResponse @@ -162,6 +175,7 @@ func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputM return outputResult(p, model, flavors, versions, storages) } +// Deprecated: Will be removed after 2027-03-07. func outputResult(p *print.Printer, model *inputModel, flavors *mongodbflex.ListFlavorsResponse, versions *mongodbflex.ListVersionsResponse, storages *mongodbflex.ListStoragesResponse) error { if model == nil || model.GlobalFlagModel == nil { return fmt.Errorf("model is nil") @@ -186,6 +200,7 @@ func outputResult(p *print.Printer, model *inputModel, flavors *mongodbflex.List }) } +// Deprecated: Will be removed after 2027-03-07. func outputResultAsTable(p *print.Printer, model *inputModel, options *options) error { if model == nil { return fmt.Errorf("model is nil") @@ -212,6 +227,7 @@ func outputResultAsTable(p *print.Printer, model *inputModel, options *options) return nil } +// Deprecated: Will be removed after 2027-03-07. func buildFlavorsTable(flavors []mongodbflex.InstanceFlavor) tables.Table { table := tables.NewTable() table.SetTitle("Flavors") @@ -229,6 +245,7 @@ func buildFlavorsTable(flavors []mongodbflex.InstanceFlavor) tables.Table { return table } +// Deprecated: Will be removed after 2027-03-07. func buildVersionsTable(versions []string) tables.Table { table := tables.NewTable() table.SetTitle("Versions") @@ -240,6 +257,7 @@ func buildVersionsTable(versions []string) tables.Table { return table } +// Deprecated: Will be removed after 2027-03-07. func buildStoragesTable(storagesResp mongodbflex.ListStoragesResponse) tables.Table { storages := storagesResp.StorageClasses table := tables.NewTable() diff --git a/internal/cmd/mongodbflex/storage/list/list.go b/internal/cmd/mongodbflex/storage/list/list.go new file mode 100644 index 000000000..972068628 --- /dev/null +++ b/internal/cmd/mongodbflex/storage/list/list.go @@ -0,0 +1,140 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + 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/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + flavorIdFlag = "flavor-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + FlavorId *string + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists MongoDB Flex storages for a certain flavor", + Long: "Lists MongoDB Flex storages for a certain flavor.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List MongoDB Flex storages for flavor with ID "xxx"`, + "$ stackit mongodbflex storage list --flavor-id xxx"), + examples.NewExample( + `List MongoDB Flex storages for flavor with ID "xxx" in JSON format`, + "$ stackit mongodbflex storage list --flavor-id xxx --output-format json"), + examples.NewExample( + `List up to 10 storages for flavor with ID "xxx"`, + "$ stackit mongodbflex storage list --flavor-id xxx --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + storages, err := buildRequest(ctx, model, apiClient.DefaultAPI).Execute() + if err != nil { + return fmt.Errorf("get MongoDB Flex storages: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, storages) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(flavorIdFlag, "", "Flavor ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, flavorIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + FlavorId: flags.FlagToStringPointer(p, cmd, flavorIdFlag), + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient mongodbflex.DefaultAPI) mongodbflex.ApiListStoragesRequest { + return apiClient.ListStorages(ctx, model.ProjectId, *model.FlavorId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, storagesResp *mongodbflex.ListStoragesResponse) error { + return p.OutputResult(outputFormat, storagesResp, func() error { + if storagesResp == nil { + return fmt.Errorf("storages resp is empty") + } + storages := storagesResp.StorageClasses + if len(storages) == 0 { + p.Outputf("No MongoDB Flex storages found.") + return nil + } + + table := tables.NewTable() + table.SetTitle("Storages") + table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") + for _, storageClass := range storages { + table.AddRow( + utils.PtrString(storagesResp.StorageRange.Min), + utils.PtrString(storagesResp.StorageRange.Max), + storageClass, + ) + table.AddSeparator() + } + + return table.Display(p) + }) +} diff --git a/internal/cmd/mongodbflex/storage/list/list_test.go b/internal/cmd/mongodbflex/storage/list/list_test.go new file mode 100644 index 000000000..39f32fbe7 --- /dev/null +++ b/internal/cmd/mongodbflex/storage/list/list_test.go @@ -0,0 +1,213 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" + testFlavorId = "1.2" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flavorIdFlag: testFlavorId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + FlavorId: utils.Ptr(testFlavorId), + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiListStoragesRequest)) mongodbflex.ApiListStoragesRequest { + request := testClient.DefaultAPI.ListStorages(testCtx, testProjectId, testFlavorId, testRegion) + 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", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "flavor id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, flavorIdFlag) + }), + 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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiListStoragesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + storages *mongodbflex.ListStoragesResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "storages slice is nil", + args: args{ + storages: nil, + }, + wantErr: true, + }, + { + name: "storages slice is empty", + args: args{ + storages: &mongodbflex.ListStoragesResponse{}, + }, + wantErr: false, + }, + { + name: "empty storage class in storages", + args: args{ + storages: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.storages); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/storage/storage.go b/internal/cmd/mongodbflex/storage/storage.go new file mode 100644 index 000000000..a3d68938a --- /dev/null +++ b/internal/cmd/mongodbflex/storage/storage.go @@ -0,0 +1,26 @@ +package storage + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/storage/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "storage", + Short: "Provides functionality for MongoDB Flex storages for a certain flavor", + Long: "Provides functionality for MongoDB Flex storages for a certain flavor.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/mongodbflex/version/list/list.go b/internal/cmd/mongodbflex/version/list/list.go new file mode 100644 index 000000000..2d3cc0493 --- /dev/null +++ b/internal/cmd/mongodbflex/version/list/list.go @@ -0,0 +1,98 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "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/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists MongoDB Flex versions", + Long: "Lists MongoDB Flex versions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List MongoDB Flex version options`, + "$ stackit mongodbflex version list"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + versions, err := buildRequest(ctx, model, apiClient.DefaultAPI).Execute() + if err != nil { + return fmt.Errorf("get MongoDB Flex versions: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, versions.Versions) + }, + } + + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient mongodbflex.DefaultAPI) mongodbflex.ApiListVersionsRequest { + return apiClient.ListVersions(ctx, model.ProjectId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, versions []string) error { + return p.OutputResult(outputFormat, versions, func() error { + if len(versions) == 0 { + p.Outputf("No MongoDB Flex versions found.") + return nil + } + + table := tables.NewTable() + table.SetTitle("Versions") + table.SetHeader("VERSION") + + for _, v := range versions { + table.AddRow(v) + table.AddSeparator() + } + + return table.Display(p) + }) +} diff --git a/internal/cmd/mongodbflex/version/list/list_test.go b/internal/cmd/mongodbflex/version/list/list_test.go new file mode 100644 index 000000000..32adc5c43 --- /dev/null +++ b/internal/cmd/mongodbflex/version/list/list_test.go @@ -0,0 +1,158 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiListVersionsRequest)) mongodbflex.ApiListVersionsRequest { + request := testClient.DefaultAPI.ListVersions(testCtx, testProjectId, testRegion) + 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", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiListVersionsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + versions []string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "versions slice is nil", + args: args{ + versions: nil, + }, + wantErr: false, + }, + { + name: "versions slice is empty", + args: args{ + versions: []string{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.versions); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/version/version.go b/internal/cmd/mongodbflex/version/version.go new file mode 100644 index 000000000..ff39571b5 --- /dev/null +++ b/internal/cmd/mongodbflex/version/version.go @@ -0,0 +1,26 @@ +package version + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/version/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Provides functionality for MongoDB Flex versions", + Long: "Provides functionality for MongoDB Flex versions.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/postgresflex/options/options.go b/internal/cmd/postgresflex/options/options.go index b376780ab..486450c57 100644 --- a/internal/cmd/postgresflex/options/options.go +++ b/internal/cmd/postgresflex/options/options.go @@ -71,7 +71,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { `List PostgreSQL Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit postgresflex options --flavors"`, "$ stackit postgresflex options --storages --flavor-id "), ), - Deprecated: `Command "stackit postgresflex options" is deprecated and will be removed after 2027-01-31. Please use "stackit postgresflex version list", "stackit postgresflex flavors list" and "stackit postgresflex flavor describe" instead.`, + Deprecated: `Command "stackit postgresflex options" is deprecated and will be removed after 2027-01-31. Please use "stackit postgresflex version list", "stackit postgresflex flavor list" and "stackit postgresflex flavor describe" instead.`, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() model, err := parseInput(params.Printer, cmd, args) diff --git a/internal/pkg/services/mongodbflex/utils/utils.go b/internal/pkg/services/mongodbflex/utils/utils.go index 8ee805f89..37a887aa2 100644 --- a/internal/pkg/services/mongodbflex/utils/utils.go +++ b/internal/pkg/services/mongodbflex/utils/utils.go @@ -5,29 +5,20 @@ import ( "context" "fmt" "slices" - "strings" "golang.org/x/mod/semver" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" ) -// The number of replicas is enforced by the API according to the instance type var instanceTypeToReplicas = map[string]int32{ "Single": 1, "Replica": 3, "Sharded": 9, } -type MongoDBFlexClient interface { - ListVersions(ctx context.Context, projectId, region string) mongodbflex.ApiListVersionsRequest - GetInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiGetInstanceRequest - GetUser(ctx context.Context, projectId, instanceId, userId, region string) mongodbflex.ApiGetUserRequest - ListRestoreJobs(ctx context.Context, projectId string, instanceId, region string) mongodbflex.ApiListRestoreJobsRequest -} - func AvailableInstanceTypes() []string { instanceTypes := make([]string, len(instanceTypeToReplicas)) i := 0 @@ -49,59 +40,7 @@ func GetInstanceReplicas(instanceType string) (int32, error) { return numReplicas, nil } -func GetInstanceType(numReplicas int32) (string, error) { - for k, v := range instanceTypeToReplicas { - if v == numReplicas { - return k, nil - } - } - return "", fmt.Errorf("invalid number of replicas: %v", numReplicas) -} - -func ValidateFlavorId(flavorId string, flavors []mongodbflex.InstanceFlavor) error { - if flavors == nil { - return fmt.Errorf("nil flavors") - } - - for _, f := range flavors { - if f.Id != nil && strings.EqualFold(*f.Id, flavorId) { - return nil - } - } - - return &errors.DatabaseInvalidFlavorError{ - Service: "mongodbflex", - Details: fmt.Sprintf("You provided flavor ID '%s', which is invalid.", flavorId), - } -} - -func ValidateStorage(storageClass *string, storageSize *int64, storages *mongodbflex.ListStoragesResponse, flavorId string) error { - if storages == nil { - return fmt.Errorf("nil storages") - } - - if storageSize != nil { - if *storageSize < *storages.StorageRange.Min || *storageSize > *storages.StorageRange.Max { - return fmt.Errorf("%s", fmt.Sprintf("You provided storage size '%d', which is invalid. The valid range is %d-%d.", *storageSize, *storages.StorageRange.Min, *storages.StorageRange.Max)) - } - } - - if storageClass == nil { - return nil - } - - for _, sc := range storages.StorageClasses { - if strings.EqualFold(*storageClass, sc) { - return nil - } - } - return &errors.DatabaseInvalidStorageError{ - Service: "mongodbflex", - Details: fmt.Sprintf("You provided storage class '%s', which is invalid.", *storageClass), - FlavorId: flavorId, - } -} - +// Deprecated: Will be removed after 2027-03-07 func LoadFlavorId(cpu, ram int32, flavors *[]mongodbflex.InstanceFlavor) (*string, error) { if flavors == nil { return nil, fmt.Errorf("nil flavors") @@ -123,7 +62,8 @@ func LoadFlavorId(cpu, ram int32, flavors *[]mongodbflex.InstanceFlavor) (*strin } } -func GetLatestMongoDBVersion(ctx context.Context, apiClient MongoDBFlexClient, projectId, region string) (string, error) { +// Deprecated: Will be removed after 2027-03-07. +func GetLatestMongoDBVersion(ctx context.Context, apiClient mongodbflex.DefaultAPI, projectId, region string) (string, error) { resp, err := apiClient.ListVersions(ctx, projectId, region).Execute() if err != nil { return "", fmt.Errorf("get MongoDB versions: %w", err) @@ -144,7 +84,7 @@ func GetLatestMongoDBVersion(ctx context.Context, apiClient MongoDBFlexClient, p return latestVersion, nil } -func GetInstanceName(ctx context.Context, apiClient MongoDBFlexClient, projectId, instanceId, region string) (string, error) { +func GetInstanceName(ctx context.Context, apiClient mongodbflex.DefaultAPI, projectId, instanceId, region string) (string, error) { resp, err := apiClient.GetInstance(ctx, projectId, instanceId, region).Execute() if err != nil { return "", fmt.Errorf("get MongoDB Flex instance: %w", err) @@ -152,7 +92,7 @@ func GetInstanceName(ctx context.Context, apiClient MongoDBFlexClient, projectId return *resp.Item.Name, nil } -func GetUserName(ctx context.Context, apiClient MongoDBFlexClient, projectId, instanceId, userId, region string) (string, error) { +func GetUserName(ctx context.Context, apiClient mongodbflex.DefaultAPI, projectId, instanceId, userId, region string) (string, error) { resp, err := apiClient.GetUser(ctx, projectId, instanceId, userId, region).Execute() if err != nil { return "", fmt.Errorf("get MongoDB Flex user: %w", err) diff --git a/internal/pkg/services/mongodbflex/utils/utils_test.go b/internal/pkg/services/mongodbflex/utils/utils_test.go index 7960bf501..e4aebe019 100644 --- a/internal/pkg/services/mongodbflex/utils/utils_test.go +++ b/internal/pkg/services/mongodbflex/utils/utils_test.go @@ -25,7 +25,18 @@ const ( testUserName = "user" ) -func newAPIClientMock(m clientMockSettings) mongodbflex.DefaultAPI { +type mockSettings struct { + listVersionsFails bool + listVersionsResp *mongodbflex.ListVersionsResponse + getInstanceFails bool + getInstanceResp *mongodbflex.InstanceResponse + getUserFails bool + getUserResp *mongodbflex.GetUserResponse + listRestoreJobsFails bool + listRestoreJobsResp *mongodbflex.ListRestoreJobsResponse +} + +func newAPIClientMock(m mockSettings) mongodbflex.DefaultAPI { return mongodbflex.DefaultAPIServiceMock{ ListVersionsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListVersionsRequest) (*mongodbflex.ListVersionsResponse, error) { if m.listVersionsFails { @@ -54,189 +65,6 @@ func newAPIClientMock(m clientMockSettings) mongodbflex.DefaultAPI { } } -type clientMockSettings struct { - listVersionsFails bool - listVersionsResp *mongodbflex.ListVersionsResponse - getInstanceFails bool - getInstanceResp *mongodbflex.InstanceResponse - getUserFails bool - getUserResp *mongodbflex.GetUserResponse - listRestoreJobsFails bool - listRestoreJobsResp *mongodbflex.ListRestoreJobsResponse -} - -func TestValidateStorage(t *testing.T) { - tests := []struct { - description string - storageClass *string - storageSize *int64 - storages *mongodbflex.ListStoragesResponse - isValid bool - }{ - { - description: "base", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(10)), - storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(5)), - Max: utils.Ptr(int64(20)), - }, - }, - isValid: true, - }, - { - description: "nil response", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(10)), - storages: nil, - isValid: false, - }, - { - description: "storage size out of range 1", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(1)), - storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(5)), - Max: utils.Ptr(int64(20)), - }, - }, - isValid: false, - }, - { - description: "storage size out of range 2", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(200)), - storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(5)), - Max: utils.Ptr(int64(20)), - }, - }, - isValid: false, - }, - { - description: "storage size in range limit 1", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(5)), - storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(5)), - Max: utils.Ptr(int64(20)), - }, - }, - isValid: true, - }, - { - description: "storage size in range limit 2", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(20)), - storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(5)), - Max: utils.Ptr(int64(20)), - }, - }, - isValid: true, - }, - { - description: "invalid storage", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(10)), - storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "bar-3"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(5)), - Max: utils.Ptr(int64(20)), - }, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := ValidateStorage(tt.storageClass, tt.storageSize, tt.storages, "flavor-id") - if tt.isValid && err != nil { - t.Fatalf("should not have failed: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("should have failed") - } - }) - } -} - -func TestValidateFlavorId(t *testing.T) { - tests := []struct { - description string - flavorId string - flavors []mongodbflex.InstanceFlavor - isValid bool - }{ - { - description: "base", - flavorId: "foo", - flavors: []mongodbflex.InstanceFlavor{ - {Id: utils.Ptr("bar-1")}, - {Id: utils.Ptr("bar-2")}, - {Id: utils.Ptr("foo")}, - }, - isValid: true, - }, - { - description: "nil flavors", - flavorId: "foo", - flavors: nil, - isValid: false, - }, - { - description: "no flavors", - flavorId: "foo", - flavors: []mongodbflex.InstanceFlavor{}, - isValid: false, - }, - { - description: "nil flavor id", - flavorId: "foo", - flavors: []mongodbflex.InstanceFlavor{ - {Id: utils.Ptr("bar-1")}, - {Id: nil}, - {Id: utils.Ptr("foo")}, - }, - isValid: true, - }, - { - description: "invalid flavor", - flavorId: "foo", - flavors: []mongodbflex.InstanceFlavor{ - {Id: utils.Ptr("bar-1")}, - {Id: utils.Ptr("bar-2")}, - {Id: utils.Ptr("bar-3")}, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := ValidateFlavorId(tt.flavorId, tt.flavors) - if tt.isValid && err != nil { - t.Fatalf("should not have failed: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("should have failed") - } - }) - } -} - func TestLoadFlavorId(t *testing.T) { tests := []struct { description string @@ -408,7 +236,7 @@ func TestGetLatestMongoDBFlexVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - settings := clientMockSettings{ + settings := mockSettings{ listVersionsFails: tt.listVersionsFails, listVersionsResp: tt.listVersionsResp, } @@ -458,7 +286,7 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - settings := clientMockSettings{ + settings := mockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } @@ -508,7 +336,7 @@ func TestGetUserName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - settings := clientMockSettings{ + settings := mockSettings{ getUserFails: tt.getUserFails, getUserResp: tt.getUserResp, } @@ -646,55 +474,3 @@ func TestGetRestoreStatus(t *testing.T) { }) } } - -func TestGetInstanceType(t *testing.T) { - tests := []struct { - description string - numReplicas int32 - expectedOutput string - isValid bool - }{ - { - description: "single", - numReplicas: 1, - expectedOutput: "Single", - isValid: true, - }, - { - description: "replica set", - numReplicas: 3, - expectedOutput: "Replica", - isValid: true, - }, - { - description: "sharded cluster", - numReplicas: 9, - expectedOutput: "Sharded", - isValid: true, - }, - { - description: "invalid", - numReplicas: 0, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := GetInstanceType(tt.numReplicas) - if !tt.isValid { - if err == nil { - t.Fatalf("did not fail on invalid input") - } - return - } - if err != nil { - t.Fatalf("failed on valid input: %v", err) - } - - if output != tt.expectedOutput { - t.Fatalf("expected output to be %s, got %s", tt.expectedOutput, output) - } - }) - } -}