diff --git a/docs/stackit_postgresflex.md b/docs/stackit_postgresflex.md index 049c614ca..fd22939cd 100644 --- a/docs/stackit_postgresflex.md +++ b/docs/stackit_postgresflex.md @@ -31,7 +31,8 @@ stackit postgresflex [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit postgresflex backup](./stackit_postgresflex_backup.md) - Provides functionality for PostgreSQL Flex instance backups +* [stackit postgresflex flavor](./stackit_postgresflex_flavor.md) - Provides functionality for PostgreSQL Flex flavors * [stackit postgresflex instance](./stackit_postgresflex_instance.md) - Provides functionality for PostgreSQL Flex instances -* [stackit postgresflex options](./stackit_postgresflex_options.md) - Lists PostgreSQL Flex options * [stackit postgresflex user](./stackit_postgresflex_user.md) - Provides functionality for PostgreSQL Flex users +* [stackit postgresflex version](./stackit_postgresflex_version.md) - Provides functionality for PostgreSQL Flex versions diff --git a/docs/stackit_postgresflex_backup.md b/docs/stackit_postgresflex_backup.md index 667a0d3ac..b0c000fd3 100644 --- a/docs/stackit_postgresflex_backup.md +++ b/docs/stackit_postgresflex_backup.md @@ -32,5 +32,4 @@ stackit postgresflex backup [flags] * [stackit postgresflex](./stackit_postgresflex.md) - Provides functionality for PostgreSQL Flex * [stackit postgresflex backup describe](./stackit_postgresflex_backup_describe.md) - Shows details of a backup for a PostgreSQL Flex instance * [stackit postgresflex backup list](./stackit_postgresflex_backup_list.md) - Lists all backups which are available for a PostgreSQL Flex instance -* [stackit postgresflex backup update-schedule](./stackit_postgresflex_backup_update-schedule.md) - Updates backup schedule for a PostgreSQL Flex instance diff --git a/docs/stackit_postgresflex_backup_update-schedule.md b/docs/stackit_postgresflex_backup_update-schedule.md deleted file mode 100644 index fa912af61..000000000 --- a/docs/stackit_postgresflex_backup_update-schedule.md +++ /dev/null @@ -1,42 +0,0 @@ -## stackit postgresflex backup update-schedule - -Updates backup schedule for a PostgreSQL Flex instance - -### Synopsis - -Updates backup schedule for a PostgreSQL Flex instance. The current backup schedule can be seen in the output of the "stackit postgresflex instance describe" command. - -``` -stackit postgresflex backup update-schedule [flags] -``` - -### Examples - -``` - Update the backup schedule of a PostgreSQL Flex instance with ID "xxx" - $ stackit postgresflex backup update-schedule --instance-id xxx --schedule '6 6 * * *' -``` - -### Options - -``` - -h, --help Help for "stackit postgresflex backup update-schedule" - --instance-id string Instance ID - --schedule string Backup schedule, in the cron scheduling system format e.g. '0 0 * * *' -``` - -### 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 postgresflex backup](./stackit_postgresflex_backup.md) - Provides functionality for PostgreSQL Flex instance backups - diff --git a/docs/stackit_postgresflex_flavor.md b/docs/stackit_postgresflex_flavor.md new file mode 100644 index 000000000..0a3a987d1 --- /dev/null +++ b/docs/stackit_postgresflex_flavor.md @@ -0,0 +1,35 @@ +## stackit postgresflex flavor + +Provides functionality for PostgreSQL Flex flavors + +### Synopsis + +Provides functionality for PostgreSQL Flex flavors. + +``` +stackit postgresflex flavor [flags] +``` + +### Options + +``` + -h, --help Help for "stackit postgresflex 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 postgresflex](./stackit_postgresflex.md) - Provides functionality for PostgreSQL Flex +* [stackit postgresflex flavor describe](./stackit_postgresflex_flavor_describe.md) - Show details of a PostgreSQL Flex flavor +* [stackit postgresflex flavor list](./stackit_postgresflex_flavor_list.md) - Lists PostgreSQL Flex flavors + diff --git a/docs/stackit_postgresflex_flavor_describe.md b/docs/stackit_postgresflex_flavor_describe.md new file mode 100644 index 000000000..2337f439e --- /dev/null +++ b/docs/stackit_postgresflex_flavor_describe.md @@ -0,0 +1,40 @@ +## stackit postgresflex flavor describe + +Show details of a PostgreSQL Flex flavor + +### Synopsis + +Show details of a PostgreSQL Flex flavor. + +``` +stackit postgresflex flavor describe [flags] +``` + +### Examples + +``` + Show PostgreSQL Flex flavor details + $ stackit postgresflex flavor describe +``` + +### Options + +``` + -h, --help Help for "stackit postgresflex flavor describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, 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 postgresflex flavor](./stackit_postgresflex_flavor.md) - Provides functionality for PostgreSQL Flex flavors + diff --git a/docs/stackit_postgresflex_flavor_list.md b/docs/stackit_postgresflex_flavor_list.md new file mode 100644 index 000000000..c37d129bc --- /dev/null +++ b/docs/stackit_postgresflex_flavor_list.md @@ -0,0 +1,40 @@ +## stackit postgresflex flavor list + +Lists PostgreSQL Flex flavors + +### Synopsis + +Lists PostgreSQL Flex flavors. + +``` +stackit postgresflex flavor list [flags] +``` + +### Examples + +``` + List PostgreSQL Flex flavor + $ stackit postgresflex flavor list +``` + +### Options + +``` + -h, --help Help for "stackit postgresflex flavor 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 postgresflex flavor](./stackit_postgresflex_flavor.md) - Provides functionality for PostgreSQL Flex flavors + diff --git a/docs/stackit_postgresflex_instance_clone.md b/docs/stackit_postgresflex_instance_clone.md index a435dd61e..349443305 100644 --- a/docs/stackit_postgresflex_instance_clone.md +++ b/docs/stackit_postgresflex_instance_clone.md @@ -14,13 +14,7 @@ stackit postgresflex instance clone INSTANCE_ID [flags] ``` Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp. - $ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 - - Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage class. - $ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-class premium-perf6-stackit - - Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage size. - $ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10 + $ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10 --storage-class premium-perf6-stackit ``` ### Options @@ -28,8 +22,8 @@ stackit postgresflex instance clone INSTANCE_ID [flags] ``` -h, --help Help for "stackit postgresflex instance clone" --recovery-timestamp string Recovery timestamp for the instance, in a date-time with the layout format YYYY-MM-DDTHH:mm:ss±HH:mm, e.g. 2006-01-02T15:04:05-07:00 - --storage-class string Storage class. If not specified, storage class from the existing instance will be used. - --storage-size int Storage size (in GB). If not specified, storage size from the existing instance will be used. + --storage-class string Storage class. If not specified, storage class from the existing instance will be used. This flag will be required after 2027-01-31. + --storage-size int Storage size (in GB). If not specified, storage size from the existing instance will be used. This flag will be required after 2027-01-31. ``` ### Options inherited from parent commands diff --git a/docs/stackit_postgresflex_instance_create.md b/docs/stackit_postgresflex_instance_create.md index 65150d045..d8f71bc6d 100644 --- a/docs/stackit_postgresflex_instance_create.md +++ b/docs/stackit_postgresflex_instance_create.md @@ -13,30 +13,29 @@ stackit postgresflex instance create [flags] ### Examples ``` - Create a PostgreSQL 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 postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 0.0.0.0/0 + Create a PostgreSQL Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access). + $ stackit postgresflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 --storage-size 20 --retention-days 32 --version 17 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-stackit - Create a PostgreSQL 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 postgresflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 - - Create a PostgreSQL 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 postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 1.2.3.0/24 --storage-size 20 + Create a PostgreSQL Flex instance with name "my-instance", allow access to a specific range of IP addresses. + $ stackit postgresflex instance create --name my-instance --flavor-id xxx --acl 1.2.3.0/24 --storage-size 20 --retention-days 32 --version 17 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-stackit ``` ### 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 * * *") - --cpu int Number of CPUs - --flavor-id string ID of the flavor - -h, --help Help for "stackit postgresflex instance create" - -n, --name string Instance name - --ram int Amount of RAM (in GB) - --storage-class string Storage class (default "premium-perf2-stackit") - --storage-size int Storage size (in GB) (default 10) - --type string Instance type, (one of: [Replica, Single]) (default "Replica") - --version string PostgreSQL version. Defaults to the latest version available + --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. This flag will be required after 2027-01-31. + --encryption-kek-key-id string The key identifier + --encryption-kek-key-version string The key version + --encryption-kek-keyring-id string The keyring identifier + --encryption-service-account string The service account + --flavor-id string ID of the flavor. This flag will be required after 2027-01-31. + -h, --help Help for "stackit postgresflex instance create" + -n, --name string Instance name + --retention-days int32 The days for how long the backup files should be stored before cleaned up (32 to 90). This flag will be required after 2027-01-31. + --storage-class string Storage class. This flag will be required after 2027-01-31. + --storage-size int Storage size (in GB). This flag will be required after 2027-01-31. + --version string PostgreSQL version. Defaults to the latest version available. This flag will be required after 2027-01-31. ``` ### Options inherited from parent commands diff --git a/docs/stackit_postgresflex_instance_delete.md b/docs/stackit_postgresflex_instance_delete.md index c652d3745..abc3b7789 100644 --- a/docs/stackit_postgresflex_instance_delete.md +++ b/docs/stackit_postgresflex_instance_delete.md @@ -25,8 +25,7 @@ stackit postgresflex instance delete INSTANCE_ID [flags] ### Options ``` - -f, --force Force deletion of a delayed deleted instance - -h, --help Help for "stackit postgresflex instance delete" + -h, --help Help for "stackit postgresflex instance delete" ``` ### Options inherited from parent commands diff --git a/docs/stackit_postgresflex_instance_update.md b/docs/stackit_postgresflex_instance_update.md index 511ccbc9c..edb680213 100644 --- a/docs/stackit_postgresflex_instance_update.md +++ b/docs/stackit_postgresflex_instance_update.md @@ -25,14 +25,11 @@ stackit postgresflex instance update INSTANCE_ID [flags] ``` --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) --backup-schedule string Backup schedule - --cpu int Number of CPUs --flavor-id string ID of the flavor -h, --help Help for "stackit postgresflex instance update" -n, --name string Instance name - --ram int Amount of RAM (in GB) - --storage-class string Storage class + --retention-days string The days for how long the backup files should be stored before cleaned up (32 to 90). --storage-size int Storage size (in GB) - --type string Instance type, (one of: [Replica, Single]) --version string Version ``` diff --git a/docs/stackit_postgresflex_options.md b/docs/stackit_postgresflex_options.md deleted file mode 100644 index d6dc73d98..000000000 --- a/docs/stackit_postgresflex_options.md +++ /dev/null @@ -1,51 +0,0 @@ -## stackit postgresflex options - -Lists PostgreSQL Flex options - -### Synopsis - -Lists PostgreSQL Flex options (flavors, versions and storages for a given flavor) -Pass one or more flags to filter what categories are shown. - -``` -stackit postgresflex options [flags] -``` - -### Examples - -``` - List PostgreSQL Flex flavors options - $ stackit postgresflex options --flavors - - List PostgreSQL Flex available versions - $ stackit postgresflex options --versions - - 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 -``` - -### 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 postgresflex 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 postgresflex](./stackit_postgresflex.md) - Provides functionality for PostgreSQL Flex - diff --git a/docs/stackit_postgresflex_version.md b/docs/stackit_postgresflex_version.md new file mode 100644 index 000000000..dff8bb2ae --- /dev/null +++ b/docs/stackit_postgresflex_version.md @@ -0,0 +1,34 @@ +## stackit postgresflex version + +Provides functionality for PostgreSQL Flex versions + +### Synopsis + +Provides functionality for PostgreSQL Flex versions. + +``` +stackit postgresflex version [flags] +``` + +### Options + +``` + -h, --help Help for "stackit postgresflex 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 postgresflex](./stackit_postgresflex.md) - Provides functionality for PostgreSQL Flex +* [stackit postgresflex version list](./stackit_postgresflex_version_list.md) - Lists PostgreSQL Flex versions + diff --git a/docs/stackit_postgresflex_version_list.md b/docs/stackit_postgresflex_version_list.md new file mode 100644 index 000000000..7bb96f7ca --- /dev/null +++ b/docs/stackit_postgresflex_version_list.md @@ -0,0 +1,40 @@ +## stackit postgresflex version list + +Lists PostgreSQL Flex versions + +### Synopsis + +Lists PostgreSQL Flex versions. + +``` +stackit postgresflex version list [flags] +``` + +### Examples + +``` + List PostgreSQL Flex version options + $ stackit postgresflex version list +``` + +### Options + +``` + -h, --help Help for "stackit postgresflex 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 postgresflex version](./stackit_postgresflex_version.md) - Provides functionality for PostgreSQL Flex versions + diff --git a/go.mod b/go.mod index c4783d647..902cb99b2 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.0 github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.1.0 - github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.11.0 + github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.12.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0 github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.8.0 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.18.1 diff --git a/go.sum b/go.sum index e685c4296..3cbcba3a4 100644 --- a/go.sum +++ b/go.sum @@ -626,8 +626,8 @@ github.com/stackitcloud/stackit-sdk-go/services/observability v0.24.0 h1:KhuWPXr github.com/stackitcloud/stackit-sdk-go/services/observability v0.24.0/go.mod h1:0fEZQHm729mBdvg4sNrAhM6KmHROHJSeS2FwCMRk46k= github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.1.0 h1:hooc/E9Qabn8xno1NUd3uJQfUbW5KoY6mgURlnd776c= github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.1.0/go.mod h1:L+NlfC1hilLOqlLLukCj/UDnxlnNrc/oMikcw3Ansyw= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.11.0 h1:cuI4NhuFhaZ3tTkBpUM7nt2odKFJkyCcphT/3gGb9CE= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.11.0/go.mod h1:yzlakB+f8ur4yAHR6lyCABO+HcEtZG3G2Faj6m5/uW8= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.12.0 h1:elIyrDVSYlEtigFO82SWnpfuJZdpv0b2SwvL3M+z9dA= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.12.0/go.mod h1:yzlakB+f8ur4yAHR6lyCABO+HcEtZG3G2Faj6m5/uW8= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v1.1.1 h1:Vly7OKWAwfrbE31N/tUMmLkl7pKybSnek0IPh8offRk= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v1.1.1/go.mod h1:TwfVVynB/+AKbccSOLk2qZpPL1tdK43BBAiACP6EtSg= github.com/stackitcloud/stackit-sdk-go/services/redis v1.1.0 h1:ckYMRXAGE2/vaxPeNGEuun9AGcFn/8xuu0ytfsiqfWU= diff --git a/internal/cmd/postgresflex/backup/backup.go b/internal/cmd/postgresflex/backup/backup.go index f6ad7c518..2c8f1ad5f 100644 --- a/internal/cmd/postgresflex/backup/backup.go +++ b/internal/cmd/postgresflex/backup/backup.go @@ -26,5 +26,5 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) - cmd.AddCommand(updateschedule.NewCmd(params)) + cmd.AddCommand(updateschedule.NewCmd(params)) //nolint:staticcheck // Command is deprecated but must be kept for backward compatibility } diff --git a/internal/cmd/postgresflex/backup/describe/describe.go b/internal/cmd/postgresflex/backup/describe/describe.go index effe99ea0..077541946 100644 --- a/internal/cmd/postgresflex/backup/describe/describe.go +++ b/internal/cmd/postgresflex/backup/describe/describe.go @@ -3,12 +3,12 @@ package describe import ( "context" "fmt" - "time" + "strconv" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -18,24 +18,19 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( backupIdArg = "BACKUP_ID" instanceIdFlag = "instance-id" - - backupExpireYearOffset = 0 - backupExpireMonthOffset = 0 - backupExpireDayOffset = 30 ) type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - BackupId string + BackupId int64 } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -73,7 +68,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("describe backup for PostgreSQL Flex instance: %w", err) } - return outputResult(params.Printer, model.OutputFormat, resp.Item) + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -88,13 +83,18 @@ func configureFlags(cmd *cobra.Command) { } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - backupId := inputArgs[0] + backupIdStr := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + backupId, err := strconv.ParseInt(backupIdStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid backup id format, must be an integer: %w", err) + } + return &inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), @@ -107,30 +107,20 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresfle return req } -func outputResult(p *print.Printer, outputFormat string, backup *postgresflex.Backup) error { - if backup == nil { - return fmt.Errorf("backup is nil") - } - if backup.StartTime == nil || *backup.StartTime == "" { - return fmt.Errorf("start time not defined") - } - backupStartTime, err := time.Parse(time.RFC3339, utils.PtrString(backup.StartTime)) - if err != nil { - return fmt.Errorf("parse backup start time: %w", err) - } - backupExpireDate := backupStartTime.AddDate(backupExpireYearOffset, backupExpireMonthOffset, backupExpireDayOffset).Format(time.DateOnly) - +func outputResult(p *print.Printer, outputFormat string, backup *postgresflex.BackupData) error { return p.OutputResult(outputFormat, backup, func() error { + if backup == nil { + return fmt.Errorf("backup is nil") + } + table := tables.NewTable() - table.AddRow("ID", utils.PtrString(backup.Id)) + table.AddRow("ID", backup.Id) table.AddSeparator() - table.AddRow("CREATED AT", utils.PtrString(backup.StartTime)) + table.AddRow("COMPLETED AT", backup.CompletionTime) table.AddSeparator() - table.AddRow("EXPIRES AT", backupExpireDate) + table.AddRow("RETAINED UNTIL", backup.RetainedUntil) table.AddSeparator() - - backupSize := utils.PtrByteSizeDefault(backup.Size, "n/a") - table.AddRow("BACKUP SIZE", backupSize) + table.AddRow("BACKUP SIZE", backup.Size) err := table.Display(p) if err != nil { diff --git a/internal/cmd/postgresflex/backup/describe/describe_test.go b/internal/cmd/postgresflex/backup/describe/describe_test.go index a77f0fed5..09efb80a0 100644 --- a/internal/cmd/postgresflex/backup/describe/describe_test.go +++ b/internal/cmd/postgresflex/backup/describe/describe_test.go @@ -2,17 +2,19 @@ package describe import ( "context" + "strconv" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) type testCtxKey struct{} @@ -21,12 +23,15 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testBackupId = "backupID" -var testRegion = "eu01" + +const ( + testBackupId = int64(42) + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testBackupId, + strconv.FormatInt(testBackupId, 10), } for _, mod := range mods { mod(argValues) @@ -243,7 +248,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string - backup *postgresflex.Backup + backup *postgresflex.BackupData } tests := []struct { name string @@ -251,35 +256,32 @@ func Test_outputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, + name: "backup is nil", + args: args{ + backup: nil, + outputFormat: print.PrettyOutputFormat, + }, wantErr: true, }, { - name: "standard", + name: "empty backup", args: args{ - outputFormat: "", - backup: &postgresflex.Backup{ - StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), - }, + outputFormat: print.PrettyOutputFormat, + backup: &postgresflex.BackupData{}, }, wantErr: false, }, { name: "complete", args: args{ - outputFormat: "", - backup: &postgresflex.Backup{ - EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), - Id: utils.Ptr("id"), - Labels: []string{"foo", - "bar", - "baz", - }, - Name: utils.Ptr("name"), - Options: &map[string]string{"test1": "test1", "test2": "test2"}, - Size: utils.Ptr(int64(42)), - StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), + outputFormat: print.PrettyOutputFormat, + backup: &postgresflex.BackupData{ + CompletionTime: time.Now().Format(time.RFC3339), + Id: int64(1), + Name: "name", + RetainedUntil: time.Now().Format(time.RFC3339), + Size: int64(42), + Type: "type", }, }, wantErr: false, diff --git a/internal/cmd/postgresflex/backup/list/list.go b/internal/cmd/postgresflex/backup/list/list.go index 0b4d763e9..c04285233 100644 --- a/internal/cmd/postgresflex/backup/list/list.go +++ b/internal/cmd/postgresflex/backup/list/list.go @@ -6,6 +6,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "time" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,20 +17,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "time" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) const ( - instanceIdFlag = "instance-id" - limitFlag = "limit" - backupExpireYearOffset = 0 - backupExpireMonthOffset = 0 - backupExpireDayOffset = 30 + instanceIdFlag = "instance-id" + limitFlag = "limit" ) type inputModel struct { @@ -80,7 +76,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get backups for PostgreSQL Flex instance %q: %w", instanceLabel, err) } - backups := resp.Items + backups := resp.Backups // Truncate output if model.Limit != nil && len(backups) > int(*model.Limit) { @@ -129,29 +125,32 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresfle return req } -func outputResult(p *print.Printer, outputFormat, instanceLabel string, backups []postgresflex.Backup) error { +func outputResult(p *print.Printer, outputFormat, instanceLabel string, backups []postgresflex.BackupData) error { return p.OutputResult(outputFormat, backups, func() error { if len(backups) == 0 { p.Outputf("No backups found for instance %q", instanceLabel) return nil } + table := tables.NewTable() - table.SetHeader("ID", "CREATED AT", "EXPIRES AT", "BACKUP SIZE") - for i := range backups { - backup := backups[i] + table.SetHeader("ID", "CREATED AT", "RETAINED UNTIL", "BACKUP SIZE") + + for _, backup := range backups { + backupCompletionTime, err := time.Parse(time.RFC3339, backup.CompletionTime) + if err != nil { + return fmt.Errorf("parse backup completion time: %w", err) + } - backupStartTime, err := time.Parse(time.RFC3339, utils.PtrString(backup.StartTime)) + backupRetainedUntilTime, err := time.Parse(time.RFC3339, backup.RetainedUntil) if err != nil { - return fmt.Errorf("parse backup start time: %w", err) + return fmt.Errorf("parse backup retained until time: %w", err) } - backupExpireDate := backupStartTime.AddDate(backupExpireYearOffset, backupExpireMonthOffset, backupExpireDayOffset).Format(time.DateOnly) - backupSize := utils.PtrByteSizeDefault(backup.Size, "n/a") table.AddRow( - utils.PtrString(backup.Id), - utils.PtrString(backup.StartTime), - backupExpireDate, - backupSize, + backup.Id, + backupCompletionTime, + backupRetainedUntilTime, + backup.Size, ) } err := table.Display(p) diff --git a/internal/cmd/postgresflex/backup/list/list_test.go b/internal/cmd/postgresflex/backup/list/list_test.go index 34177acb8..f2b9105a3 100644 --- a/internal/cmd/postgresflex/backup/list/list_test.go +++ b/internal/cmd/postgresflex/backup/list/list_test.go @@ -12,7 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -179,36 +179,53 @@ func Test_outputResult(t *testing.T) { type args struct { outputFormat string instanceLabel string - backups []postgresflex.Backup + backups []postgresflex.BackupData } tests := []struct { name string args args wantErr bool }{ - {"empty", args{}, false}, - {"standard", args{outputFormat: "", instanceLabel: "label", backups: []postgresflex.Backup{}}, false}, - {"complete", args{outputFormat: "", instanceLabel: "label", backups: []postgresflex.Backup{ - { - EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), - Id: utils.Ptr("id"), - Labels: []string{"foo", "bar", "baz"}, - Name: utils.Ptr("name"), - Options: &map[string]string{"test1": "test1", "test2": "test2"}, - Size: utils.Ptr(int64(42)), - StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "standard", + args: args{ + outputFormat: "", + instanceLabel: "label", + backups: []postgresflex.BackupData{}, }, - { - EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), - Id: utils.Ptr("id"), - Labels: []string{"foo", "bar", "baz"}, - Name: utils.Ptr("name"), - Options: &map[string]string{"test1": "test1", "test2": "test2"}, - Size: utils.Ptr(int64(42)), - StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), + wantErr: false, + }, + { + name: "complete", + args: args{ + outputFormat: "", + instanceLabel: "label", + backups: []postgresflex.BackupData{ + { + CompletionTime: time.Now().Format(time.RFC3339), + Id: int64(1), + Name: "name", + RetainedUntil: time.Now().Format(time.RFC3339), + Size: int64(42), + Type: "type", + }, + { + CompletionTime: time.Now().Format(time.RFC3339), + Id: int64(2), + Name: "name", + RetainedUntil: time.Now().Format(time.RFC3339), + Size: int64(42), + Type: "type", + }, + }, }, + wantErr: false, }, - }, false}, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go b/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go index 8b200f83a..ebb22bb60 100644 --- a/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go +++ b/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflexLegacy "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -20,10 +20,13 @@ import ( ) const ( + // Deprecated: Will be removed after 2027-01-31. instanceIdFlag = "instance-id" - scheduleFlag = "schedule" + // Deprecated: Will be removed after 2027-01-31. + scheduleFlag = "schedule" ) +// Deprecated: Will be removed after 2027-01-31. type inputModel struct { *globalflags.GlobalFlagModel @@ -31,12 +34,14 @@ type inputModel struct { BackupSchedule string } +// Deprecated: Will be removed after 2027-01-31. func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "update-schedule", - Short: "Updates backup schedule for a PostgreSQL Flex instance", - Long: `Updates backup schedule for a PostgreSQL Flex instance. The current backup schedule can be seen in the output of the "stackit postgresflex instance describe" command.`, - Args: args.NoArgs, + Use: "update-schedule", + Short: "Updates backup schedule for a PostgreSQL Flex instance", + Long: `Updates backup schedule for a PostgreSQL Flex instance. The current backup schedule can be seen in the output of the "stackit postgresflex instance describe" command.`, + Args: args.NoArgs, + Deprecated: `Command "stackit postgresflex backup update-schedule" is deprecated and will be removed after 2027-01-31. Please use "stackit postgresflex instance update --backup-schedule" instead.`, Example: examples.Build( examples.NewExample( `Update the backup schedule of a PostgreSQL Flex instance with ID "xxx"`, @@ -57,6 +62,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } + apiClientLegacy, err := client.ConfigureClientLegacy(params.Printer, params.CliVersion) + if err != nil { + return err + } + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) @@ -70,7 +80,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClientLegacy) err = req.Execute() if err != nil { return fmt.Errorf("update backup schedule of PostgreSQL Flex instance: %w", err) @@ -84,6 +94,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return cmd } +// Deprecated: Will be removed after 2027-01-31. func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") cmd.Flags().String(scheduleFlag, "", "Backup schedule, in the cron scheduling system format e.g. '0 0 * * *'") @@ -92,6 +103,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } +// Deprecated: Will be removed after 2027-01-31. func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { @@ -105,9 +117,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiUpdateBackupScheduleRequest { +// Deprecated: Will be removed after 2027-01-31. +func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflexLegacy.APIClient) postgresflexLegacy.ApiUpdateBackupScheduleRequest { req := apiClient.DefaultAPI.UpdateBackupSchedule(ctx, model.ProjectId, model.Region, model.InstanceId) - req = req.UpdateBackupSchedulePayload(postgresflex.UpdateBackupSchedulePayload{ + req = req.UpdateBackupSchedulePayload(postgresflexLegacy.UpdateBackupSchedulePayload{ BackupSchedule: model.BackupSchedule, }) return req diff --git a/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go index 93ef49667..4b33301d6 100644 --- a/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go +++ b/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go @@ -19,8 +19,9 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testSchedule = "0 0 * * *" -var testRegion = "eu01" + +const testSchedule = "0 0 * * *" +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/flavor/describe/describe.go b/internal/cmd/postgresflex/flavor/describe/describe.go new file mode 100644 index 000000000..fdfb0139e --- /dev/null +++ b/internal/cmd/postgresflex/flavor/describe/describe.go @@ -0,0 +1,137 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "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/postgresflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + flavorIdArg = "FLAVOR_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + FlavorId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Show details of a PostgreSQL Flex flavor", + Long: "Show details of a PostgreSQL Flex flavor.", + Args: args.SingleArg(flavorIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Show PostgreSQL Flex flavor details`, + "$ stackit postgresflex flavor describe "), + ), + 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 + flavor, err := buildAndExecuteRequest(ctx, model, apiClient.DefaultAPI) + if err != nil { + return fmt.Errorf("get PostgreSQL Flex flavor: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, flavor) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + flavorId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + model := inputModel{ + GlobalFlagModel: globalFlags, + FlavorId: flavorId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildAndExecuteRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (*postgresflex.ListFlavors, error) { + // Note: There's currently no GET endpoint for a single flavor. So we have to use the list endpoint and + // do client-site filtering for the correct flavor. A feature request has been opened to add a GET endpoint + // for a single flavor to the API. + + // TODO: Use GET endpoint instead of list endpoint after https://support.stackit.cloud/servicedesk/customer/portal/3/SSD-23311 is done + + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Size(100).Execute() + if err != nil { + return nil, fmt.Errorf("listing flavors: %w", err) + } + + for _, flavor := range flavors.Flavors { + if flavor.Id == model.FlavorId { + return &flavor, nil + } + } + + return nil, fmt.Errorf("flavor \"%s\" not found", model.FlavorId) +} + +func outputResult(p *print.Printer, outputFormat string, flavor *postgresflex.ListFlavors) error { + return p.OutputResult(outputFormat, flavor, func() error { + if flavor == nil { + return fmt.Errorf("flavor is nil") + } + + table := tables.NewTable() + table.SetTitle("Flavor") + table.AddRow("ID", flavor.Id) + table.AddSeparator() + table.AddRow("CPU", flavor.Cpu) + table.AddSeparator() + table.AddRow("MEMORY (GiB)", flavor.Memory) + table.AddSeparator() + table.AddRow("NODE TYPE", flavor.NodeType) + table.AddSeparator() + table.AddRow("MIN STORAGE (GB)", flavor.MinGB) + table.AddSeparator() + table.AddRow("MAX STORAGE (GB)", flavor.MaxGB) + table.AddSeparator() + table.AddRow("DESCRIPTION", flavor.Description) + + storageClassesTable := tables.NewTable() + storageClassesTable.SetTitle("Storages") + storageClassesTable.SetHeader("STORAGE CLASS", "MAX IO PER SEC", "MAX THROUGH (MB)") + + for _, sc := range flavor.StorageClasses { + storageClassesTable.AddRow(sc.Class, sc.MaxIoPerSec, sc.MaxThroughInMb) + storageClassesTable.AddSeparator() + } + + return tables.DisplayTables(p, []tables.Table{table, storageClassesTable}) + }) +} diff --git a/internal/cmd/postgresflex/flavor/describe/describe_test.go b/internal/cmd/postgresflex/flavor/describe/describe_test.go new file mode 100644 index 000000000..8a22dd4dd --- /dev/null +++ b/internal/cmd/postgresflex/flavor/describe/describe_test.go @@ -0,0 +1,257 @@ +package describe + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var ( + testProjectId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testFlavorId = "16.64" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testFlavorId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +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, + }, + FlavorId: testFlavorId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureTestFlavor() postgresflex.ListFlavors { + return postgresflex.ListFlavors{ + Id: testFlavorId, + Cpu: 16, + Description: "PostgreSQL-Flex-16.64-Single-EU01", + MaxGB: 4000, + MinGB: 5, + NodeType: "Single", + StorageClasses: []postgresflex.FlavorStorageClassesStorageClass{ + { + Class: "premium-perf2-stackit", + MaxIoPerSec: 1000, + MaxThroughInMb: 100, + }, + { + Class: "premium-perf4-stackit", + MaxIoPerSec: 2000, + MaxThroughInMb: 150, + }, + { + Class: "premium-perf6-stackit", + MaxIoPerSec: 5000, + MaxThroughInMb: 200, + }, + }, + } +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, 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) + }) + } +} + +type mockSettings struct { + listFlavorsFails bool + flavors []postgresflex.ListFlavors +} + +func newApiMock(s mockSettings) postgresflex.DefaultAPI { + return postgresflex.DefaultAPIServiceMock{ + ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { + if s.listFlavorsFails { + return nil, fmt.Errorf("mock list flavors fails") + } + + return &postgresflex.ListFlavorsResponse{ + Flavors: s.flavors, + }, nil + }), + } +} + +func TestBuildAndExecuteRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + mockSettings mockSettings + want *postgresflex.ListFlavors + wantErr bool + }{ + { + description: "base", + model: fixtureInputModel(), + mockSettings: mockSettings{ + flavors: []postgresflex.ListFlavors{ + fixtureTestFlavor(), + }, + }, + want: utils.Ptr(fixtureTestFlavor()), + wantErr: false, + }, + { + description: "flavor not found", + model: fixtureInputModel(func(model *inputModel) { + model.FlavorId = "foo-bar-does-not-exist" + }), + mockSettings: mockSettings{ + flavors: []postgresflex.ListFlavors{ + fixtureTestFlavor(), + }, + }, + want: nil, + wantErr: true, + }, + { + description: "list flavors fails", + model: fixtureInputModel(), + mockSettings: mockSettings{ + listFlavorsFails: true, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + actual, err := buildAndExecuteRequest(context.Background(), tt.model, newApiMock(tt.mockSettings)) + if err != nil && !tt.wantErr { + t.Errorf("want no error, got %v", err) + } + + if err == nil && tt.wantErr { + t.Errorf("want error, got nil") + } + + diff := cmp.Diff(actual, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + flavor *postgresflex.ListFlavors + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "flavor is nil", + args: args{ + flavor: nil, + }, + wantErr: true, + }, + { + name: "flavor is empty", + args: args{ + flavor: &postgresflex.ListFlavors{}, + }, + wantErr: false, + }, + { + name: "complete", + args: args{ + flavor: utils.Ptr(fixtureTestFlavor()), + }, + 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.flavor); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/postgresflex/flavor/flavor.go b/internal/cmd/postgresflex/flavor/flavor.go new file mode 100644 index 000000000..78dc84d23 --- /dev/null +++ b/internal/cmd/postgresflex/flavor/flavor.go @@ -0,0 +1,28 @@ +package flavor + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/flavor/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/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 PostgreSQL Flex flavors", + Long: "Provides functionality for PostgreSQL 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)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/postgresflex/flavor/list/list.go b/internal/cmd/postgresflex/flavor/list/list.go new file mode 100644 index 000000000..c60d96284 --- /dev/null +++ b/internal/cmd/postgresflex/flavor/list/list.go @@ -0,0 +1,102 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "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/postgresflex/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 PostgreSQL Flex flavors", + Long: "Lists PostgreSQL Flex flavors.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List PostgreSQL Flex flavor`, + "$ stackit postgresflex 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 PostgreSQL Flex flavors: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, flavors.Flavors) + }, + } + 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 postgresflex.DefaultAPI) postgresflex.ApiListFlavorsRequest { + return apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Size(100) +} + +func outputResult(p *print.Printer, outputFormat string, flavors []postgresflex.ListFlavors) error { + return p.OutputResult(outputFormat, flavors, func() error { + if len(flavors) == 0 { + p.Outputf("No PostgreSQL flavors found.") + return nil + } + + table := tables.NewTable() + table.SetTitle("Flavors") + table.SetHeader("ID", "CPU", "MEMORY", "NODE TYPE", "DESCRIPTION") + for _, f := range flavors { + table.AddRow( + f.Id, + f.Cpu, + f.Memory, + f.NodeType, + f.Description, + ) + table.AddSeparator() + } + + return table.Display(p) + }) +} diff --git a/internal/cmd/postgresflex/flavor/list/list_test.go b/internal/cmd/postgresflex/flavor/list/list_test.go new file mode 100644 index 000000000..c38d82c57 --- /dev/null +++ b/internal/cmd/postgresflex/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" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.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 *postgresflex.ApiListFlavorsRequest)) postgresflex.ApiListFlavorsRequest { + request := testClient.DefaultAPI.ListFlavors(testCtx, testProjectId, testRegion).Size(100) + 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 postgresflex.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, postgresflex.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + flavors []postgresflex.ListFlavors + } + 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: []postgresflex.ListFlavors{}, + }, + wantErr: false, + }, + { + name: "empty flavor in flavors slice", + args: args{ + flavors: []postgresflex.ListFlavors{{}}, + }, + 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/postgresflex/instance/clone/clone.go b/internal/cmd/postgresflex/instance/clone/clone.go index b5bea934c..6579e0a43 100644 --- a/internal/cmd/postgresflex/instance/clone/clone.go +++ b/internal/cmd/postgresflex/instance/clone/clone.go @@ -3,6 +3,7 @@ package clone import ( "context" "fmt" + "time" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -13,13 +14,12 @@ 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/postgresflex/client" - postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" - wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api/wait" ) const ( @@ -35,9 +35,10 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string + InstanceName *string StorageClass *string StorageSize *int64 - RecoveryDate *string + RecoveryDate time.Time } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -49,13 +50,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp.`, - `$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00`), - examples.NewExample( - `Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage class.`, - `$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-class premium-perf6-stackit`), - examples.NewExample( - `Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage size.`, - `$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10`), + `$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10 --storage-class premium-perf6-stackit`), ), Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), RunE: func(cmd *cobra.Command, args []string) error { @@ -72,10 +67,31 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + instance, err := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId + return fmt.Errorf("get PostgreSQL Flex instance: %w", err) + } + + instanceLabel := instance.Name + + if model.StorageSize == nil { + params.Printer.Warn("The --%s flag is not set. Using the storage size from the instance you're cloning. This behavior is deprecated, the --%s flag will be required after 2027-01-31.\n", storageSizeFlag, storageSizeFlag) + + if instance.Storage.Size == nil { + return fmt.Errorf("could not read storage size for instance %s", model.InstanceId) + } + + model.StorageSize = instance.Storage.Size + } + + if model.StorageClass == nil { + params.Printer.Warn("The --%s flag is not set. Using the storage class from the instance you're cloning. This behavior is deprecated, the --%s flag will be required after 2027-01-31.\n", storageClassFlag, storageClassFlag) + + if instance.Storage.Class == nil { + return fmt.Errorf("could not read storage class for instance %s", model.InstanceId) + } + + model.StorageClass = instance.Storage.Class } prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel) @@ -93,12 +109,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("clone PostgreSQL Flex instance: %w", err) } - instanceId := *resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { err := spinner.Run(params.Printer, "Cloning instance", func() error { - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resp.Id).WaitWithContext(ctx) return err }) if err != nil { @@ -106,7 +121,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } } - return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, instanceId, resp) + return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, resp.Id, resp) }, } configureFlags(cmd) @@ -115,8 +130,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(recoveryTimestampFlag, "", "Recovery timestamp for the instance, in a date-time with the layout format YYYY-MM-DDTHH:mm:ss±HH:mm, e.g. 2006-01-02T15:04:05-07:00") - cmd.Flags().String(storageClassFlag, "", "Storage class. If not specified, storage class from the existing instance will be used.") - cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB). If not specified, storage size from the existing instance will be used.") + cmd.Flags().String(storageClassFlag, "", "Storage class. If not specified, storage class from the existing instance will be used. This flag will be required after 2027-01-31.") + cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB). If not specified, storage size from the existing instance will be used. This flag will be required after 2027-01-31.") + + // mark storage-size flag required here after 2027-01-31 err := flags.MarkFlagsRequired(cmd, recoveryTimestampFlag) cobra.CheckErr(err) @@ -136,15 +153,19 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Flag: recoveryTimestampFlag, Details: err.Error(), } + } else if recoveryTimestamp == nil { + return nil, &cliErr.FlagValidationError{ + Flag: recoveryTimestampFlag, + Details: fmt.Sprintf("the --%s flag is required", recoveryTimestampFlag), + } } - recoveryTimestampString := recoveryTimestamp.Format(recoveryDateFormat) model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: instanceId, StorageClass: flags.FlagToStringPointer(p, cmd, storageClassFlag), StorageSize: flags.FlagToInt64Pointer(p, cmd, storageSizeFlag), - RecoveryDate: utils.Ptr(recoveryTimestampString), + RecoveryDate: *recoveryTimestamp, } p.DebugInputModel(model) @@ -152,41 +173,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (postgresflex.ApiCloneInstanceRequest, error) { - req := apiClient.CloneInstance(ctx, model.ProjectId, model.Region, model.InstanceId) - - var storages *postgresflex.ListStoragesResponse - if model.StorageClass != nil || model.StorageSize != nil { - currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) - } - validationFlavorId := currentInstance.Item.Flavor.Id - currentInstanceStorageClass := currentInstance.Item.Storage.Class - currentInstanceStorageSize := currentInstance.Item.Storage.Size - - storages, err = apiClient.ListStorages(ctx, model.ProjectId, model.Region, *validationFlavorId).Execute() - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) - } + if model.StorageSize == nil { + return postgresflex.ApiCloneInstanceRequest{}, fmt.Errorf("storage size is nil") + } - if model.StorageClass == nil { - err = postgresflexUtils.ValidateStorage(currentInstanceStorageClass, model.StorageSize, storages, *validationFlavorId) - } else if model.StorageSize == nil { - err = postgresflexUtils.ValidateStorage(model.StorageClass, currentInstanceStorageSize, storages, *validationFlavorId) - } else { - err = postgresflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *validationFlavorId) - } - if err != nil { - return req, err - } + payload := postgresflex.CloneInstancePayload{ + InstanceOverrides: postgresflex.CloneInstanceOverrides{ + Class: model.StorageClass, + Size: *model.StorageSize, + Name: model.InstanceName, + }, + PointInTime: model.RecoveryDate, } - req = req.CloneInstancePayload(postgresflex.CloneInstancePayload{ - Class: model.StorageClass, - Size: model.StorageSize, - Timestamp: model.RecoveryDate, - }) - return req, nil + return apiClient.CloneInstance(ctx, model.ProjectId, model.Region, model.InstanceId).CloneInstancePayload(payload), nil } func outputResult(p *print.Printer, outputFormat string, async bool, instanceLabel, instanceId string, resp *postgresflex.CloneInstanceResponse) error { @@ -194,11 +194,13 @@ func outputResult(p *print.Printer, outputFormat string, async bool, instanceLab if resp == nil { return fmt.Errorf("response not set") } + operationState := "Cloned" if async { operationState = "Triggered cloning of" } - p.Info("%s instance from instance %q. New Instance ID: %s\n", operationState, instanceLabel, instanceId) + + p.Outputf("%s instance from instance %q. New Instance ID: %s\n", operationState, instanceLabel, instanceId) return nil }) } diff --git a/internal/cmd/postgresflex/instance/clone/clone_test.go b/internal/cmd/postgresflex/instance/clone/clone_test.go index 4e99f08e7..5c8d69e9c 100644 --- a/internal/cmd/postgresflex/instance/clone/clone_test.go +++ b/internal/cmd/postgresflex/instance/clone/clone_test.go @@ -2,14 +2,13 @@ package clone import ( "context" - "fmt" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -19,40 +18,20 @@ import ( type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} -type mockSettings struct { - listStoragesFails bool - listStoragesResp *postgresflex.ListStoragesResponse - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse -} - -func newAPIMockClient(c mockSettings) postgresflex.DefaultAPI { - return &postgresflex.DefaultAPIServiceMock{ - GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") - } - return c.getInstanceResp, nil - }), - ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil - }), - } -} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() -var testRecoveryTimestamp = "2024-03-08T09:28:00+00:00" -var testFlavorId = uuid.NewString() -var testStorageClass = "premium-perf4-stackit" -var testStorageSize = int64(10) -var testRegion = "eu01" +const ( + testStorageSize = int64(10) + testRecoveryTimestamp = "2024-03-08T09:28:00+00:00" + testStorageClass = "premium-perf4-stackit" + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -95,7 +74,6 @@ func fixtureRequiredInputModel(mods ...func(model *inputModel)) *inputModel { if err != nil { return &inputModel{} } - recoveryTimestampString := testRecoveryTimestamp.Format(recoveryDateFormat) model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ @@ -104,7 +82,7 @@ func fixtureRequiredInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - RecoveryDate: utils.Ptr(recoveryTimestampString), + RecoveryDate: testRecoveryTimestamp, } for _, mod := range mods { mod(model) @@ -117,7 +95,6 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { if err != nil { return &inputModel{} } - recoveryTimestampString := testRecoveryTimestamp.Format(recoveryDateFormat) model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ @@ -128,7 +105,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { InstanceId: testInstanceId, StorageClass: utils.Ptr(testStorageClass), StorageSize: utils.Ptr(testStorageSize), - RecoveryDate: utils.Ptr(recoveryTimestampString), + RecoveryDate: testRecoveryTimestamp, } for _, mod := range mods { mod(model) @@ -150,10 +127,12 @@ func fixturePayload(mods ...func(payload *postgresflex.CloneInstancePayload)) po if err != nil { return postgresflex.CloneInstancePayload{} } - recoveryTimestampString := testRecoveryTimestamp.Format(recoveryDateFormat) payload := postgresflex.CloneInstancePayload{ - Timestamp: utils.Ptr(recoveryTimestampString), + InstanceOverrides: postgresflex.CloneInstanceOverrides{ + Size: testStorageSize, + }, + PointInTime: testRecoveryTimestamp, } for _, mod := range mods { mod(&payload) @@ -312,53 +291,46 @@ func TestBuildRequest(t *testing.T) { if err != nil { return } - recoveryTimestampString := testRecoveryTimestamp.Format(recoveryDateFormat) tests := []struct { - description string - model *inputModel - expectedRequest postgresflex.ApiCloneInstanceRequest - mockClientSettings mockSettings - isValid bool + description string + model *inputModel + expectedRequest postgresflex.ApiCloneInstanceRequest + isValid bool }{ { - description: "base", - model: fixtureRequiredInputModel(), + description: "base", + model: fixtureRequiredInputModel( + func(model *inputModel) { + model.StorageSize = utils.Ptr(testStorageSize) + }, + ), isValid: true, expectedRequest: fixtureRequest(), }, { description: "specify storage class only", model: fixtureRequiredInputModel(func(model *inputModel) { + model.StorageSize = utils.Ptr(testStorageSize) model.StorageClass = utils.Ptr("class") }), isValid: true, - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, expectedRequest: testClient.DefaultAPI.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId). CloneInstancePayload(postgresflex.CloneInstancePayload{ - Class: utils.Ptr("class"), - Timestamp: utils.Ptr(recoveryTimestampString), + InstanceOverrides: postgresflex.CloneInstanceOverrides{ + Class: utils.Ptr("class"), + Size: testStorageSize, + }, + PointInTime: testRecoveryTimestamp, }), }, + { + description: "storage size missing", + model: fixtureRequiredInputModel(func(model *inputModel) { + model.StorageSize = nil + }), + isValid: false, + }, { description: "specify storage class and size", model: fixtureRequiredInputModel(func(model *inputModel) { @@ -366,31 +338,13 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(10)) }), isValid: true, - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, expectedRequest: testClient.DefaultAPI.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId). CloneInstancePayload(postgresflex.CloneInstancePayload{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(10)), - Timestamp: utils.Ptr(recoveryTimestampString), + InstanceOverrides: postgresflex.CloneInstanceOverrides{ + Class: utils.Ptr("class"), + Size: int64(10), + }, + PointInTime: testRecoveryTimestamp, }), }, { @@ -398,77 +352,16 @@ func TestBuildRequest(t *testing.T) { model: fixtureRequiredInputModel( func(model *inputModel) { model.StorageClass = utils.Ptr("class") - model.RecoveryDate = utils.Ptr(recoveryTimestampString) + model.RecoveryDate = testRecoveryTimestamp }, ), - mockClientSettings: mockSettings{ - getInstanceFails: true, - }, - isValid: false, - }, - { - description: "invalid storage class", - model: fixtureRequiredInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("non-existing-class") - }, - ), - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.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)) - }, - ), - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request, err := buildRequest(testCtx, tt.model, newAPIMockClient(tt.mockClientSettings)) + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) if err != nil { if !tt.isValid { return @@ -505,7 +398,7 @@ func Test_outputResult(t *testing.T) { {"standard", args{ instanceLabel: "foo", instanceId: "bar", - resp: &postgresflex.CloneInstanceResponse{InstanceId: utils.Ptr("id")}, + resp: &postgresflex.CloneInstanceResponse{Id: "id"}, }, false}, } params := testparams.NewTestParams() diff --git a/internal/cmd/postgresflex/instance/create/create.go b/internal/cmd/postgresflex/instance/create/create.go index 5537cc370..86caa1d3c 100644 --- a/internal/cmd/postgresflex/instance/create/create.go +++ b/internal/cmd/postgresflex/instance/create/create.go @@ -2,10 +2,14 @@ package create import ( "context" - "errors" "fmt" "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api/wait" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -17,11 +21,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" - wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" ) const ( @@ -29,19 +28,27 @@ const ( aclFlag = "acl" backupScheduleFlag = "backup-schedule" flavorIdFlag = "flavor-id" - cpuFlag = "cpu" - ramFlag = "ram" storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" + retentionDaysFlag = "retention-days" + + defaultBackupSchedule = "0 0 * * *" // Deprecated: Will be removed after 2027-01-31. + defaultStorageSize = 10 // Deprecated: Will be removed after 2027-01-31. + defaultStorageClass = "premium-perf2-stackit" // Deprecated: Will be removed after 2027-01-31. + + encryptionKekKeyIdFlag = "encryption-kek-key-id" + encryptionKekKeyringIdFlag = "encryption-kek-keyring-id" + encryptionKekKeyVersionFlag = "encryption-kek-key-version" + encryptionServiceAccountFlag = "encryption-service-account" - defaultBackupSchedule = "0 0 * * *" - defaultStorageClass = "premium-perf2-stackit" - defaultStorageSize = 10 - defaultType = "Replica" + cpuFlag = "cpu" // Deprecated: Will be removed after 2027-01-31. Flavor id should be used instead. + ramFlag = "ram" // Deprecated: Will be removed after 2027-01-31. Flavor id should be used instead. + defaultType = "Replica" // Deprecated: Will be removed after 2027-01-31. Replicas are managed via the flavor id on API side now. ) var ( + // Deprecated: Will be removed after 2027-01-31. Replicas are managed via the flavor id on API side now. typeFlag = flags.StringEnumFlag( "type", postgresflexUtils.AvailableInstanceTypes(), @@ -55,14 +62,21 @@ type inputModel struct { InstanceName string ACL []string - BackupSchedule string + BackupSchedule *string FlavorId *string - CPU *int64 - RAM *int64 StorageClass *string StorageSize *int64 - Version string - Type string + Version *string + RetentionDays *int32 + + EncryptionKekKeyId *string + EncryptionKekKeyringId *string + EncryptionKekKeyVersion *string + EncryptionServiceAccount *string + + CPU *int64 // Deprecated: Will be removed after 2027-01-31 + RAM *int64 // Deprecated: Will be removed after 2027-01-31 + Type string // Deprecated: Will be removed after 2027-01-31 } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -73,14 +87,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a PostgreSQL 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 postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 0.0.0.0/0`), + `Create a PostgreSQL Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access).`, + `$ stackit postgresflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 --storage-size 20 --retention-days 32 --version 17 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-stackit`), examples.NewExample( - `Create a PostgreSQL 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 postgresflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0`), - examples.NewExample( - `Create a PostgreSQL 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 postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 1.2.3.0/24 --storage-size 20`), + `Create a PostgreSQL Flex instance with name "my-instance", allow access to a specific range of IP addresses.`, + `$ stackit postgresflex instance create --name my-instance --flavor-id xxx --acl 1.2.3.0/24 --storage-size 20 --retention-days 32 --version 17 --backup-schedule "6 6 * * *" --storage-size 10 --storage-class premium-perf2-stackit`), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -102,19 +113,53 @@ func NewCmd(params *types.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - prompt := fmt.Sprintf("Are you sure you want to create a PostgreSQL Flex instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) + // load flavor id - remove after 2027-01-31 + 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-01-31.\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) } - // Fill in version, if needed - if model.Version == "" { + // remove after 2027-01-31 + if model.RetentionDays == nil { + params.Printer.Warn("The --%s flag is not set. Using the default value (32). This behavior is deprecated, the --%s flag will be required after 2027-01-31.\n", retentionDaysFlag, retentionDaysFlag) + model.RetentionDays = utils.Ptr(int32(32)) // transform the model.RetentionDays field from "*int32" to "int32" once this is removed + } + + // remove after 2027-01-31 + if model.BackupSchedule == 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-01-31.\n", backupScheduleFlag, defaultBackupSchedule, backupScheduleFlag) + model.BackupSchedule = utils.Ptr(defaultBackupSchedule) // transform the model.BackupSchedule field from "*string" to "string" once this is removed + } + + // Fill in version, if needed - remove after 2027-01-31 + 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-01-31.\n", versionFlag, versionFlag) + version, err := postgresflexUtils.GetLatestPostgreSQLVersion(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) if err != nil { return fmt.Errorf("get latest PostgreSQL version: %w", err) } - model.Version = version + model.Version = utils.Ptr(version) // transform the model.Version field from "*string" to "string" once this is removed + } + + 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-01-31.\n", storageSizeFlag, defaultStorageSize, storageSizeFlag) + model.StorageSize = utils.Ptr(int64(defaultStorageSize)) + } + + 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-01-31.\n", storageClassFlag, defaultStorageClass, storageClassFlag) + model.StorageClass = utils.Ptr(defaultStorageClass) + } + + prompt := fmt.Sprintf("Are you sure you want to create a PostgreSQL Flex instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -126,12 +171,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create PostgreSQL Flex instance: %w", err) } - instanceId := *resp.Id // Wait for async operation, if async mode not enabled if !model.Async { err := spinner.Run(params.Printer, "Creating instance", func() error { - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resp.Id).WaitWithContext(ctx) return err }) if err != nil { @@ -139,7 +183,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } } - return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, instanceId, resp) + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp.Id, resp) }, } configureFlags(cmd) @@ -149,17 +193,38 @@ 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().Int64(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int64(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, "", "PostgreSQL version. Defaults to the latest version available") + cmd.Flags().String(backupScheduleFlag, "", "Backup schedule. This flag will be required after 2027-01-31.") + cmd.Flags().String(flavorIdFlag, "", "ID of the flavor. This flag will be required after 2027-01-31.") + cmd.Flags().String(storageClassFlag, "", "Storage class. This flag will be required after 2027-01-31.") + cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB). This flag will be required after 2027-01-31.") + cmd.Flags().String(versionFlag, "", "PostgreSQL version. Defaults to the latest version available. This flag will be required after 2027-01-31.") + cmd.Flags().Int32(retentionDaysFlag, 0, "The days for how long the backup files should be stored before cleaned up (32 to 90). This flag will be required after 2027-01-31.") + cmd.Flags().String(encryptionKekKeyIdFlag, "", "The key identifier") + cmd.Flags().String(encryptionKekKeyringIdFlag, "", "The keyring identifier") + cmd.Flags().String(encryptionKekKeyVersionFlag, "", "The key version") + cmd.Flags().String(encryptionServiceAccountFlag, "", "The service account") + + // remove after 2027-01-31 + cmd.Flags().Int64(cpuFlag, 0, "Number of CPUs.") + cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB).") typeFlag.Register(cmd.Flags()) + // after 2027-01-31: add backup-schedule,storage-size,version,flavor-id,retention-days err := flags.MarkFlagsRequired(cmd, instanceNameFlag, aclFlag) cobra.CheckErr(err) + + cmd.MarkFlagsRequiredTogether(encryptionKekKeyIdFlag, encryptionKekKeyringIdFlag, encryptionKekKeyVersionFlag, encryptionServiceAccountFlag) + + // remove after 2027-01-31 + err = cmd.Flags().MarkDeprecated("type", fmt.Sprintf("Will be removed after 2027-01-31. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + err = cmd.Flags().MarkDeprecated(cpuFlag, fmt.Sprintf("Will be removed after 2027-01-31. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + err = cmd.Flags().MarkDeprecated(ramFlag, fmt.Sprintf("Will be removed after 2027-01-31. 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) { @@ -168,98 +233,122 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return nil, &cliErr.ProjectIdError{} } - storageSize := flags.FlagWithDefaultToInt64Value(p, cmd, storageSizeFlag) - flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) cpu := flags.FlagToInt64Pointer(p, cmd, cpuFlag) ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) + // remove after 2027-01-31: flavor id flag will be required then if flavorId == nil && (cpu == nil || ram == nil) { return nil, &cliErr.DatabaseInputFlavorError{ Cmd: cmd, } } + // remove after 2027-01-31: flavor id flag will be required then if flavorId != nil && (cpu != nil || ram != nil) { return nil, &cliErr.DatabaseInputFlavorError{ Cmd: cmd, } } + // remove after 2027-01-31: storage size flag will be required then (no pointer anymore) + var storageSize *int64 + if cmd.Flags().Changed(storageSizeFlag) { + storageSize = flags.FlagToInt64Pointer(p, cmd, storageSizeFlag) + } + + // remove after 2027-01-31: retention days flag will be required then (no pointer anymore) + var retentionDays *int32 + if cmd.Flags().Changed(retentionDaysFlag) { + retentionDays = flags.FlagToInt32Pointer(p, cmd, retentionDaysFlag) + } + 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), - Type: typeFlag.Get(), + StorageClass: flags.FlagToStringPointer(p, cmd, storageClassFlag), + StorageSize: storageSize, + Version: flags.FlagToStringPointer(p, cmd, versionFlag), + RetentionDays: retentionDays, + + EncryptionKekKeyId: flags.FlagToStringPointer(p, cmd, encryptionKekKeyIdFlag), + EncryptionKekKeyringId: flags.FlagToStringPointer(p, cmd, encryptionKekKeyringIdFlag), + EncryptionKekKeyVersion: flags.FlagToStringPointer(p, cmd, encryptionKekKeyVersionFlag), + EncryptionServiceAccount: flags.FlagToStringPointer(p, cmd, encryptionServiceAccountFlag), + + // deprecated fields + CPU: cpu, + RAM: ram, + Type: typeFlag.Get(), } p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (postgresflex.ApiCreateInstanceRequest, error) { - req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) +// Deprecated: remove after 2027-01-31 +func getFlavorId(ctx context.Context, model *inputModel, apiClient postgresflex.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 + } - flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() + // Load all flavors + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Size(100).Execute() if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex flavors: %w", err) + return nil, fmt.Errorf("loading flavors: %w", err) } - if model.FlavorId == nil { - foundFlavorId, err := postgresflexUtils.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 := postgresflexUtils.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 && flavor.NodeType == model.Type { + return &flavor.Id, nil } - flavorId = *model.FlavorId } - storages, err := apiClient.ListStorages(ctx, model.ProjectId, model.Region, flavorId).Execute() - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) - } - err = postgresflexUtils.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 postgresflex.DefaultAPI) (postgresflex.ApiCreateInstanceRequest, error) { + req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) + + var encryption *postgresflex.InstanceEncryption + if model.EncryptionKekKeyId != nil && model.EncryptionKekKeyringId != nil && model.EncryptionKekKeyVersion != nil && model.EncryptionServiceAccount != nil { + encryption = &postgresflex.InstanceEncryption{ + KekKeyId: *model.EncryptionKekKeyId, + KekKeyRingId: *model.EncryptionKekKeyringId, + KekKeyVersion: *model.EncryptionKekKeyVersion, + ServiceAccount: *model.EncryptionServiceAccount, + } } - replicas, err := postgresflexUtils.GetInstanceReplicas(model.Type) - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex instance type: %w", err) + // remove after 2027-01-31 + if model.BackupSchedule == nil { + return postgresflex.ApiCreateInstanceRequest{}, fmt.Errorf("backup schedule is nil") + } else if model.StorageSize == nil { + return postgresflex.ApiCreateInstanceRequest{}, fmt.Errorf("storage size is nil") + } else if model.Version == nil { + return postgresflex.ApiCreateInstanceRequest{}, fmt.Errorf("version is nil") } req = req.CreateInstancePayload(postgresflex.CreateInstancePayload{ + BackupSchedule: *model.BackupSchedule, + Encryption: encryption, + FlavorId: utils.PtrString(model.FlavorId), Name: model.InstanceName, - Acl: postgresflex.ACL{Items: model.ACL}, - BackupSchedule: model.BackupSchedule, - FlavorId: flavorId, - Replicas: replicas, - Storage: postgresflex.Storage{ - Class: model.StorageClass, - Size: model.StorageSize, + Network: postgresflex.InstanceNetworkCreate{ + Acl: model.ACL, }, - Version: model.Version, - Options: map[string]string{ - "type": model.Type, + RetentionDays: *postgresflex.NewNullableInt32(model.RetentionDays), + Storage: postgresflex.StorageCreate{ + Class: model.StorageClass, + Size: *model.StorageSize, }, + Version: *model.Version, }) return req, nil } diff --git a/internal/cmd/postgresflex/instance/create/create_test.go b/internal/cmd/postgresflex/instance/create/create_test.go index d0f86d364..d016005f2 100644 --- a/internal/cmd/postgresflex/instance/create/create_test.go +++ b/internal/cmd/postgresflex/instance/create/create_test.go @@ -2,13 +2,12 @@ package create import ( "context" - "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -20,35 +19,11 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} -var testRegion = "eu01" - -type mockSettings struct { - listFlavorsFails bool - listFlavorsResp *postgresflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *postgresflex.ListStoragesResponse -} - -func newAPIClientMock(c mockSettings) postgresflex.DefaultAPI { - return postgresflex.DefaultAPIServiceMock{ - ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil - }), - ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") - } - return c.listFlavorsResp, nil - }), - } -} - var testProjectId = uuid.NewString() var testFlavorId = uuid.NewString() +const testRegion = "eu01" + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, @@ -77,11 +52,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { }, InstanceName: "example-name", ACL: []string{"0.0.0.0/0"}, - BackupSchedule: "0 0 * * *", + BackupSchedule: utils.Ptr("0 0 * * *"), FlavorId: utils.Ptr(testFlavorId), StorageClass: utils.Ptr("premium-perf4-stackit"), StorageSize: utils.Ptr(int64(10)), - Version: "6.0", + Version: utils.Ptr("6.0"), Type: "Replica", } for _, mod := range mods { @@ -101,19 +76,18 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiCreateInstanceRequest) func fixturePayload(mods ...func(payload *postgresflex.CreateInstancePayload)) postgresflex.CreateInstancePayload { payload := postgresflex.CreateInstancePayload{ - Name: "example-name", - Acl: postgresflex.ACL{Items: []string{"0.0.0.0/0"}}, BackupSchedule: "0 0 * * *", FlavorId: testFlavorId, - Replicas: int32(3), - Storage: postgresflex.Storage{ - Class: utils.Ptr("premium-perf4-stackit"), - Size: utils.Ptr(int64(10)), + Name: "example-name", + Network: postgresflex.InstanceNetworkCreate{ + Acl: []string{"0.0.0.0/0"}, }, - Version: "6.0", - Options: map[string]string{ - "type": "Replica", + Storage: postgresflex.StorageCreate{ + Class: utils.Ptr("premium-perf4-stackit"), + Size: int64(10), }, + Version: "6.0", + RetentionDays: *postgresflex.NewNullableInt32(nil), } for _, mod := range mods { mod(&payload) @@ -142,8 +116,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", @@ -214,7 +190,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Version = "" + model.Version = nil }), }, { @@ -258,212 +234,22 @@ func TestParseInput(t *testing.T) { func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest postgresflex.ApiCreateInstanceRequest - mockClientSettings mockSettings - isValid bool + description string + model *inputModel + expectedRequest postgresflex.ApiCreateInstanceRequest + isValid bool }{ { description: "base with flavor ID", model: fixtureInputModel(), isValid: true, expectedRequest: fixtureRequest(), - mockClientSettings: mockSettings{ - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - }, - { - description: "with CPU and RAM", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) - }, - ), - isValid: true, - expectedRequest: fixtureRequest(), - mockClientSettings: mockSettings{ - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - }, - { - description: "single instance type", - model: fixtureInputModel(func(model *inputModel) { model.Type = "Single" }), - isValid: true, - expectedRequest: fixtureRequest().CreateInstancePayload(fixturePayload(func(payload *postgresflex.CreateInstancePayload) { - payload.Options = map[string]string{"type": "Single"} - payload.Replicas = int32(1) - })), - mockClientSettings: mockSettings{ - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - }, - { - description: "get flavors fails", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsFails: true, - }, - isValid: false, - }, - { - description: "flavor id not found", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(5)) - model.RAM = utils.Ptr(int64(9)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), - }, - }, - }, - }, - isValid: false, - }, - { - description: "get storages fails", - model: fixtureInputModel( - func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsFails: true, - }, - isValid: false, - }, - { - description: "invalid storage class", - model: fixtureInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("non-existing-class") - }, - ), - mockClientSettings: mockSettings{ - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - isValid: false, - }, - { - description: "invalid storage size", - model: fixtureInputModel( - func(model *inputModel) { - model.StorageSize = utils.Ptr(int64(9)) - }, - ), - mockClientSettings: mockSettings{ - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - 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 @@ -473,8 +259,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, postgresflex.DefaultAPIService{}, postgresflex.NullableInt32{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -496,15 +281,23 @@ func Test_outputResult(t *testing.T) { args args wantErr bool }{ - {"empty", args{}, true}, - {"standard", args{ - outputFormat: "", - async: false, - projectLabel: "label", - instanceId: "4711", - resp: &postgresflex.CreateInstanceResponse{Id: utils.Ptr("id")}, + { + name: "empty", + args: args{}, + wantErr: true, }, - false, + { + name: "standard", + args: args{ + outputFormat: "", + async: false, + projectLabel: "label", + instanceId: "4711", + resp: &postgresflex.CreateInstanceResponse{ + Id: "id", + }, + }, + wantErr: false, }, } params := testparams.NewTestParams() diff --git a/internal/cmd/postgresflex/instance/delete/delete.go b/internal/cmd/postgresflex/instance/delete/delete.go index c26787a42..8889e78f2 100644 --- a/internal/cmd/postgresflex/instance/delete/delete.go +++ b/internal/cmd/postgresflex/instance/delete/delete.go @@ -18,14 +18,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" - wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api/wait" ) const ( instanceIdArg = "INSTANCE_ID" - forceDeleteFlag = "force" + forceDeleteFlag = "force" // Deprecated: Will be removed after 2026-01-31 ) type inputModel struct { @@ -77,62 +77,30 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - toDelete, toForceDelete, err := getNextOperations(ctx, model, apiClient.DefaultAPI) + // Call API + delReq := buildDeleteRequest(ctx, model, apiClient) + err = delReq.Execute() if err != nil { - return err - } - - if toDelete { - // Call API - delReq := buildDeleteRequest(ctx, model, apiClient) - err = delReq.Execute() - if err != nil { - return fmt.Errorf("delete PostgreSQL Flex instance: %w", err) - } - - // Wait for async operation, if async mode not enabled - if !model.Async { - err := spinner.Run(params.Printer, "Deleting instance", func() error { - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) - return err - }) - if err != nil { - return fmt.Errorf("wait for PostgreSQL Flex instance deletion: %w", err) - } - } + return fmt.Errorf("delete PostgreSQL Flex instance: %w", err) } - if toForceDelete { - // Call API - forceDelReq := buildForceDeleteRequest(ctx, model, apiClient) - err = forceDelReq.Execute() + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { - return fmt.Errorf("force delete PostgreSQL Flex instance: %w", err) - } - - // Wait for async operation, if async mode not enabled - if !model.Async { - err := spinner.Run(params.Printer, "Forcing deletion of instance", func() error { - _, err = wait.ForceDeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) - return err - }) - if err != nil { - return fmt.Errorf("wait for PostgreSQL Flex instance force deletion: %w", err) - } + return fmt.Errorf("wait for PostgreSQL Flex instance deletion: %w", err) } } operationState := "Deleted" - if toForceDelete { - operationState = "Forcefully deleted" - } if model.Async { operationState = "Triggered deletion of" - if toForceDelete { - operationState = "Triggered forced deletion of" - } } - params.Printer.Info("%s instance %q\n", operationState, instanceLabel) + + params.Printer.Outputf("%s instance %q\n", operationState, instanceLabel) return nil }, } @@ -142,6 +110,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().BoolP(forceDeleteFlag, "f", false, "Force deletion of a delayed deleted instance") + + err := cmd.Flags().MarkDeprecated(forceDeleteFlag, "Force delete is the default option by now. This flag will be removed after 2027-01-31.") + cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -166,25 +137,3 @@ func buildDeleteRequest(ctx context.Context, model *inputModel, apiClient *postg req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } - -func buildForceDeleteRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiForceDeleteInstanceRequest { - req := apiClient.DefaultAPI.ForceDeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) - return req -} - -func getNextOperations(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (toDelete, toForceDelete bool, err error) { - instanceStatus, err := postgresflexUtils.GetInstanceStatus(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) - if err != nil { - return false, false, fmt.Errorf("get PostgreSQL Flex instance status: %w", err) - } - - if instanceStatus == wait.InstanceStateDeleted { - if !model.ForceDelete { - return false, false, fmt.Errorf("instance is already deleted, use --force to force the deletion of a delayed deleted instance") - } - - return false, model.ForceDelete, nil - } - - return true, model.ForceDelete, nil -} diff --git a/internal/cmd/postgresflex/instance/delete/delete_test.go b/internal/cmd/postgresflex/instance/delete/delete_test.go index ffdd8c3b9..09a32573a 100644 --- a/internal/cmd/postgresflex/instance/delete/delete_test.go +++ b/internal/cmd/postgresflex/instance/delete/delete_test.go @@ -2,18 +2,15 @@ package delete import ( "context" - "fmt" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "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" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" - wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) type testCtxKey struct{} @@ -22,23 +19,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" -type mockSettings struct { - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse -} - -func newAPIMockClient(c mockSettings) postgresflex.DefaultAPI { - return postgresflex.DefaultAPIServiceMock{ - GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") - } - return c.getInstanceResp, nil - }), - } -} +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -76,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureDeleteRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRequest)) postgresflex.ApiDeleteInstanceRequest { +func fixtureRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRequest)) postgresflex.ApiDeleteInstanceRequest { request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) @@ -84,14 +66,6 @@ func fixtureDeleteRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRe return request } -func fixtureForceDeleteRequest(mods ...func(request *postgresflex.ApiForceDeleteInstanceRequest)) postgresflex.ApiForceDeleteInstanceRequest { - request := testClient.DefaultAPI.ForceDeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) - for _, mod := range mods { - mod(&request) - } - return request -} - func TestParseInput(t *testing.T) { tests := []struct { description string @@ -170,7 +144,7 @@ func TestParseInput(t *testing.T) { } } -func TestBuildDeleteRequest(t *testing.T) { +func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel @@ -179,7 +153,7 @@ func TestBuildDeleteRequest(t *testing.T) { { description: "base", model: fixtureInputModel(), - expectedRequest: fixtureDeleteRequest(), + expectedRequest: fixtureRequest(), }, } @@ -197,129 +171,3 @@ func TestBuildDeleteRequest(t *testing.T) { }) } } - -func TestBuildForceDeleteRequest(t *testing.T) { - tests := []struct { - description string - model *inputModel - expectedRequest postgresflex.ApiForceDeleteInstanceRequest - }{ - { - description: "base", - model: fixtureInputModel(), - expectedRequest: fixtureForceDeleteRequest(), - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - request := buildForceDeleteRequest(testCtx, tt.model, testClient) - - diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), - ) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func TestCheckIfInstanceIsDeleted(t *testing.T) { - tests := []struct { - description string - model *inputModel - expectedToDelete bool - expectedToForceDelete bool - mockClientSettings mockSettings - isValid bool - }{ - { - description: "delete instance state Ready", - model: fixtureInputModel(), - expectedToDelete: true, - expectedToForceDelete: false, - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateSuccess), - }, - }, - }, - isValid: true, - }, - { - description: "force delete instance state Ready", - model: fixtureInputModel(func(model *inputModel) { - model.ForceDelete = true - }), - expectedToDelete: true, - expectedToForceDelete: true, - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateSuccess), - }, - }, - }, - isValid: true, - }, - { - description: "force delete instance state Deleted", - model: fixtureInputModel(func(model *inputModel) { - model.ForceDelete = true - }), - expectedToDelete: false, - expectedToForceDelete: true, - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateDeleted), - }, - }, - }, - isValid: true, - }, - { - description: "delete instance state Deleted", - model: fixtureInputModel(), - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateDeleted), - }, - }, - }, - isValid: false, - }, - { - description: "delete instance get instance fails", - model: fixtureInputModel(), - mockClientSettings: mockSettings{ - getInstanceFails: true, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - toDelete, toForceDelete, err := getNextOperations(testCtx, tt.model, newAPIMockClient(tt.mockClientSettings)) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error checking if instance is deleted: %v", err) - } - - if toDelete != tt.expectedToDelete { - t.Fatalf("toDelete does not match: got %v, expected %v", toDelete, tt.expectedToDelete) - } - - if toForceDelete != tt.expectedToForceDelete { - t.Fatalf("toForceDelete does not match: got %v, expected %v", toForceDelete, tt.expectedToForceDelete) - } - }) - } -} diff --git a/internal/cmd/postgresflex/instance/describe/describe.go b/internal/cmd/postgresflex/instance/describe/describe.go index 8a3b277c4..e0743a338 100644 --- a/internal/cmd/postgresflex/instance/describe/describe.go +++ b/internal/cmd/postgresflex/instance/describe/describe.go @@ -6,21 +6,17 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" - postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) const ( @@ -65,7 +61,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("read PostgreSQL Flex instance: %w", err) } - return outputResult(params.Printer, model.OutputFormat, resp.Item) + return outputResult(params.Printer, model.OutputFormat, resp) }, } return cmd @@ -93,54 +89,31 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresfle return req } -func outputResult(p *print.Printer, outputFormat string, instance *postgresflex.Instance) error { +func outputResult(p *print.Printer, outputFormat string, instance *postgresflex.GetInstanceResponse) error { return p.OutputResult(outputFormat, instance, func() error { if instance == nil { return fmt.Errorf("no response passed") } - acls := "" - if instance.HasAcl() && instance.Acl.HasItems() { - acls = utils.JoinStringPtr(&instance.Acl.Items, ",") - } - - instanceType, err := postgresflexUtils.GetInstanceType(utils.PtrValue(instance.Replicas)) - if err != nil { - // Should never happen - instanceType = "" - } table := tables.NewTable() - table.AddRow("ID", utils.PtrString(instance.Id)) + table.AddRow("ID", instance.Id) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - table.AddRow("STATUS", cases.Title(language.English).String(utils.PtrString(instance.Status))) + table.AddRow("STATUS", instance.State) table.AddSeparator() - if instance.Storage != nil { - table.AddRow("STORAGE SIZE (GB)", utils.PtrString(instance.Storage.Size)) - } - table.AddSeparator() - table.AddRow("VERSION", utils.PtrString(instance.Version)) + table.AddRow("STORAGE SIZE (GB)", utils.PtrString(instance.Storage.Size)) table.AddSeparator() - table.AddRow("ACL", acls) + table.AddRow("VERSION", instance.Version) table.AddSeparator() - if instance.Flavor != nil { - table.AddRow("FLAVOR DESCRIPTION", utils.PtrString(instance.Flavor.Description)) - } + table.AddRow("ACL", utils.JoinStringPtr(&instance.Network.Acl, ",")) table.AddSeparator() - table.AddRow("TYPE", instanceType) + table.AddRow("FLAVOR ID", instance.FlavorId) table.AddSeparator() - table.AddRow("REPLICAS", utils.PtrString(instance.Replicas)) - table.AddSeparator() - if instance.Flavor != nil { - 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.AddRow("BACKUP SCHEDULE (UTC)", instance.BackupSchedule) table.AddSeparator() - err = table.Display(p) + err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } diff --git a/internal/cmd/postgresflex/instance/describe/describe_test.go b/internal/cmd/postgresflex/instance/describe/describe_test.go index 487343136..bdaba1fcf 100644 --- a/internal/cmd/postgresflex/instance/describe/describe_test.go +++ b/internal/cmd/postgresflex/instance/describe/describe_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -175,43 +175,47 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string - instance *postgresflex.Instance + instance *postgresflex.GetInstanceResponse } tests := []struct { name string args args wantErr bool }{ - {"empty", args{}, true}, - {"standard", args{ - outputFormat: "", - instance: &postgresflex.Instance{}, - }, false}, - {"complete", args{ - outputFormat: "", - instance: &postgresflex.Instance{ - Acl: &postgresflex.ACL{ - Items: []string{}, - }, - BackupSchedule: new(string), - Flavor: &postgresflex.Flavor{ - Cpu: new(int64), - Description: new(string), - Id: new(string), - Memory: new(int64), - }, - Id: new(string), - Name: new(string), - Options: &map[string]string{}, - Replicas: new(int32), - Status: new(string), - Storage: &postgresflex.Storage{ - Class: new(string), - Size: new(int64), - }, - Version: new(string), + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "standard", + args: args{ + outputFormat: "", + instance: &postgresflex.GetInstanceResponse{}, }, - }, false}, + wantErr: false, + }, + { + name: "complete", + args: args{ + outputFormat: "", + instance: &postgresflex.GetInstanceResponse{ + BackupSchedule: "* * * 1 0", + FlavorId: "PostgreSQL-Flex-16.64-Single-EU01", + Id: uuid.NewString(), + Name: "example-instance-01", + Network: postgresflex.InstanceNetwork{ + Acl: []string{}, + }, + State: postgresflex.STATE_READY, + Storage: postgresflex.Storage{ + Class: new(string), + Size: new(int64), + }, + Version: "9", + }, + }, wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/instance/list/list.go b/internal/cmd/postgresflex/instance/list/list.go index ac3eac5a7..45398b1b0 100644 --- a/internal/cmd/postgresflex/instance/list/list.go +++ b/internal/cmd/postgresflex/instance/list/list.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -20,7 +20,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( @@ -69,7 +68,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("get PostgreSQL Flex instances: %w", err) } - instances := resp.Items + instances := resp.Instances // Truncate output if model.Limit != nil && len(instances) > int(*model.Limit) { @@ -121,21 +120,22 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresfle return req } -func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []postgresflex.InstanceListInstance) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []postgresflex.ListInstance) error { return p.OutputResult(outputFormat, instances, func() error { if len(instances) == 0 { p.Outputf("No instances found for project %q\n", projectLabel) return nil } + caser := cases.Title(language.English) table := tables.NewTable() table.SetHeader("ID", "NAME", "STATUS") for i := range instances { instance := instances[i] table.AddRow( - utils.PtrString(instance.Id), - utils.PtrString(instance.Name), - caser.String(utils.PtrString(instance.Status)), + instance.Id, + instance.Name, + caser.String(string(instance.State)), ) } err := table.Display(p) diff --git a/internal/cmd/postgresflex/instance/list/list_test.go b/internal/cmd/postgresflex/instance/list/list_test.go index 9e08559f7..0684f7722 100644 --- a/internal/cmd/postgresflex/instance/list/list_test.go +++ b/internal/cmd/postgresflex/instance/list/list_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -152,32 +152,52 @@ func Test_outputResult(t *testing.T) { type args struct { outputFormat string projectLabel string - instances []postgresflex.InstanceListInstance + instances []postgresflex.ListInstance } tests := []struct { name string args args wantErr bool }{ - {"empty", args{}, false}, - {"standard", args{"", "label", []postgresflex.InstanceListInstance{}}, false}, - {"complete", args{"", "label", []postgresflex.InstanceListInstance{ - { - Id: new(string), - Name: new(string), - Status: new(string), - }, - { - Id: new(string), - Name: new(string), - Status: new(string), + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "standard", + args: args{ + outputFormat: "", + projectLabel: "label", + instances: []postgresflex.ListInstance{}, }, - { - Id: new(string), - Name: new(string), - Status: new(string), + wantErr: false, + }, + { + name: "complete", + args: args{ + outputFormat: "", + projectLabel: "label", + instances: []postgresflex.ListInstance{ + { + Id: uuid.NewString(), + Name: "instance-01", + State: postgresflex.STATE_READY, + }, + { + Id: uuid.NewString(), + Name: "instance-02", + State: postgresflex.STATE_PROGRESSING, + }, + { + Id: uuid.NewString(), + Name: "instance-03", + State: postgresflex.STATE_TERMINATING, + }, + }, }, - }}, false}, + wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/instance/update/update.go b/internal/cmd/postgresflex/instance/update/update.go index aaeedd410..4846b79fe 100644 --- a/internal/cmd/postgresflex/instance/update/update.go +++ b/internal/cmd/postgresflex/instance/update/update.go @@ -19,8 +19,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" - wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api/wait" ) const ( @@ -30,13 +30,18 @@ const ( aclFlag = "acl" backupScheduleFlag = "backup-schedule" flavorIdFlag = "flavor-id" - cpuFlag = "cpu" - ramFlag = "ram" - storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" + retentionDaysFlag = "retention-days" + + // Deprecated: Will be removed after 2027-01-31. Storage class can not be updated. + storageClassFlag = "storage-class" + + cpuFlag = "cpu" // Deprecated: Will be removed after 2027-01-31. Flavor id should be used instead. + ramFlag = "ram" // Deprecated: Will be removed after 2027-01-31. Flavor id should be used instead. ) +// Deprecated: Will be removed after 2027-01-31. Replicas are managed via the flavor id on API side now. var typeFlag = flags.StringEnumFlag( "type", postgresflexUtils.AvailableInstanceTypes(), @@ -51,12 +56,13 @@ type inputModel struct { ACL []string BackupSchedule *string FlavorId *string - CPU *int64 - RAM *int64 - StorageClass *string StorageSize *int64 Version *string - Type *string + RetentionDays *int32 + + CPU *int64 // Deprecated: Will be removed after 2027-01-31 + RAM *int64 // Deprecated: Will be removed after 2027-01-31 + Type *string // Deprecated: Will be removed after 2027-01-31 } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -104,16 +110,17 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return err } - resp, err := req.Execute() + err = req.Execute() if err != nil { return fmt.Errorf("update PostgreSQL Flex instance: %w", err) } - instanceId := *resp.Item.Id + + var waitResp *postgresflex.GetInstanceResponse // Wait for async operation, if async mode not enabled if !model.Async { err := spinner.Run(params.Printer, "Updating instance", func() error { - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + waitResp, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) return err }) if err != nil { @@ -121,7 +128,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } } - return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, resp) + return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, waitResp) }, } configureFlags(cmd) @@ -133,12 +140,28 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "List 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().Int64(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB)") - cmd.Flags().String(storageClassFlag, "", "Storage class") + cmd.Flags().Int64(cpuFlag, 0, "Number of CPUs") // remove after 2027-01-31 + cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB)") // remove after 2027-01-31 + cmd.Flags().String(storageClassFlag, "", "Storage class.") // remove after 2027-01-31 cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB)") cmd.Flags().String(versionFlag, "", "Version") + cmd.Flags().String(retentionDaysFlag, "", "The days for how long the backup files should be stored before cleaned up (32 to 90).") typeFlag.Register(cmd.Flags()) + + // remove after 2027-01-31 + err := cmd.Flags().MarkDeprecated(storageClassFlag, "This flag has no effect and will be removed after 2027-01-31.") + cobra.CheckErr(err) + + // remove after 2027-01-31 + err = cmd.Flags().MarkDeprecated("type", fmt.Sprintf("Will be removed after 2027-01-31. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + err = cmd.Flags().MarkDeprecated(cpuFlag, fmt.Sprintf("Will be removed after 2027-01-31. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + err = cmd.Flags().MarkDeprecated(ramFlag, fmt.Sprintf("Will be removed after 2027-01-31. Use the --%s flag instead.", flavorIdFlag)) + cobra.CheckErr(err) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, cpuFlag) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, ramFlag) + cmd.MarkFlagsMutuallyExclusive(flavorIdFlag, "type") } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -155,13 +178,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) acl := flags.FlagToStringSliceValue(p, cmd, aclFlag) backupSchedule := flags.FlagToStringPointer(p, cmd, backupScheduleFlag) - storageClass := flags.FlagToStringPointer(p, cmd, storageClassFlag) storageSize := flags.FlagToInt64Pointer(p, cmd, storageSizeFlag) version := flags.FlagToStringPointer(p, cmd, versionFlag) instanceType := typeFlag.Ptr() + retentionDays := flags.FlagToInt32Pointer(p, cmd, retentionDaysFlag) if instanceName == nil && flavorId == nil && cpu == nil && ram == nil && acl == nil && - backupSchedule == nil && storageClass == nil && storageSize == nil && version == nil && instanceType == nil { + backupSchedule == nil && storageSize == nil && version == nil && instanceType == nil && retentionDays == nil { return nil, &cliErr.EmptyUpdateError{} } @@ -179,12 +202,14 @@ 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, + RetentionDays: retentionDays, + + // deprecated fields + Type: instanceType, + CPU: cpu, + RAM: ram, } p.DebugInputModel(model) @@ -192,31 +217,47 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (postgresflex.ApiPartialUpdateInstanceRequest, error) { - req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.Region, model.InstanceId) - var flavorId *string var err error + req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.Region, model.InstanceId) + + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() + if err != nil { + return req, fmt.Errorf("failed to get instance %s: %w", model.InstanceId, err) + } + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL 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.Region, model.InstanceId).Execute() - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) + var currentFlavor *postgresflex.ListFlavors + for _, f := range flavors.Flavors { + if f.Id == currentInstance.FlavorId { + currentFlavor = &f + } } + + if currentFlavor == nil { + return req, fmt.Errorf("flavor %s not found", currentInstance.FlavorId) + } + if model.RAM == nil { - ram = currentInstance.Item.Flavor.Memory + ram = utils.Ptr(currentFlavor.Memory) } if model.CPU == nil { - cpu = currentInstance.Item.Flavor.Cpu + cpu = utils.Ptr(currentFlavor.Cpu) } } + flavorId, err = postgresflexUtils.LoadFlavorId(*cpu, *ram, flavors.Flavors) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError @@ -226,84 +267,48 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex return req, err } } else if model.FlavorId != nil { - err := postgresflexUtils.ValidateFlavorId(*model.FlavorId, flavors.Flavors) - if err != nil { - return req, err - } flavorId = model.FlavorId } - var storages *postgresflex.ListStoragesResponse - if model.StorageClass != nil || model.StorageSize != nil { - validationFlavorId := flavorId - if validationFlavorId == nil { - currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) - } - validationFlavorId = currentInstance.Item.Flavor.Id - } - storages, err = apiClient.ListStorages(ctx, model.ProjectId, model.Region, *validationFlavorId).Execute() - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) - } - err = postgresflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *validationFlavorId) - if err != nil { - return req, err - } - } - - var payloadAcl *postgresflex.ACL + var payloadNetwork *postgresflex.InstanceNetworkOpt if model.ACL != nil { - payloadAcl = &postgresflex.ACL{Items: model.ACL} + payloadNetwork = &postgresflex.InstanceNetworkOpt{ + Acl: model.ACL, + } } var payloadStorage *postgresflex.StorageUpdate - if model.StorageClass != nil || model.StorageSize != nil { + if model.StorageSize != nil { payloadStorage = &postgresflex.StorageUpdate{ - Class: model.StorageClass, - Size: model.StorageSize, - } - } - - var replicas *int32 - var payloadOptions *map[string]string - if model.Type != nil { - replicasInt, err := postgresflexUtils.GetInstanceReplicas(*model.Type) - if err != nil { - return req, fmt.Errorf("get PostgreSQL Flex instance type: %w", err) + Size: model.StorageSize, } - - replicas = &replicasInt - payloadOptions = utils.Ptr(map[string]string{ - "type": *model.Type, - }) } - req = req.PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ - Name: model.InstanceName, - Acl: payloadAcl, + payload := postgresflex.PartialUpdateInstancePayload{ BackupSchedule: model.BackupSchedule, FlavorId: flavorId, - Replicas: replicas, + Name: model.InstanceName, + Network: payloadNetwork, + RetentionDays: model.RetentionDays, Storage: payloadStorage, Version: model.Version, - Options: payloadOptions, - }) - return req, nil -} - -func outputResult(p *print.Printer, outputFormat string, async bool, instanceLabel string, resp *postgresflex.PartialUpdateInstanceResponse) error { - if resp == nil { - return fmt.Errorf("no response passed") } + return req.PartialUpdateInstancePayload(payload), nil +} + +func outputResult(p *print.Printer, outputFormat string, async bool, instanceLabel string, resp *postgresflex.GetInstanceResponse) error { return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("no response passed") + } + operationState := "Updated" if async { operationState = "Triggered update of" } - p.Info("%s instance %q\n", operationState, instanceLabel) + + p.Outputf("%s instance %q\n", operationState, instanceLabel) return nil }) } diff --git a/internal/cmd/postgresflex/instance/update/update_test.go b/internal/cmd/postgresflex/instance/update/update_test.go index 1cb0b5aff..2f676f707 100644 --- a/internal/cmd/postgresflex/instance/update/update_test.go +++ b/internal/cmd/postgresflex/instance/update/update_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -25,25 +25,18 @@ var testRegion = "eu01" type mockSettings struct { listFlavorsFails bool listFlavorsResp *postgresflex.ListFlavorsResponse - listStoragesResp *postgresflex.ListStoragesResponse getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse + getInstanceResp *postgresflex.GetInstanceResponse } func newAPIClientMock(c mockSettings) postgresflex.DefaultAPI { return postgresflex.DefaultAPIServiceMock{ - GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { + GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.GetInstanceResponse, error) { if c.getInstanceFails { return nil, fmt.Errorf("get instance failed") } return c.getInstanceResp, nil }), - ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil - }), ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { if c.listFlavorsFails { return nil, fmt.Errorf("list flavors failed") @@ -124,7 +117,6 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { InstanceName: utils.Ptr("example-name"), ACL: []string{"0.0.0.0/0"}, BackupSchedule: utils.Ptr("0 0 * * *"), - StorageClass: utils.Ptr("class"), StorageSize: utils.Ptr(int64(10)), Version: utils.Ptr("5.0"), Type: utils.Ptr("Single"), @@ -365,11 +357,11 @@ func TestBuildRequest(t *testing.T) { isValid: true, mockClientSettings: mockSettings{ listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ + Flavors: []postgresflex.ListFlavors{ { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Id: testFlavorId, + Cpu: int64(2), + Memory: int64(4), }, }, }, @@ -388,11 +380,11 @@ func TestBuildRequest(t *testing.T) { isValid: true, mockClientSettings: mockSettings{ listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ + Flavors: []postgresflex.ListFlavors{ { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Id: testFlavorId, + Cpu: int64(2), + Memory: int64(4), }, }, }, @@ -403,62 +395,20 @@ func TestBuildRequest(t *testing.T) { }), }, { - description: "update storage class only", - model: fixtureRequiredInputModel(func(model *inputModel) { - model.StorageClass = utils.Ptr("class") - }), - isValid: true, - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). - PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ - Storage: &postgresflex.StorageUpdate{ - Class: utils.Ptr("class"), - }, - }), - }, - { - description: "update storage class and size", + description: "update size", model: fixtureRequiredInputModel(func(model *inputModel) { - model.StorageClass = utils.Ptr("class") model.StorageSize = utils.Ptr(int64(10)) }), isValid: true, mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, + getInstanceResp: &postgresflex.GetInstanceResponse{ + FlavorId: testFlavorId, }, }, expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ Storage: &postgresflex.StorageUpdate{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(10)), + Size: utils.Ptr(int64(10)), }, }), }, @@ -485,16 +435,16 @@ func TestBuildRequest(t *testing.T) { ), mockClientSettings: mockSettings{ listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{ + Flavors: []postgresflex.ListFlavors{ { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Id: testFlavorId, + Cpu: int64(2), + Memory: int64(4), }, { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + Id: "other-flavor", + Cpu: int64(1), + Memory: int64(8), }, }, }, @@ -503,11 +453,7 @@ func TestBuildRequest(t *testing.T) { }, { description: "get instance fails", - model: fixtureRequiredInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("class") - }, - ), + model: fixtureRequiredInputModel(), mockClientSettings: mockSettings{ getInstanceFails: true, }, @@ -527,56 +473,6 @@ func TestBuildRequest(t *testing.T) { }, isValid: false, }, - { - description: "invalid storage class", - model: fixtureRequiredInputModel( - func(model *inputModel) { - model.StorageClass = utils.Ptr("non-existing-class") - }, - ), - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.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)) - }, - ), - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }, - }, - isValid: false, - }, } for _, tt := range tests { @@ -605,27 +501,34 @@ func Test_outputResult(t *testing.T) { type args struct { outputFormat string instanceLabel string - resp *postgresflex.PartialUpdateInstanceResponse + resp *postgresflex.GetInstanceResponse } tests := []struct { name string args args wantErr bool }{ - {"empty model", args{}, true}, - {"empty response", args{outputFormat: ""}, true}, - {"standard", args{ - outputFormat: "", - instanceLabel: "test", - resp: &postgresflex.PartialUpdateInstanceResponse{}, - }, false}, - {"complet", args{ - outputFormat: "", - instanceLabel: "test", - resp: &postgresflex.PartialUpdateInstanceResponse{ - Item: &postgresflex.Instance{}, + { + name: "empty model", + args: args{}, + wantErr: true, + }, + { + name: "empty response", + args: args{ + outputFormat: "", }, - }, false}, + wantErr: true, + }, + { + name: "standard", + args: args{ + outputFormat: "", + instanceLabel: "test", + resp: &postgresflex.GetInstanceResponse{}, + }, + wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/options/options.go b/internal/cmd/postgresflex/options/options.go index c2e6e5b08..b376780ab 100644 --- a/internal/cmd/postgresflex/options/options.go +++ b/internal/cmd/postgresflex/options/options.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -17,16 +17,20 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( - flavorsFlag = "flavors" + // Deprecated: Will be removed after 2027-01-31. + flavorsFlag = "flavors" + // Deprecated: Will be removed after 2027-01-31. versionsFlag = "versions" + // Deprecated: Will be removed after 2027-01-31. storagesFlag = "storages" + // Deprecated: Will be removed after 2027-01-31. flavorIdFlag = "flavor-id" ) +// Deprecated: Will be removed after 2027-01-31. type inputModel struct { *globalflags.GlobalFlagModel @@ -36,17 +40,20 @@ type inputModel struct { FlavorId *string } +// Deprecated: Will be removed after 2027-01-31. type options struct { - Flavors []postgresflex.Flavor `json:"flavors,omitempty"` - Versions []string `json:"versions,omitempty"` - Storages *flavorStorages `json:"flavorStorages,omitempty"` + Flavors []postgresflex.ListFlavors `json:"flavors,omitempty"` + Versions []postgresflex.Version `json:"versions,omitempty"` + Storages *flavorStorages `json:"flavorStorages,omitempty"` } +// Deprecated: Will be removed after 2027-01-31. type flavorStorages struct { - FlavorId string `json:"flavorId"` - Storages *postgresflex.ListStoragesResponse `json:"storages"` + FlavorId string `json:"flavorId"` + Storages []postgresflex.FlavorStorageClassesStorageClass `json:"storages"` } +// Deprecated: Will be removed after 2027-01-31. func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", @@ -64,6 +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.`, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() model, err := parseInput(params.Printer, cmd, args) @@ -78,25 +86,27 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Call API - err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient.DefaultAPI) + options, err := buildAndExecuteRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("get PostgreSQL Flex options: %w", err) } - return nil + return outputResult(params.Printer, model, options) }, } configureFlags(cmd) return cmd } +// Deprecated: Will be removed after 2027-01-31. func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(flavorsFlag, false, "Lists supported flavors") cmd.Flags().Bool(versionsFlag, false, "Lists supported versions") cmd.Flags().Bool(storagesFlag, false, "Lists supported storages for a given flavor") - cmd.Flags().String(flavorIdFlag, "", `The flavor ID to show storages for. Only relevant when "--storages" is passed`) + cmd.Flags().String(flavorIdFlag, "", fmt.Sprintf("The flavor ID to show storages for. Only relevant when \"--%s\" is passed", storagesFlag)) } +// Deprecated: Will be removed after 2027-01-31. func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { @@ -132,53 +142,59 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputModel, apiClient postgresflex.DefaultAPI) error { - var flavors *postgresflex.ListFlavorsResponse - var versions *postgresflex.ListVersionsResponse - var storages *postgresflex.ListStoragesResponse - var err error +// Deprecated: Will be removed after 2027-01-31. +func buildAndExecuteRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (*options, error) { + options := options{} - if model.Flavors { - flavors, err = apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() + if model.Flavors || model.Storages { + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { - return fmt.Errorf("get PostgreSQL Flex flavors: %w", err) + return nil, fmt.Errorf("get PostgreSQL Flex flavors: %w", err) } - } - if model.Versions { - versions, err = apiClient.ListVersions(ctx, model.ProjectId, model.Region).Execute() - if err != nil { - return fmt.Errorf("get PostgreSQL Flex versions: %w", err) + + if model.Flavors { + options.Flavors = flavors.Flavors + } + + if model.Storages && model.FlavorId != nil { + for _, f := range flavors.Flavors { + if f.Id == *model.FlavorId { + options.Storages = &flavorStorages{ + FlavorId: f.Id, + Storages: f.StorageClasses, + } + } + } + + if options.Storages == nil { + return nil, fmt.Errorf("couldn't find flavor with id \"%s\"", *model.FlavorId) + } } } - if model.Storages { - storages, err = apiClient.ListStorages(ctx, model.ProjectId, model.Region, *model.FlavorId).Execute() + + if model.Versions { + versions, err := apiClient.ListVersions(ctx, model.ProjectId, model.Region).Execute() if err != nil { - return fmt.Errorf("get PostgreSQL Flex storages: %w", err) + return nil, fmt.Errorf("get PostgreSQL Flex versions: %w", err) } + + options.Versions = versions.Versions } - return outputResult(p, *model, flavors, versions, storages) + return &options, nil } -func outputResult(p *print.Printer, model inputModel, flavors *postgresflex.ListFlavorsResponse, versions *postgresflex.ListVersionsResponse, storages *postgresflex.ListStoragesResponse) error { - options := &options{} - if flavors != nil { - options.Flavors = flavors.Flavors - } - if model.GlobalFlagModel == nil { - return fmt.Errorf("no global model defined") - } - if versions != nil { - options.Versions = versions.Versions - } - if storages != nil && model.FlavorId != nil { - options.Storages = &flavorStorages{ - FlavorId: utils.PtrString(model.FlavorId), - Storages: storages, - } +// Deprecated: Will be removed after 2027-01-31. +func outputResult(p *print.Printer, model *inputModel, options *options) error { + if model == nil || model.GlobalFlagModel == nil { + return fmt.Errorf("model is nil") } return p.OutputResult(model.OutputFormat, options, func() error { + if options == nil { + return fmt.Errorf("options is nil") + } + content := []tables.Table{} if model.Flavors && len(options.Flavors) != 0 { content = append(content, buildFlavorsTable(options.Flavors)) @@ -186,8 +202,8 @@ func outputResult(p *print.Printer, model inputModel, flavors *postgresflex.List if model.Versions && len(options.Versions) != 0 { content = append(content, buildVersionsTable(options.Versions)) } - if model.Storages && options.Storages.Storages != nil && len(options.Storages.Storages.StorageClasses) > 0 { - content = append(content, buildStoragesTable(*options.Storages.Storages)) + if model.Storages && options.Storages != nil && len(options.Storages.Storages) > 0 { + content = append(content, buildStoragesTable(options.Storages.Storages)) } err := tables.DisplayTables(p, content) @@ -199,44 +215,45 @@ func outputResult(p *print.Printer, model inputModel, flavors *postgresflex.List }) } -func buildFlavorsTable(flavors []postgresflex.Flavor) tables.Table { +// Deprecated: Will be removed after 2027-01-31. +func buildFlavorsTable(flavors []postgresflex.ListFlavors) tables.Table { table := tables.NewTable() table.SetTitle("Flavors") table.SetHeader("ID", "CPU", "MEMORY", "DESCRIPTION") - for i := range flavors { - f := flavors[i] + for _, f := range flavors { table.AddRow( - utils.PtrString(f.Id), - utils.PtrString(f.Cpu), - utils.PtrString(f.Memory), - utils.PtrString(f.Description), + f.Id, + f.Cpu, + f.Memory, + f.Description, ) + table.AddSeparator() } return table } -func buildVersionsTable(versions []string) tables.Table { +// Deprecated: Will be removed after 2027-01-31. +func buildVersionsTable(versions []postgresflex.Version) tables.Table { table := tables.NewTable() table.SetTitle("Versions") - table.SetHeader("VERSION") - for i := range versions { - v := versions[i] - table.AddRow(v) + table.SetHeader("VERSION", "RECOMMEND", "BETA", "DEPRECATED") + + for _, v := range versions { + table.AddRow(v.Version, v.Recommend, v.Beta, v.Deprecated) + table.AddSeparator() } return table } -func buildStoragesTable(storagesResp postgresflex.ListStoragesResponse) tables.Table { +// Deprecated: Will be removed after 2027-01-31. +func buildStoragesTable(storageClasses []postgresflex.FlavorStorageClassesStorageClass) tables.Table { table := tables.NewTable() table.SetTitle("Storages") - table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") - for _, sc := range storagesResp.StorageClasses { - table.AddRow( - utils.PtrString(storagesResp.StorageRange.Min), - utils.PtrString(storagesResp.StorageRange.Max), - sc, - ) + table.SetHeader("STORAGE CLASS", "MAX IO PER SEC", "MAX THROUGH (MB)") + + for _, sc := range storageClasses { + table.AddRow(sc.Class, sc.MaxIoPerSec, sc.MaxThroughInMb) + table.AddSeparator() } - table.EnableAutoMergeOnColumns(1, 2, 3) return table } diff --git a/internal/cmd/postgresflex/options/options_test.go b/internal/cmd/postgresflex/options/options_test.go index f2798225f..4735e1a0e 100644 --- a/internal/cmd/postgresflex/options/options_test.go +++ b/internal/cmd/postgresflex/options/options_test.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "testing" + "time" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -23,45 +25,37 @@ var testProjectId = uuid.NewString() type mockSettings struct { listFlavorsFails bool listVersionsFails bool - listStoragesFails bool + + flavors []postgresflex.ListFlavors + versions []postgresflex.Version listFlavorsCalled bool listVersionsCalled bool - listStoragesCalled bool } func newAPIClientMock(c *mockSettings) postgresflex.DefaultAPI { return postgresflex.DefaultAPIServiceMock{ ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { c.listFlavorsCalled = true + if c.listFlavorsFails { return nil, fmt.Errorf("list flavors failed") } - return utils.Ptr(postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{}, - }), nil + + return &postgresflex.ListFlavorsResponse{ + Flavors: c.flavors, + }, nil }), ListVersionsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListVersionsRequest) (*postgresflex.ListVersionsResponse, error) { c.listVersionsCalled = true + if c.listVersionsFails { return nil, fmt.Errorf("list versions failed") } - return utils.Ptr(postgresflex.ListVersionsResponse{ - Versions: []string{}, - }), nil - }), - ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { - c.listStoragesCalled = true - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") - } - return utils.Ptr(postgresflex.ListStoragesResponse{ - StorageClasses: []string{}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }), nil + + return &postgresflex.ListVersionsResponse{ + Versions: c.versions, + }, nil }), } } @@ -69,10 +63,6 @@ func newAPIClientMock(c *mockSettings) postgresflex.DefaultAPI { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - flavorsFlag: "true", - versionsFlag: "true", - storagesFlag: "true", - flavorIdFlag: "2.4", } for _, mod := range mods { mod(flagValues) @@ -80,7 +70,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st return flagValues } -func fixtureInputModelAllFalse(mods ...func(model *inputModel)) *inputModel { +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, @@ -96,23 +86,6 @@ func fixtureInputModelAllFalse(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { - model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ - ProjectId: testProjectId, - Verbosity: globalflags.VerbosityDefault, - }, - Flavors: true, - Versions: true, - Storages: true, - FlavorId: utils.Ptr("2.4"), - } - for _, mod := range mods { - mod(model) - } - return model -} - func TestParseInput(t *testing.T) { tests := []struct { description string @@ -122,10 +95,25 @@ func TestParseInput(t *testing.T) { expectedModel *inputModel }{ { - description: "all values", - flagValues: fixtureFlagValues(), - isValid: true, - expectedModel: fixtureInputModelAllTrue(), + description: "default (invalid) - at least one flag must be set", + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "all values", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[versionsFlag] = "true" + flagValues[storagesFlag] = "true" + flagValues[flavorsFlag] = "true" + flagValues[flavorIdFlag] = "16.64" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Storages = true + model.Versions = true + model.Flavors = true + model.FlavorId = utils.Ptr("16.64") + }), }, { description: "no values", @@ -135,11 +123,11 @@ func TestParseInput(t *testing.T) { { description: "some values 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[storagesFlag] = "false" - delete(flagValues, flavorIdFlag) + flagValues[versionsFlag] = "true" + flagValues[flavorsFlag] = "true" }), isValid: true, - expectedModel: fixtureInputModelAllFalse(func(model *inputModel) { + expectedModel: fixtureInputModel(func(model *inputModel) { model.Flavors = true model.Versions = true }), @@ -153,7 +141,7 @@ func TestParseInput(t *testing.T) { flagValues[flavorIdFlag] = "2.4" }), isValid: true, - expectedModel: fixtureInputModelAllFalse(func(model *inputModel) { + expectedModel: fixtureInputModel(func(model *inputModel) { model.Storages = true model.FlavorId = utils.Ptr("2.4") }), @@ -161,6 +149,7 @@ func TestParseInput(t *testing.T) { { description: "storages without flavor-id", flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[storagesFlag] = "true" delete(flagValues, flavorIdFlag) }), isValid: false, @@ -168,12 +157,10 @@ func TestParseInput(t *testing.T) { { description: "flavor-id without storage", flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[storagesFlag] = "false" delete(flagValues, storagesFlag) }), - isValid: true, - expectedModel: fixtureInputModelAllTrue(func(model *inputModel) { - model.Storages = false - }), + isValid: false, }, } @@ -185,6 +172,58 @@ func TestParseInput(t *testing.T) { } func TestBuildAndExecuteRequest(t *testing.T) { + fixtureVersions := func() []postgresflex.Version { + return []postgresflex.Version{ + { + Version: "2.4", + Beta: false, + Recommend: true, + Deprecated: "", + }, + { + Version: "1.9", + Beta: false, + Recommend: false, + Deprecated: time.Date(2026, time.July, 27, 12, 0, 0, 0, time.UTC).String(), + }, + { + Version: "3.0", + Beta: true, + Recommend: false, + }, + } + } + + fixtureFlavors := func() []postgresflex.ListFlavors { + return []postgresflex.ListFlavors{ + { + Id: "16.64", + Cpu: 16, + Description: "PostgreSQL-Flex-16.64-Single-EU01", + MaxGB: 4000, + MinGB: 5, + NodeType: "Single", + StorageClasses: []postgresflex.FlavorStorageClassesStorageClass{ + { + Class: "premium-perf2-stackit", + MaxIoPerSec: 1000, + MaxThroughInMb: 100, + }, + { + Class: "premium-perf4-stackit", + MaxIoPerSec: 2000, + MaxThroughInMb: 150, + }, + { + Class: "premium-perf6-stackit", + MaxIoPerSec: 5000, + MaxThroughInMb: 200, + }, + }, + }, + } + } + tests := []struct { description string model *inputModel @@ -192,85 +231,117 @@ func TestBuildAndExecuteRequest(t *testing.T) { mockClientSettings mockSettings expectListFlavorsCalled bool expectListVersionsCalled bool - expectListStoragesCalled bool + want *options }{ { - description: "all values", - model: fixtureInputModelAllTrue(), + description: "all values", + model: fixtureInputModel(func(model *inputModel) { + model.Storages = true + model.Versions = true + model.Flavors = true + }), + mockClientSettings: mockSettings{ + flavors: fixtureFlavors(), + versions: fixtureVersions(), + }, isValid: true, expectListFlavorsCalled: true, expectListVersionsCalled: true, - expectListStoragesCalled: true, + want: &options{ + Flavors: fixtureFlavors(), + Versions: fixtureVersions(), + }, }, { description: "no values", - model: fixtureInputModelAllFalse(), + model: fixtureInputModel(), isValid: true, expectListFlavorsCalled: false, expectListVersionsCalled: false, - expectListStoragesCalled: false, + want: &options{}, }, { description: "only flavors", - model: fixtureInputModelAllFalse(func(model *inputModel) { model.Flavors = true }), + model: fixtureInputModel(func(model *inputModel) { model.Flavors = true }), isValid: true, expectListFlavorsCalled: true, + want: &options{}, }, { - description: "only versions", - model: fixtureInputModelAllFalse(func(model *inputModel) { model.Versions = true }), + description: "only versions", + model: fixtureInputModel(func(model *inputModel) { model.Versions = true }), + mockClientSettings: mockSettings{ + versions: fixtureVersions(), + }, isValid: true, expectListVersionsCalled: true, + want: &options{ + Versions: fixtureVersions(), + }, }, { - description: "only storages", - model: fixtureInputModelAllFalse(func(model *inputModel) { + description: "only storages - flavor not found", + model: fixtureInputModel(func(model *inputModel) { model.Storages = true - model.FlavorId = utils.Ptr("2.4") + model.FlavorId = utils.Ptr("2.2") }), - isValid: true, - expectListStoragesCalled: true, + isValid: false, + }, + { + description: "only storages - flavor found", + model: fixtureInputModel(func(model *inputModel) { + model.Storages = true + model.FlavorId = func() *string { + return &fixtureFlavors()[0].Id + }() + }), + mockClientSettings: mockSettings{ + flavors: fixtureFlavors(), + }, + expectListFlavorsCalled: true, + want: &options{ + Storages: func() *flavorStorages { + return &flavorStorages{ + FlavorId: fixtureFlavors()[0].Id, + Storages: fixtureFlavors()[0].StorageClasses, + } + }(), + }, + isValid: true, }, { description: "list flavors fails", - model: fixtureInputModelAllTrue(), - isValid: false, + model: fixtureInputModel(func(model *inputModel) { + model.Storages = true + model.Versions = true + model.Flavors = true + }), + isValid: false, mockClientSettings: mockSettings{ listFlavorsFails: true, }, expectListFlavorsCalled: true, expectListVersionsCalled: false, - expectListStoragesCalled: false, }, { description: "list versions fails", - model: fixtureInputModelAllTrue(), - isValid: false, + model: fixtureInputModel(func(model *inputModel) { + model.Storages = true + model.Versions = true + model.Flavors = true + }), + isValid: false, mockClientSettings: mockSettings{ listVersionsFails: true, }, expectListFlavorsCalled: true, expectListVersionsCalled: true, - expectListStoragesCalled: false, - }, - { - description: "list storages fails", - model: fixtureInputModelAllTrue(), - isValid: false, - mockClientSettings: mockSettings{ - listStoragesFails: true, - }, - expectListFlavorsCalled: true, - expectListVersionsCalled: true, - expectListStoragesCalled: true, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - params := testparams.NewTestParams() - client := newAPIClientMock(&tt.mockClientSettings) - err := buildAndExecuteRequest(testCtx, params.Printer, tt.model, client) + actual, err := buildAndExecuteRequest(testCtx, tt.model, newAPIClientMock(&tt.mockClientSettings)) if err != nil && tt.isValid { t.Fatalf("error building and executing request: %v", err) } @@ -287,8 +358,10 @@ func TestBuildAndExecuteRequest(t *testing.T) { if tt.expectListVersionsCalled != tt.mockClientSettings.listVersionsCalled { t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, tt.mockClientSettings.listVersionsCalled) } - if tt.expectListStoragesCalled != tt.mockClientSettings.listStoragesCalled { - t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, tt.mockClientSettings.listStoragesCalled) + + diff := cmp.Diff(actual, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) } }) } @@ -296,45 +369,69 @@ func TestBuildAndExecuteRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { - model inputModel - flavors *postgresflex.ListFlavorsResponse - versions *postgresflex.ListVersionsResponse - storages *postgresflex.ListStoragesResponse + model *inputModel + options *options } tests := []struct { name string args args wantErr bool }{ - {"empty", args{model: inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}}, false}, - {"standard", args{ - model: inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, - flavors: &postgresflex.ListFlavorsResponse{}, - versions: &postgresflex.ListVersionsResponse{}, - storages: &postgresflex.ListStoragesResponse{}, - }, false}, { - "complete", - args{ - model: inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}, Flavors: false, Versions: false, Storages: false, FlavorId: new(string)}, - flavors: &postgresflex.ListFlavorsResponse{ - Flavors: []postgresflex.Flavor{}, + name: "options is nil", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{}, }, - versions: &postgresflex.ListVersionsResponse{ - Versions: []string{}, + options: nil, + }, + wantErr: true, + }, + { + name: "empty", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{}, + }, + options: &options{}, + }, + wantErr: false, + }, + { + name: "empty flavors and versions slice", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{}, + }, + options: &options{ + Flavors: []postgresflex.ListFlavors{}, + Versions: []postgresflex.Version{}, + }, + }, + wantErr: false, + }, + { + name: "complete", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{}, + Flavors: false, + Versions: false, + Storages: false, + FlavorId: new(string), }, - storages: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{}, - StorageRange: &postgresflex.StorageRange{}, + options: &options{ + Flavors: []postgresflex.ListFlavors{}, + Versions: []postgresflex.Version{}, }, }, - false, + wantErr: false, }, } params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(params.Printer, tt.args.model, tt.args.flavors, tt.args.versions, tt.args.storages); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/postgresflex.go b/internal/cmd/postgresflex/postgresflex.go index 536584f2f..7e7dc57b0 100644 --- a/internal/cmd/postgresflex/postgresflex.go +++ b/internal/cmd/postgresflex/postgresflex.go @@ -2,9 +2,11 @@ package postgresflex import ( "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/backup" + "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/flavor" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/options" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user" + "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/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" @@ -28,6 +30,8 @@ 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)) } diff --git a/internal/cmd/postgresflex/user/create/create.go b/internal/cmd/postgresflex/user/create/create.go index 3ea952b06..424f3e45d 100644 --- a/internal/cmd/postgresflex/user/create/create.go +++ b/internal/cmd/postgresflex/user/create/create.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -17,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( @@ -39,7 +38,7 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - Username *string + Username string Roles []string } @@ -120,7 +119,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), - Username: flags.FlagToStringPointer(p, cmd, usernameFlag), + Username: flags.FlagToStringValue(p, cmd, usernameFlag), Roles: roleFlag.Get(), } @@ -131,28 +130,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiCreateUserRequest { req := apiClient.DefaultAPI.CreateUser(ctx, model.ProjectId, model.Region, model.InstanceId) req = req.CreateUserPayload(postgresflex.CreateUserPayload{ - Username: model.Username, - Roles: model.Roles, + Name: model.Username, + Roles: model.Roles, }) return req } -func outputResult(p *print.Printer, outputFormat, instanceLabel string, resp *postgresflex.CreateUserResponse) error { - if resp == nil { - return fmt.Errorf("no response passed") - } - - return p.OutputResult(outputFormat, resp, func() error { - if user := resp.Item; user != nil { - p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, utils.PtrString(user.Id)) - p.Outputf("Username: %s\n", utils.PtrString(user.Username)) - p.Outputf("Password: %s\n", utils.PtrString(user.Password)) - p.Outputf("Roles: %v\n", user.Roles) - p.Outputf("Host: %s\n", utils.PtrString(user.Host)) - p.Outputf("Port: %s\n", utils.PtrString(user.Port)) - p.Outputf("URI: %s\n", utils.PtrString(user.Uri)) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, user *postgresflex.CreateUserResponse) error { + return p.OutputResult(outputFormat, user, func() error { + if user == nil { + return fmt.Errorf("no response passed") } + p.Outputf("Created user for instance %q. User ID: %d\n\n", instanceLabel, user.Id) + p.Outputf("Username: %s\n", user.Name) + p.Outputf("Password: %s\n", user.Password) + return nil }) } diff --git a/internal/cmd/postgresflex/user/create/create_test.go b/internal/cmd/postgresflex/user/create/create_test.go index 82bf6cec8..d5898fc74 100644 --- a/internal/cmd/postgresflex/user/create/create_test.go +++ b/internal/cmd/postgresflex/user/create/create_test.go @@ -7,12 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) type testCtxKey struct{} @@ -45,7 +44,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - Username: utils.Ptr("johndoe"), + Username: "johndoe", Roles: []string{"login"}, } for _, mod := range mods { @@ -57,8 +56,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureRequest(mods ...func(request *postgresflex.ApiCreateUserRequest)) postgresflex.ApiCreateUserRequest { request := testClient.DefaultAPI.CreateUser(testCtx, testProjectId, testRegion, testInstanceId) request = request.CreateUserPayload(postgresflex.CreateUserPayload{ - Username: utils.Ptr("johndoe"), - Roles: []string{"login"}, + Name: "johndoe", + Roles: []string{"login"}, }) for _, mod := range mods { @@ -166,15 +165,6 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), }, - { - description: "no username specified", - model: fixtureInputModel(func(model *inputModel) { - model.Username = nil - }), - expectedRequest: fixtureRequest().CreateUserPayload(postgresflex.CreateUserPayload{ - Roles: []string{"login"}, - }), - }, } for _, tt := range tests { @@ -203,20 +193,29 @@ func Test_outputResult(t *testing.T) { args args wantErr bool }{ - {"empty", args{}, true}, - {"standard", args{resp: &postgresflex.CreateUserResponse{}}, false}, - {"complete", args{resp: &postgresflex.CreateUserResponse{ - Item: &postgresflex.User{ - Database: new(string), - Host: new(string), - Id: new(string), - Password: new(string), - Port: new(int64), - Roles: []string{}, - Uri: new(string), - Username: new(string), + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "standard", + args: args{ + resp: &postgresflex.CreateUserResponse{}, }, - }}, false}, + wantErr: false, + }, + { + name: "complete", + args: args{ + resp: &postgresflex.CreateUserResponse{ + Id: int64(12), + Name: "username", + Password: "password", + }, + }, + wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/user/delete/delete.go b/internal/cmd/postgresflex/user/delete/delete.go index f6667191c..db3fc0dc1 100644 --- a/internal/cmd/postgresflex/user/delete/delete.go +++ b/internal/cmd/postgresflex/user/delete/delete.go @@ -3,6 +3,7 @@ package delete import ( "context" "fmt" + "strconv" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -16,7 +17,7 @@ import ( postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - UserId string + UserId int64 } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -69,7 +70,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) - userLabel = model.UserId + userLabel = strconv.FormatInt(model.UserId, 10) } prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) @@ -101,13 +102,18 @@ func configureFlags(cmd *cobra.Command) { } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - userId := inputArgs[0] + userIdStr := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + userId, err := strconv.ParseInt(userIdStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid user id format, must be an integer: %w", err) + } + model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), diff --git a/internal/cmd/postgresflex/user/delete/delete_test.go b/internal/cmd/postgresflex/user/delete/delete_test.go index a91291466..ccc7e9a04 100644 --- a/internal/cmd/postgresflex/user/delete/delete_test.go +++ b/internal/cmd/postgresflex/user/delete/delete_test.go @@ -2,6 +2,7 @@ package delete import ( "context" + "strconv" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -10,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) type testCtxKey struct{} @@ -19,12 +20,15 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testUserId = "12345" -var testRegion = "eu01" + +const ( + testUserId = int64(12345) + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testUserId, + strconv.FormatInt(testUserId, 10), } for _, mod := range mods { mod(argValues) diff --git a/internal/cmd/postgresflex/user/describe/describe.go b/internal/cmd/postgresflex/user/describe/describe.go index 17a68f2e0..bbccf0e41 100644 --- a/internal/cmd/postgresflex/user/describe/describe.go +++ b/internal/cmd/postgresflex/user/describe/describe.go @@ -3,11 +3,13 @@ package describe import ( "context" "fmt" + "strconv" + "strings" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -17,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( @@ -30,7 +31,7 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - UserId string + UserId int64 } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -71,7 +72,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("get postgresflex user: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp.Item) + return outputResult(params.Printer, model.OutputFormat, resp) }, } @@ -87,13 +88,18 @@ func configureFlags(cmd *cobra.Command) { } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - userId := inputArgs[0] + userIdStr := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + userId, err := strconv.ParseInt(userIdStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid user id format, must be an integer: %w", err) + } + model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), @@ -109,18 +115,19 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresfle return req } -func outputResult(p *print.Printer, outputFormat string, user postgresflex.UserResponse) error { +func outputResult(p *print.Printer, outputFormat string, user *postgresflex.GetUserResponse) error { return p.OutputResult(outputFormat, user, func() error { + if user == nil { + return fmt.Errorf("user is nil") + } + table := tables.NewTable() - table.AddRow("ID", utils.PtrString(user.Id)) - table.AddSeparator() - table.AddRow("USERNAME", utils.PtrString(user.Username)) + table.AddRow("ID", user.Id) table.AddSeparator() - table.AddRow("ROLES", user.Roles) + table.AddRow("USERNAME", user.Name) table.AddSeparator() - table.AddRow("HOST", utils.PtrString(user.Host)) + table.AddRow("ROLES", strings.Join(user.Roles, ", ")) table.AddSeparator() - table.AddRow("PORT", utils.PtrString(user.Port)) err := table.Display(p) if err != nil { diff --git a/internal/cmd/postgresflex/user/describe/describe_test.go b/internal/cmd/postgresflex/user/describe/describe_test.go index e5f64a31f..f2c2e36f9 100644 --- a/internal/cmd/postgresflex/user/describe/describe_test.go +++ b/internal/cmd/postgresflex/user/describe/describe_test.go @@ -2,12 +2,13 @@ package describe import ( "context" + "strconv" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -20,12 +21,15 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testUserId = "12345" -var testRegion = "eu01" + +const ( + testUserId = int64(12345) + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testUserId, + strconv.FormatInt(testUserId, 10), } for _, mod := range mods { mod(argValues) @@ -196,22 +200,36 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string - user postgresflex.UserResponse + user *postgresflex.GetUserResponse } tests := []struct { name string args args wantErr bool }{ - {"basic", args{}, false}, - {"standard", args{user: postgresflex.UserResponse{}}, false}, - {"complete", args{user: postgresflex.UserResponse{ - Host: new(string), - Id: new(string), - Port: new(int64), - Roles: []string{}, - Username: new(string), - }}, false}, + { + name: "user is nil", + args: args{ + user: nil, + }, + wantErr: true, + }, + { + name: "standard", + args: args{ + user: &postgresflex.GetUserResponse{}, + }, + wantErr: false}, + { + name: "complete", + args: args{user: &postgresflex.GetUserResponse{ + Id: int64(1000), + Name: "username", + Roles: []string{}, + State: "foo", + }}, + wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/user/list/list.go b/internal/cmd/postgresflex/user/list/list.go index abe95d1a4..a7a6aa869 100644 --- a/internal/cmd/postgresflex/user/list/list.go +++ b/internal/cmd/postgresflex/user/list/list.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -18,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( @@ -69,7 +68,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get PostgreSQL Flex users: %w", err) } - users := resp.Items + users := resp.Users // Truncate output if model.Limit != nil && len(users) > int(*model.Limit) { @@ -126,7 +125,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresfle return req } -func outputResult(p *print.Printer, outputFormat, instanceLabel string, users []postgresflex.ListUsersResponseItem) error { +func outputResult(p *print.Printer, outputFormat, instanceLabel string, users []postgresflex.ListUser) error { return p.OutputResult(outputFormat, users, func() error { if len(users) == 0 { p.Outputf("No users found for instance %q\n", instanceLabel) @@ -137,8 +136,8 @@ func outputResult(p *print.Printer, outputFormat, instanceLabel string, users [] for i := range users { user := users[i] table.AddRow( - utils.PtrString(user.Id), - utils.PtrString(user.Username), + user.Id, + user.Name, ) } err := table.Display(p) diff --git a/internal/cmd/postgresflex/user/list/list_test.go b/internal/cmd/postgresflex/user/list/list_test.go index 808079f8a..5b9c19725 100644 --- a/internal/cmd/postgresflex/user/list/list_test.go +++ b/internal/cmd/postgresflex/user/list/list_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -169,19 +169,47 @@ func Test_outputResult(t *testing.T) { type args struct { outputFormat string instanceLabel string - users []postgresflex.ListUsersResponseItem + users []postgresflex.ListUser } tests := []struct { name string args args wantErr bool }{ - {"empty", args{}, false}, - {"standard", args{users: []postgresflex.ListUsersResponseItem{{}}}, false}, - {"complete", args{instanceLabel: "label", users: []postgresflex.ListUsersResponseItem{{ - Id: new(string), - Username: new(string), - }}}, false}, + { + name: "users is nil", + args: args{ + users: nil, + }, + wantErr: false, + }, + { + name: "users is empty", + args: args{ + users: []postgresflex.ListUser{}, + }, + wantErr: false, + }, + { + name: "empty user in users slice", + args: args{ + users: []postgresflex.ListUser{{}}, + }, + wantErr: false, + }, + { + name: "complete", + args: args{ + instanceLabel: "label", + users: []postgresflex.ListUser{ + { + Id: int64(42), + Name: "username", + }, + }, + }, + wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/user/reset-password/reset_password.go b/internal/cmd/postgresflex/user/reset-password/reset_password.go index 7a6ce4efb..a6e1020f1 100644 --- a/internal/cmd/postgresflex/user/reset-password/reset_password.go +++ b/internal/cmd/postgresflex/user/reset-password/reset_password.go @@ -3,11 +3,12 @@ package resetpassword import ( "context" "fmt" + "strconv" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - UserId string + UserId int64 } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -69,7 +69,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) - userLabel = model.UserId + userLabel = strconv.FormatInt(model.UserId, 10) } prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) @@ -101,13 +101,18 @@ func configureFlags(cmd *cobra.Command) { } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - userId := inputArgs[0] + userIdStr := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + userId, err := strconv.ParseInt(userIdStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid user id format, must be an integer: %w", err) + } + model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), @@ -118,22 +123,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiResetUserRequest { - req := apiClient.DefaultAPI.ResetUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) +func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiResetUserPasswordRequest { + req := apiClient.DefaultAPI.ResetUserPassword(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) return req } -func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel string, user *postgresflex.ResetUserResponse) error { +func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel string, user *postgresflex.ResetUserPasswordResponse) error { return p.OutputResult(outputFormat, user, func() error { if user == nil { return fmt.Errorf("no response passed") } + p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel) - if item := user.Item; item != nil { - p.Outputf("Username: %s\n", utils.PtrString(item.Username)) - p.Outputf("New password: %s\n", utils.PtrString(item.Password)) - p.Outputf("New URI: %s\n", utils.PtrString(item.Uri)) - } + p.Outputf("Username: %s\n", user.Name) + p.Outputf("New password: %s\n", user.Password) return nil }) } diff --git a/internal/cmd/postgresflex/user/reset-password/reset_password_test.go b/internal/cmd/postgresflex/user/reset-password/reset_password_test.go index 4ee3fc726..bf250eda5 100644 --- a/internal/cmd/postgresflex/user/reset-password/reset_password_test.go +++ b/internal/cmd/postgresflex/user/reset-password/reset_password_test.go @@ -2,12 +2,13 @@ package resetpassword import ( "context" + "strconv" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" @@ -20,12 +21,15 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testUserId = "12345" -var testRegion = "eu01" + +const ( + testUserId = int64(12345) + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testUserId, + strconv.FormatInt(testUserId, 10), } for _, mod := range mods { mod(argValues) @@ -61,8 +65,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *postgresflex.ApiResetUserRequest)) postgresflex.ApiResetUserRequest { - request := testClient.DefaultAPI.ResetUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) +func fixtureRequest(mods ...func(request *postgresflex.ApiResetUserPasswordRequest)) postgresflex.ApiResetUserPasswordRequest { + request := testClient.DefaultAPI.ResetUserPassword(testCtx, testProjectId, testRegion, testInstanceId, testUserId) for _, mod := range mods { mod(&request) } @@ -169,7 +173,7 @@ func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest postgresflex.ApiResetUserRequest + expectedRequest postgresflex.ApiResetUserPasswordRequest }{ { description: "base", @@ -198,21 +202,33 @@ func Test_outputResult(t *testing.T) { outputFormat string userLabel string instanceLabel string - user *postgresflex.ResetUserResponse + user *postgresflex.ResetUserPasswordResponse } tests := []struct { name string args args wantErr bool }{ - {"empty", args{}, true}, - {"standard", args{user: &postgresflex.ResetUserResponse{}}, false}, - {"complete", args{ - userLabel: "userLabel", - instanceLabel: "instanceLabel", - user: &postgresflex.ResetUserResponse{ - Item: &postgresflex.User{}, - }}, false}, + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "standard", + args: args{ + user: &postgresflex.ResetUserPasswordResponse{}, + }, + wantErr: false, + }, + { + name: "complete", + args: args{ + userLabel: "userLabel", + instanceLabel: "instanceLabel", + user: &postgresflex.ResetUserPasswordResponse{}}, + wantErr: false, + }, } params := testparams.NewTestParams() for _, tt := range tests { diff --git a/internal/cmd/postgresflex/user/update/update.go b/internal/cmd/postgresflex/user/update/update.go index e76bce64c..7d02f80dc 100644 --- a/internal/cmd/postgresflex/user/update/update.go +++ b/internal/cmd/postgresflex/user/update/update.go @@ -3,6 +3,7 @@ package update import ( "context" "fmt" + "strconv" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -16,7 +17,7 @@ import ( postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/spf13/cobra" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) const ( @@ -35,7 +36,7 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - UserId string + UserId int64 Roles []string } @@ -72,7 +73,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) - userLabel = model.UserId + userLabel = strconv.FormatInt(model.UserId, 10) } prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) @@ -106,13 +107,18 @@ func configureFlags(cmd *cobra.Command) { } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - userId := inputArgs[0] + userIdStr := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + userId, err := strconv.ParseInt(userIdStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid user id format, must be an integer: %w", err) + } + roles := roleFlag.Get() if len(roles) == 0 { return nil, &errors.EmptyUpdateError{} diff --git a/internal/cmd/postgresflex/user/update/update_test.go b/internal/cmd/postgresflex/user/update/update_test.go index 89a194b4b..bb2dee2f8 100644 --- a/internal/cmd/postgresflex/user/update/update_test.go +++ b/internal/cmd/postgresflex/user/update/update_test.go @@ -2,6 +2,7 @@ package update import ( "context" + "strconv" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -10,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) type testCtxKey struct{} @@ -19,12 +20,15 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testUserId = "12345" -var testRegion = "eu01" + +const ( + testUserId = int64(12345) + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testUserId, + strconv.FormatInt(testUserId, 10), } for _, mod := range mods { mod(argValues) diff --git a/internal/cmd/postgresflex/version/list/list.go b/internal/cmd/postgresflex/version/list/list.go new file mode 100644 index 000000000..4d8928938 --- /dev/null +++ b/internal/cmd/postgresflex/version/list/list.go @@ -0,0 +1,96 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "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/postgresflex/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 PostgreSQL Flex versions", + Long: "Lists PostgreSQL Flex versions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List PostgreSQL Flex version options`, + "$ stackit postgresflex 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 PostgreSQL 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 postgresflex.DefaultAPI) postgresflex.ApiListVersionsRequest { + return apiClient.ListVersions(ctx, model.ProjectId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, versions []postgresflex.Version) error { + return p.OutputResult(outputFormat, versions, func() error { + if len(versions) == 0 { + p.Outputf("No PostgreSQL Flex versions found.") + return nil + } + + table := tables.NewTable() + table.SetTitle("Versions") + table.SetHeader("VERSION", "RECOMMEND", "BETA", "DEPRECATED") + + for _, v := range versions { + table.AddRow(v.Version, v.Recommend, v.Beta, v.Deprecated) + table.AddSeparator() + } + + return table.Display(p) + }) +} diff --git a/internal/cmd/postgresflex/version/list/list_test.go b/internal/cmd/postgresflex/version/list/list_test.go new file mode 100644 index 000000000..e943c441b --- /dev/null +++ b/internal/cmd/postgresflex/version/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" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.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 *postgresflex.ApiListVersionsRequest)) postgresflex.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 postgresflex.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, postgresflex.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + versions []postgresflex.Version + } + 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: []postgresflex.Version{}, + }, + wantErr: false, + }, + { + name: "empty version in versions slice", + args: args{ + versions: []postgresflex.Version{{}}, + }, + 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/postgresflex/version/version.go b/internal/cmd/postgresflex/version/version.go new file mode 100644 index 000000000..22e8a0355 --- /dev/null +++ b/internal/cmd/postgresflex/version/version.go @@ -0,0 +1,26 @@ +package version + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/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 PostgreSQL Flex versions", + Long: "Provides functionality for PostgreSQL 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/pkg/services/postgresflex/client/client.go b/internal/pkg/services/postgresflex/client/client.go index a40b07aec..892eddc57 100644 --- a/internal/pkg/services/postgresflex/client/client.go +++ b/internal/pkg/services/postgresflex/client/client.go @@ -1,14 +1,20 @@ package client import ( + postgresflexLegacy "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/spf13/viper" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*postgresflex.APIClient, error) { - return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.PostgresFlexCustomEndpointKey), false, genericclient.CreateApiClient[*postgresflex.APIClient](postgresflex.NewAPIClient)) + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.PostgresFlexCustomEndpointKey), false, postgresflex.NewAPIClient) +} + +func ConfigureClientLegacy(p *print.Printer, cliVersion string) (*postgresflexLegacy.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.PostgresFlexCustomEndpointKey), false, postgresflexLegacy.NewAPIClient) } diff --git a/internal/pkg/services/postgresflex/utils/utils.go b/internal/pkg/services/postgresflex/utils/utils.go index 4920feb15..c2b3fe8ed 100644 --- a/internal/pkg/services/postgresflex/utils/utils.go +++ b/internal/pkg/services/postgresflex/utils/utils.go @@ -8,7 +8,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" "golang.org/x/mod/semver" ) @@ -54,13 +54,13 @@ func GetInstanceType(numReplicas int32) (string, error) { return "", fmt.Errorf("invalid number of replicas: %v", numReplicas) } -func ValidateFlavorId(flavorId string, flavors []postgresflex.Flavor) error { +func ValidateFlavorId(flavorId string, flavors []postgresflex.ListFlavors) error { if flavors == nil { return fmt.Errorf("nil flavors") } for _, f := range flavors { - if f.Id != nil && strings.EqualFold(*f.Id, flavorId) { + if strings.EqualFold(f.Id, flavorId) { return nil } } @@ -71,14 +71,18 @@ func ValidateFlavorId(flavorId string, flavors []postgresflex.Flavor) error { } } -func ValidateStorage(storageClass *string, storageSize *int64, storages *postgresflex.ListStoragesResponse, flavorId string) error { - if storages == nil { - return fmt.Errorf("nil storages") +/*func ValidateStorage(storageClass *string, storageSize *int64, flavors []postgresflex.ListFlavors, flavorId string) error { + if flavors == nil { + return fmt.Errorf("nil flavors") } - 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)) + for _, flavor := range flavors { + if flavor.Id == flavorId { + 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)) + } + } } } @@ -96,22 +100,19 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *postgre Details: fmt.Sprintf("You provided storage class '%s', which is invalid.", *storageClass), FlavorId: flavorId, } -} +}*/ -func LoadFlavorId(cpu, ram int64, flavors []postgresflex.Flavor) (*string, error) { +func LoadFlavorId(cpu, ram int64, flavors []postgresflex.ListFlavors) (*string, error) { if flavors == nil { return nil, fmt.Errorf("nil flavors") } availableFlavors := "" for _, f := range flavors { - if f.Id == nil || f.Cpu == nil || f.Memory == nil { - continue - } - if *f.Cpu == cpu && *f.Memory == ram { - return f.Id, nil + if f.Cpu == cpu && f.Memory == ram { + return &f.Id, nil } - availableFlavors = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", availableFlavors, *f.Cpu, *f.Cpu) + availableFlavors = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", availableFlavors, f.Cpu, f.Cpu) } return nil, &errors.DatabaseInvalidFlavorError{ Service: "postgresflex", @@ -119,7 +120,7 @@ func LoadFlavorId(cpu, ram int64, flavors []postgresflex.Flavor) (*string, error } } -func GetLatestPostgreSQLVersion(ctx context.Context, apiClient PostgresFlexClient, projectId, region string) (string, error) { +func GetLatestPostgreSQLVersion(ctx context.Context, apiClient postgresflex.DefaultAPI, projectId, region string) (string, error) { resp, err := apiClient.ListVersions(ctx, projectId, region).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL versions: %w", err) @@ -128,11 +129,11 @@ func GetLatestPostgreSQLVersion(ctx context.Context, apiClient PostgresFlexClien latestVersion := "0" for i := range resp.Versions { oldSemVer := fmt.Sprintf("v%s", latestVersion) - newSemVer := fmt.Sprintf("v%s", resp.Versions[i]) + newSemVer := fmt.Sprintf("v%s", resp.Versions[i].Version) if semver.Compare(newSemVer, oldSemVer) != 1 { continue } - latestVersion = resp.Versions[i] + latestVersion = resp.Versions[i].Version } if latestVersion == "0" { return "", fmt.Errorf("no PostgreSQL versions found") @@ -140,26 +141,18 @@ func GetLatestPostgreSQLVersion(ctx context.Context, apiClient PostgresFlexClien return latestVersion, nil } -func GetInstanceName(ctx context.Context, apiClient PostgresFlexClient, projectId, region, instanceId string) (string, error) { - resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() - if err != nil { - return "", fmt.Errorf("get PostgreSQL Flex instance: %w", err) - } - return *resp.Item.Name, nil -} - -func GetInstanceStatus(ctx context.Context, apiClient PostgresFlexClient, projectId, region, instanceId string) (string, error) { +func GetInstanceName(ctx context.Context, apiClient postgresflex.DefaultAPI, projectId, region, instanceId string) (string, error) { resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL Flex instance: %w", err) } - return *resp.Item.Status, nil + return resp.Name, nil } -func GetUserName(ctx context.Context, apiClient PostgresFlexClient, projectId, region, instanceId, userId string) (string, error) { +func GetUserName(ctx context.Context, apiClient postgresflex.DefaultAPI, projectId, region, instanceId string, userId int64) (string, error) { resp, err := apiClient.GetUser(ctx, projectId, region, instanceId, userId).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL Flex user: %w", err) } - return *resp.Item.Username, nil + return resp.Name, nil } diff --git a/internal/pkg/services/postgresflex/utils/utils_test.go b/internal/pkg/services/postgresflex/utils/utils_test.go index 34a0d4b9c..a95952e11 100644 --- a/internal/pkg/services/postgresflex/utils/utils_test.go +++ b/internal/pkg/services/postgresflex/utils/utils_test.go @@ -9,27 +9,26 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" - postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api" ) var ( testProjectId = uuid.NewString() testInstanceId = uuid.NewString() - testUserId = uuid.NewString() ) const ( testInstanceName = "instance" testUserName = "user" - testStatus = "running" testRegion = "eu01" + testUserId = int64(102) ) type mockSettings struct { listVersionsFails bool listVersionsResp *postgresflex.ListVersionsResponse getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse + getInstanceResp *postgresflex.GetInstanceResponse getUserFails bool getUserResp *postgresflex.GetUserResponse } @@ -42,7 +41,7 @@ func newAPIMockClient(s mockSettings) postgresflex.DefaultAPI { } return s.listVersionsResp, nil }), - GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { + GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.GetInstanceResponse, error) { if s.getInstanceFails { return nil, fmt.Errorf("could not get instance") } @@ -57,128 +56,20 @@ func newAPIMockClient(s mockSettings) postgresflex.DefaultAPI { } } -func TestValidateStorage(t *testing.T) { - tests := []struct { - description string - storageClass *string - storageSize *int64 - storages *postgresflex.ListStoragesResponse - isValid bool - }{ - { - description: "base", - storageClass: utils.Ptr("foo"), - storageSize: utils.Ptr(int64(10)), - storages: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &postgresflex.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: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &postgresflex.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: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &postgresflex.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: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &postgresflex.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: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "foo"}, - StorageRange: &postgresflex.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: &postgresflex.ListStoragesResponse{ - StorageClasses: []string{"bar-1", "bar-2", "bar-3"}, - StorageRange: &postgresflex.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 []postgresflex.Flavor + flavors []postgresflex.ListFlavors isValid bool }{ { description: "base", flavorId: "foo", - flavors: []postgresflex.Flavor{ - {Id: utils.Ptr("bar-1")}, - {Id: utils.Ptr("bar-2")}, - {Id: utils.Ptr("foo")}, + flavors: []postgresflex.ListFlavors{ + {Id: "bar-1"}, + {Id: "bar-2"}, + {Id: "foo"}, }, isValid: true, }, @@ -191,26 +82,16 @@ func TestValidateFlavorId(t *testing.T) { { description: "no flavors", flavorId: "foo", - flavors: []postgresflex.Flavor{}, + flavors: []postgresflex.ListFlavors{}, isValid: false, }, - { - description: "nil flavor id", - flavorId: "foo", - flavors: []postgresflex.Flavor{ - {Id: utils.Ptr("bar-1")}, - {Id: nil}, - {Id: utils.Ptr("foo")}, - }, - isValid: true, - }, { description: "invalid flavor", flavorId: "foo", - flavors: []postgresflex.Flavor{ - {Id: utils.Ptr("bar-1")}, - {Id: utils.Ptr("bar-2")}, - {Id: utils.Ptr("bar-3")}, + flavors: []postgresflex.ListFlavors{ + {Id: "bar-1"}, + {Id: "bar-2"}, + {Id: "bar-3"}, }, isValid: false, }, @@ -234,7 +115,7 @@ func TestLoadFlavorId(t *testing.T) { description string cpu int64 ram int64 - flavors []postgresflex.Flavor + flavors []postgresflex.ListFlavors isValid bool expectedOutput *string }{ @@ -242,21 +123,21 @@ func TestLoadFlavorId(t *testing.T) { description: "base", cpu: 2, ram: 4, - flavors: []postgresflex.Flavor{ + flavors: []postgresflex.ListFlavors{ { - Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Id: "bar-1", + Cpu: int64(2), + Memory: int64(2), }, { - Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Id: "bar-2", + Cpu: int64(4), + Memory: int64(4), }, { - Id: utils.Ptr("foo"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Id: "foo", + Cpu: int64(2), + Memory: int64(4), }, }, isValid: true, @@ -273,70 +154,23 @@ func TestLoadFlavorId(t *testing.T) { description: "no flavors", cpu: 2, ram: 4, - flavors: []postgresflex.Flavor{}, + flavors: []postgresflex.ListFlavors{}, isValid: false, }, - { - description: "flavors with details missing", - cpu: 2, - ram: 4, - flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr("bar-1"), - Cpu: nil, - Memory: nil, - }, - { - Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("foo"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - }, - isValid: true, - expectedOutput: utils.Ptr("foo"), - }, - { - description: "match with nil id", - cpu: 2, - ram: 4, - flavors: []postgresflex.Flavor{ - { - Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), - }, - { - Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: nil, - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - }, - isValid: false, - }, { description: "invalid settings", cpu: 2, ram: 4, - flavors: []postgresflex.Flavor{ + flavors: []postgresflex.ListFlavors{ { - Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Id: "bar-1", + Cpu: int64(2), + Memory: int64(2), }, { - Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Id: "bar-2", + Cpu: int64(4), + Memory: int64(4), }, }, isValid: false, @@ -379,7 +213,17 @@ func TestGetLatestPostgreSQLVersion(t *testing.T) { description: "base", mockClientSettings: mockSettings{ listVersionsResp: &postgresflex.ListVersionsResponse{ - Versions: []string{"8", "10", "9"}, + Versions: []postgresflex.Version{ + { + Version: "8", + }, + { + Version: "10", + }, + { + Version: "9", + }, + }, }, }, isValid: true, @@ -396,7 +240,7 @@ func TestGetLatestPostgreSQLVersion(t *testing.T) { description: "no versions", mockClientSettings: mockSettings{ listVersionsResp: &postgresflex.ListVersionsResponse{ - Versions: []string{}, + Versions: []postgresflex.Version{}, }, }, isValid: false, @@ -433,10 +277,8 @@ func TestGetInstanceName(t *testing.T) { { description: "base", mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Name: utils.Ptr(testInstanceName), - }, + getInstanceResp: &postgresflex.GetInstanceResponse{ + Name: testInstanceName, }, }, isValid: true, @@ -471,54 +313,6 @@ func TestGetInstanceName(t *testing.T) { } } -func TestGetInstanceStatus(t *testing.T) { - tests := []struct { - description string - mockClientSettings mockSettings - isValid bool - expectedOutput string - }{ - { - description: "base", - mockClientSettings: mockSettings{ - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(testStatus), - }, - }, - }, - isValid: true, - expectedOutput: testStatus, - }, - { - description: "get instance fails", - mockClientSettings: mockSettings{ - getInstanceFails: true, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := GetInstanceStatus(context.Background(), newAPIMockClient(tt.mockClientSettings), testProjectId, testRegion, testInstanceId) - - if tt.isValid && err != nil { - t.Errorf("failed on valid input") - } - if !tt.isValid && err == nil { - t.Errorf("did not fail on invalid input") - } - if !tt.isValid { - return - } - if output != tt.expectedOutput { - t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) - } - }) - } -} - func TestGetUserName(t *testing.T) { tests := []struct { description string @@ -530,9 +324,7 @@ func TestGetUserName(t *testing.T) { description: "base", mockClientSettings: mockSettings{ getUserResp: &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{ - Username: utils.Ptr(testUserName), - }, + Name: testUserName, }, }, isValid: true,