mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## stackit beta server backup disable | ||
| Disables Server Backup service | ||
| ### Synopsis | ||
| Disables Server Backup service. | ||
| ``` | ||
| stackit beta server backup disable [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Disable Server Backup functionality for your server. | ||
| $ stackit beta server backup disable --server-id=zzz | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup disable" | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup | ||
| ## stackit beta server backup enable | ||
| Enables Server Backup service | ||
| ### Synopsis | ||
| Enables Server Backup service. | ||
| ``` | ||
| stackit beta server backup enable [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Enable Server Backup functionality for your server | ||
| $ stackit beta server backup enable --server-id=zzz | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup enable" | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup | ||
| ## stackit beta server backup schedule create | ||
| Creates a Server Backup Schedule | ||
| ### Synopsis | ||
| Creates a Server Backup Schedule. | ||
| ``` | ||
| stackit beta server backup schedule create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a Server Backup Schedule with name "myschedule" and backup name "mybackup" | ||
| $ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule | ||
| Create a Server Backup Schedule with name "myschedule", backup name "mybackup" and retention period of 5 days | ||
| $ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule --backup-retention-period=5 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -b, --backup-name string Backup name | ||
| -d, --backup-retention-period int Backup retention period (in days) (default 14) | ||
| -n, --backup-schedule-name string Backup schedule name | ||
| -i, --backup-volume-ids string Backup volume ids, as comma separated UUID values. | ||
| -e, --enabled Is the server backup schedule enabled (default true) | ||
| -h, --help Help for "stackit beta server backup schedule create" | ||
| -r, --rrule string Backup RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule | ||
| ## stackit beta server backup schedule delete | ||
| Deletes a Server Backup Schedule | ||
| ### Synopsis | ||
| Deletes a Server Backup Schedule. | ||
| ``` | ||
| stackit beta server backup schedule delete SCHEDULE_ID [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete a Server Backup Schedule with ID "xxx" for server "zzz" | ||
| $ stackit beta server backup schedule delete xxx --server-id=zzz | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup schedule delete" | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule | ||
| ## stackit beta server backup schedule describe | ||
| Shows details of a Server Backup Schedule | ||
| ### Synopsis | ||
| Shows details of a Server Backup Schedule. | ||
| ``` | ||
| stackit beta server backup schedule describe BACKUP_SCHEDULE_ID [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get details of a Server Backup Schedule with id "my-schedule-id" | ||
| $ stackit beta server backup schedule describe my-schedule-id | ||
| Get details of a Server Backup Schedule with id "my-schedule-id" in JSON format | ||
| $ stackit beta server backup schedule describe my-schedule-id --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup schedule describe" | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule | ||
| ## stackit beta server backup schedule list | ||
| Lists all server backup schedules | ||
| ### Synopsis | ||
| Lists all server backup schedules. | ||
| ``` | ||
| stackit beta server backup schedule list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all backup schedules for a server with ID "xxx" | ||
| $ stackit beta server backup schedule list --server-id xxx | ||
| List all backup schedules for a server with ID "xxx" in JSON format | ||
| $ stackit beta server backup schedule list --server-id xxx --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup schedule list" | ||
| --limit int Maximum number of entries to list | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule | ||
| ## stackit beta server backup schedule update | ||
| Updates a Server Backup Schedule | ||
| ### Synopsis | ||
| Updates a Server Backup Schedule. | ||
| ``` | ||
| stackit beta server backup schedule update SCHEDULE_ID [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update the retention period of the backup schedule "zzz" of server "xxx" | ||
| $ stackit beta server backup schedule update zzz --server-id=xxx --backup-retention-period=20 | ||
| Update the backup name of the backup schedule "zzz" of server "xxx" | ||
| $ stackit beta server backup schedule update zzz --server-id=xxx --backup-name=newname | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -b, --backup-name string Backup name | ||
| -d, --backup-retention-period int Backup retention period (in days) (default 14) | ||
| -n, --backup-schedule-name string Backup schedule name | ||
| -i, --backup-volume-ids string Backup volume ids, as comma separated UUID values. | ||
| -e, --enabled Is the server backup schedule enabled (default true) | ||
| -h, --help Help for "stackit beta server backup schedule update" | ||
| -r, --rrule string Backup RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule | ||
| ## stackit beta server backup schedule | ||
| Provides functionality for Server Backup Schedule | ||
| ### Synopsis | ||
| Provides functionality for Server Backup Schedule. | ||
| ``` | ||
| stackit beta server backup schedule [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup schedule" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup | ||
| * [stackit beta server backup schedule create](./stackit_beta_server_backup_schedule_create.md) - Creates a Server Backup Schedule | ||
| * [stackit beta server backup schedule delete](./stackit_beta_server_backup_schedule_delete.md) - Deletes a Server Backup Schedule | ||
| * [stackit beta server backup schedule describe](./stackit_beta_server_backup_schedule_describe.md) - Shows details of a Server Backup Schedule | ||
| * [stackit beta server backup schedule list](./stackit_beta_server_backup_schedule_list.md) - Lists all server backup schedules | ||
| * [stackit beta server backup schedule update](./stackit_beta_server_backup_schedule_update.md) - Updates a Server Backup Schedule | ||
| ## stackit beta server backup | ||
| Provides functionality for Server Backup | ||
| ### Synopsis | ||
| Provides functionality for Server Backup. | ||
| ``` | ||
| stackit beta server backup [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server backup" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for Server | ||
| * [stackit beta server backup disable](./stackit_beta_server_backup_disable.md) - Disables Server Backup service | ||
| * [stackit beta server backup enable](./stackit_beta_server_backup_enable.md) - Enables Server Backup service | ||
| * [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule | ||
| ## stackit beta server | ||
| Provides functionality for Server | ||
| ### Synopsis | ||
| Provides functionality for Server. | ||
| ``` | ||
| stackit beta server [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands | ||
| * [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup | ||
| ## stackit beta sqlserverflex database create | ||
| Creates a SQLServer Flex database | ||
| ### Synopsis | ||
| Creates a SQLServer Flex database. | ||
| This operation cannot be triggered asynchronously (the "--async" flag will have no effect). | ||
| ``` | ||
| stackit beta sqlserverflex database create DATABASE_NAME [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a SQLServer Flex database with name "my-database" on instance with ID "xxx" | ||
| $ stackit beta sqlserverflex database create my-database --instance-id xxx --owner some-username | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta sqlserverflex database create" | ||
| --instance-id string SQLServer Flex instance ID | ||
| --owner string Username of the owner user | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases | ||
| ## stackit beta sqlserverflex database delete | ||
| Deletes a SQLServer Flex database | ||
| ### Synopsis | ||
| Deletes a SQLServer Flex database. | ||
| This operation cannot be triggered asynchronously (the "--async" flag will have no effect). | ||
| ``` | ||
| stackit beta sqlserverflex database delete DATABASE_NAME [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete a SQLServer Flex database with name "my-database" of instance with ID "xxx" | ||
| $ stackit beta sqlserverflex database delete my-database --instance-id xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta sqlserverflex database delete" | ||
| --instance-id string SQLServer Flex instance ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases | ||
| ## stackit beta sqlserverflex database | ||
| Provides functionality for SQLServer Flex databases | ||
| ### Synopsis | ||
| Provides functionality for SQLServer Flex databases. | ||
| ``` | ||
| stackit beta sqlserverflex database [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta sqlserverflex database" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex | ||
| * [stackit beta sqlserverflex database create](./stackit_beta_sqlserverflex_database_create.md) - Creates a SQLServer Flex database | ||
| * [stackit beta sqlserverflex database delete](./stackit_beta_sqlserverflex_database_delete.md) - Deletes a SQLServer Flex database | ||
| ## stackit beta sqlserverflex user delete | ||
| Deletes a SQLServer Flex user | ||
| ### Synopsis | ||
| Deletes a SQLServer Flex user by ID. You can get the IDs of users for an instance by running: | ||
| $ stackit beta sqlserverflex user list --instance-id <INSTANCE_ID> | ||
| ``` | ||
| stackit beta sqlserverflex user delete USER_ID [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete a SQLServer Flex user with ID "xxx" for instance with ID "yyy" | ||
| $ stackit beta sqlserverflex user delete xxx --instance-id yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta sqlserverflex user delete" | ||
| --instance-id string Instance ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users | ||
| ## stackit beta sqlserverflex user describe | ||
| Shows details of a SQLServer Flex user | ||
| ### Synopsis | ||
| Shows details of a SQLServer Flex user. | ||
| The user password is only visible upon creation. You can reset it by running: | ||
| $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID | ||
| ``` | ||
| stackit beta sqlserverflex user describe USER_ID [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy" | ||
| $ stackit beta sqlserverflex user describe xxx --instance-id yyy | ||
| Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy" in JSON format | ||
| $ stackit beta sqlserverflex user describe xxx --instance-id yyy --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta sqlserverflex user describe" | ||
| --instance-id string ID of the instance | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users | ||
| ## stackit beta sqlserverflex user list | ||
| Lists all SQLServer Flex users of an instance | ||
| ### Synopsis | ||
| Lists all SQLServer Flex users of an instance. | ||
| ``` | ||
| stackit beta sqlserverflex user list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all SQLServer Flex users of instance with ID "xxx" | ||
| $ stackit beta sqlserverflex user list --instance-id xxx | ||
| List all SQLServer Flex users of instance with ID "xxx" in JSON format | ||
| $ stackit beta sqlserverflex user list --instance-id xxx --output-format json | ||
| List up to 10 SQLServer Flex users of instance with ID "xxx" | ||
| $ stackit beta sqlserverflex user list --instance-id xxx --limit 10 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta sqlserverflex user list" | ||
| --instance-id string Instance ID | ||
| --limit int Maximum number of entries to list | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users | ||
| package backup | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/disable" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/enable" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "backup", | ||
| Short: "Provides functionality for Server Backup", | ||
| Long: "Provides functionality for Server Backup.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(enable.NewCmd(p)) | ||
| cmd.AddCommand(disable.NewCmd(p)) | ||
| cmd.AddCommand(schedule.NewCmd(p)) | ||
| } |
| package disable | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiDisableServiceRequest)) serverbackup.ApiDisableServiceRequest { | ||
| request := testClient.DisableService(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.ServerId = "" | ||
| }), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| cmd := &cobra.Command{} | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| p := print.NewPrinter() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest serverbackup.ApiDisableServiceRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package disable | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| serverbackupUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "disable", | ||
| Short: "Disables Server Backup service", | ||
| Long: "Disables Server Backup service.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Disable Server Backup functionality for your server.`, | ||
| "$ stackit beta server backup disable --server-id=zzz"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| canDisable, err := serverbackupUtils.CanDisableBackupService(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !canDisable { | ||
| p.Info("Cannot disable backup service for server %s - existing backups or existing backup schedules found\n", model.ServerId) | ||
| return nil | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable the backup service for server %s?", model.ServerId) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("disable server backup service: %w", err) | ||
| } | ||
| p.Info("Disabled Server Backup service for server %s\n", model.ServerId) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiDisableServiceRequest { | ||
| req := apiClient.DisableService(ctx, model.ProjectId, model.ServerId) | ||
| return req | ||
| } |
| package enable | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiEnableServiceRequest)) serverbackup.ApiEnableServiceRequest { | ||
| request := testClient.EnableService(testCtx, testProjectId, testServerId).EnableServicePayload(serverbackup.EnableServicePayload{}) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.ServerId = "" | ||
| }), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| cmd := &cobra.Command{} | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| p := print.NewPrinter() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest serverbackup.ApiEnableServiceRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package enable | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "enable", | ||
| Short: "Enables Server Backup service", | ||
| Long: "Enables Server Backup service.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Enable Server Backup functionality for your server`, | ||
| "$ stackit beta server backup enable --server-id=zzz"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable the Server Backup service for server %s?", model.ServerId) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| if !strings.Contains(err.Error(), "Tried to activate already active service") { | ||
| return fmt.Errorf("enable Server Backup: %w", err) | ||
| } | ||
| } | ||
| p.Info("Enabled backup service for server %s\n", model.ServerId) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiEnableServiceRequest { | ||
| payload := serverbackup.EnableServicePayload{} | ||
| req := apiClient.EnableService(ctx, model.ProjectId, model.ServerId).EnableServicePayload(payload) | ||
| return req | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "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" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| backupScheduleNameFlag: "example-backup-schedule-name", | ||
| enabledFlag: "true", | ||
| rruleFlag: defaultRrule, | ||
| backupNameFlag: "example-backup-name", | ||
| backupRetentionPeriodFlag: "14", | ||
| backupVolumeIdsFlag: defaultVolumeIds, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ServerId: testServerId, | ||
| BackupScheduleName: "example-backup-schedule-name", | ||
| Enabled: defaultEnabled, | ||
| Rrule: defaultRrule, | ||
| BackupName: "example-backup-name", | ||
| BackupRetentionPeriod: int64(14), | ||
| BackupVolumeIds: defaultVolumeIds, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiCreateBackupScheduleRequest)) serverbackup.ApiCreateBackupScheduleRequest { | ||
| request := testClient.CreateBackupSchedule(testCtx, testProjectId, testServerId) | ||
| request = request.CreateBackupSchedulePayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixturePayload(mods ...func(payload *serverbackup.CreateBackupSchedulePayload)) serverbackup.CreateBackupSchedulePayload { | ||
| payload := serverbackup.CreateBackupSchedulePayload{ | ||
| Name: utils.Ptr("example-backup-schedule-name"), | ||
| Enabled: utils.Ptr(defaultEnabled), | ||
| Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), | ||
| BackupProperties: &serverbackup.BackupProperties{ | ||
| Name: utils.Ptr("example-backup-name"), | ||
| RetentionPeriod: utils.Ptr(int64(14)), | ||
| VolumeIds: nil, | ||
| }, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| aclValues []string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "with defaults", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, backupRetentionPeriodFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest serverbackup.ApiCreateBackupScheduleRequest | ||
| isValid bool | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| isValid: true, | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request, err := buildRequest(testCtx, tt.model, testClient) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error building request: %v", err) | ||
| } | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| backupScheduleNameFlag = "backup-schedule-name" | ||
| enabledFlag = "enabled" | ||
| rruleFlag = "rrule" | ||
| backupNameFlag = "backup-name" | ||
| backupVolumeIdsFlag = "backup-volume-ids" | ||
| backupRetentionPeriodFlag = "backup-retention-period" | ||
| serverIdFlag = "server-id" | ||
| defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1" | ||
| defaultRetentionPeriod = 14 | ||
| defaultEnabled = true | ||
| defaultVolumeIds = "" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| BackupScheduleName string | ||
| Enabled bool | ||
| Rrule string | ||
| BackupName string | ||
| BackupRetentionPeriod int64 | ||
| BackupVolumeIds string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates a Server Backup Schedule", | ||
| Long: "Creates a Server Backup Schedule.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create a Server Backup Schedule with name "myschedule" and backup name "mybackup"`, | ||
| `$ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule`), | ||
| examples.NewExample( | ||
| `Create a Server Backup Schedule with name "myschedule", backup name "mybackup" and retention period of 5 days`, | ||
| `$ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule --backup-retention-period=5`), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Backup Schedule for server %s?", model.ServerId) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("create Server Backup Schedule: %w", err) | ||
| } | ||
| return outputResult(p, model, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| cmd.Flags().StringP(backupScheduleNameFlag, "n", "", "Backup schedule name") | ||
| cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name") | ||
| cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") | ||
| cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server backup schedule enabled") | ||
| cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "Backup RRULE (recurrence rule)") | ||
| cmd.Flags().StringP(backupVolumeIdsFlag, "i", defaultVolumeIds, "Backup volume ids, as comma separated UUID values.") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag, backupScheduleNameFlag, backupNameFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| BackupRetentionPeriod: flags.FlagWithDefaultToInt64Value(p, cmd, backupRetentionPeriodFlag), | ||
| BackupScheduleName: flags.FlagToStringValue(p, cmd, backupScheduleNameFlag), | ||
| BackupName: flags.FlagToStringValue(p, cmd, backupNameFlag), | ||
| Rrule: flags.FlagWithDefaultToStringValue(p, cmd, rruleFlag), | ||
| Enabled: flags.FlagToBoolValue(p, cmd, enabledFlag), | ||
| BackupVolumeIds: flags.FlagToStringValue(p, cmd, backupVolumeIdsFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) (serverbackup.ApiCreateBackupScheduleRequest, error) { | ||
| req := apiClient.CreateBackupSchedule(ctx, model.ProjectId, model.ServerId) | ||
| backupProperties := serverbackup.BackupProperties{ | ||
| Name: &model.BackupName, | ||
| RetentionPeriod: &model.BackupRetentionPeriod, | ||
| } | ||
| if model.BackupVolumeIds == "" { | ||
| backupProperties.VolumeIds = nil | ||
| } else { | ||
| ids := strings.Split(model.BackupVolumeIds, ",") | ||
| backupProperties.VolumeIds = &ids | ||
| } | ||
| req = req.CreateBackupSchedulePayload(serverbackup.CreateBackupSchedulePayload{ | ||
| Enabled: &model.Enabled, | ||
| Name: &model.BackupScheduleName, | ||
| Rrule: &model.Rrule, | ||
| BackupProperties: &backupProperties, | ||
| }) | ||
| return req, nil | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, resp *serverbackup.BackupSchedule) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal server backup schedule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal server backup schedule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Outputf("Created server backup schedule for server %s. Backup Schedule ID: %d\n", model.ServerId, *resp.Id) | ||
| return nil | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testBackupScheduleId = "5" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testBackupScheduleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ServerId: testServerId, | ||
| ScheduleId: testBackupScheduleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiDeleteBackupScheduleRequest)) serverbackup.ApiDeleteBackupScheduleRequest { | ||
| request := testClient.DeleteBackupSchedule(testCtx, testProjectId, testServerId, testBackupScheduleId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest serverbackup.ApiDeleteBackupScheduleRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| scheduleIdArg = "SCHEDULE_ID" | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ScheduleId string | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("delete %s", scheduleIdArg), | ||
| Short: "Deletes a Server Backup Schedule", | ||
| Long: "Deletes a Server Backup Schedule.", | ||
| Args: args.SingleArg(scheduleIdArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete a Server Backup Schedule with ID "xxx" for server "zzz"`, | ||
| "$ stackit beta server backup schedule delete xxx --server-id=zzz"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server backup schedule %q? (This cannot be undone)", model.ScheduleId) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("delete Server Backup Schedule: %w", err) | ||
| } | ||
| p.Info("Deleted server backup schedule %q\n", model.ScheduleId) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| scheduleId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ScheduleId: scheduleId, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiDeleteBackupScheduleRequest { | ||
| req := apiClient.DeleteBackupSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId) | ||
| return req | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testBackupScheduleId = "5" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testBackupScheduleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ServerId: testServerId, | ||
| BackupScheduleId: testBackupScheduleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiGetBackupScheduleRequest)) serverbackup.ApiGetBackupScheduleRequest { | ||
| request := testClient.GetBackupSchedule(testCtx, testProjectId, testServerId, testBackupScheduleId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| isValid bool | ||
| expectedRequest serverbackup.ApiGetBackupScheduleRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| isValid: true, | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| backupScheduleIdArg = "BACKUP_SCHEDULE_ID" | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| BackupScheduleId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("describe %s", backupScheduleIdArg), | ||
| Short: "Shows details of a Server Backup Schedule", | ||
| Long: "Shows details of a Server Backup Schedule.", | ||
| Args: args.SingleArg(backupScheduleIdArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get details of a Server Backup Schedule with id "my-schedule-id"`, | ||
| "$ stackit beta server backup schedule describe my-schedule-id"), | ||
| examples.NewExample( | ||
| `Get details of a Server Backup Schedule with id "my-schedule-id" in JSON format`, | ||
| "$ stackit beta server backup schedule describe my-schedule-id --output-format json"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("read server backup schedule: %w", err) | ||
| } | ||
| return outputResult(p, model.OutputFormat, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| backupScheduleId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| BackupScheduleId: backupScheduleId, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiGetBackupScheduleRequest { | ||
| req := apiClient.GetBackupSchedule(ctx, model.ProjectId, model.ServerId, model.BackupScheduleId) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, schedule *serverbackup.BackupSchedule) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(schedule, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal server backup schedule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(schedule, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal server backup schedule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.AddRow("SCHEDULE ID", *schedule.Id) | ||
| table.AddSeparator() | ||
| table.AddRow("SCHEDULE NAME", *schedule.Name) | ||
| table.AddSeparator() | ||
| table.AddRow("ENABLED", *schedule.Enabled) | ||
| table.AddSeparator() | ||
| table.AddRow("RRULE", *schedule.Rrule) | ||
| table.AddSeparator() | ||
| if schedule.BackupProperties != nil { | ||
| table.AddRow("BACKUP NAME", *schedule.BackupProperties.Name) | ||
| table.AddSeparator() | ||
| table.AddRow("BACKUP RETENTION DAYS", *schedule.BackupProperties.RetentionPeriod) | ||
| table.AddSeparator() | ||
| ids := schedule.BackupProperties.VolumeIds | ||
| if ids == nil || len(*ids) == 0 { | ||
| table.AddRow("BACKUP VOLUME IDS", "") | ||
| } else { | ||
| table.AddRow("BACKUP VOLUME IDS", strings.Join(*ids, "\n")) | ||
| } | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "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" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| limitFlag: "10", | ||
| serverIdFlag: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Limit: utils.Ptr(int64(10)), | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiListBackupSchedulesRequest)) serverbackup.ApiListBackupSchedulesRequest { | ||
| request := testClient.ListBackupSchedules(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest serverbackup.ApiListBackupSchedulesRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| limitFlag = "limit" | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| Limit *int64 | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists all server backup schedules", | ||
| Long: "Lists all server backup schedules.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all backup schedules for a server with ID "xxx"`, | ||
| "$ stackit beta server backup schedule list --server-id xxx"), | ||
| examples.NewExample( | ||
| `List all backup schedules for a server with ID "xxx" in JSON format`, | ||
| "$ stackit beta server backup schedule list --server-id xxx --output-format json"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list server backup schedules: %w", err) | ||
| } | ||
| schedules := *resp.Items | ||
| if len(schedules) == 0 { | ||
| p.Info("No backup schedules found for server %s\n", model.ServerId) | ||
| return nil | ||
| } | ||
| // Truncate output | ||
| if model.Limit != nil && len(schedules) > int(*model.Limit) { | ||
| schedules = schedules[:*model.Limit] | ||
| } | ||
| return outputResult(p, model.OutputFormat, schedules) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| Limit: limit, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiListBackupSchedulesRequest { | ||
| req := apiClient.ListBackupSchedules(ctx, model.ProjectId, model.ServerId) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, schedules []serverbackup.BackupSchedule) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(schedules, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal Server Backup Schedules list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(schedules, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal Server Backup Schedules list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("SCHEDULE ID", "SCHEDULE NAME", "ENABLED", "RRULE", "BACKUP NAME", "BACKUP RETENTION DAYS", "BACKUP VOLUME IDS") | ||
| for i := range schedules { | ||
| s := schedules[i] | ||
| ids := "" | ||
| if s.BackupProperties.VolumeIds != nil && len(*s.BackupProperties.VolumeIds) != 0 { | ||
| ids = strings.Join(*s.BackupProperties.VolumeIds, ",") | ||
| } | ||
| table.AddRow(*s.Id, *s.Name, *s.Enabled, *s.Rrule, *s.BackupProperties.Name, *s.BackupProperties.RetentionPeriod, ids) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package schedule | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/create" | ||
| del "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/update" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "schedule", | ||
| Short: "Provides functionality for Server Backup Schedule", | ||
| Long: "Provides functionality for Server Backup Schedule.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| cmd.AddCommand(describe.NewCmd(p)) | ||
| cmd.AddCommand(create.NewCmd(p)) | ||
| cmd.AddCommand(del.NewCmd(p)) | ||
| cmd.AddCommand(update.NewCmd(p)) | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "strconv" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "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" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &serverbackup.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testBackupScheduleId = "5" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testBackupScheduleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| backupScheduleNameFlag: "example-backup-schedule-name", | ||
| enabledFlag: "true", | ||
| rruleFlag: defaultRrule, | ||
| backupNameFlag: "example-backup-name", | ||
| backupRetentionPeriodFlag: "14", | ||
| backupVolumeIdsFlag: defaultVolumeIds, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| BackupScheduleId: testBackupScheduleId, | ||
| ServerId: testServerId, | ||
| BackupScheduleName: utils.Ptr("example-backup-schedule-name"), | ||
| Enabled: utils.Ptr(defaultEnabled), | ||
| Rrule: utils.Ptr(defaultRrule), | ||
| BackupName: utils.Ptr("example-backup-name"), | ||
| BackupRetentionPeriod: utils.Ptr(int64(14)), | ||
| BackupVolumeIds: utils.Ptr(defaultVolumeIds), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureBackupSchedule(mods ...func(schedule *serverbackup.BackupSchedule)) *serverbackup.BackupSchedule { | ||
| id, _ := strconv.ParseInt(testBackupScheduleId, 10, 64) | ||
| schedule := &serverbackup.BackupSchedule{ | ||
| Name: utils.Ptr("example-backup-schedule-name"), | ||
| Id: utils.Ptr(id), | ||
| Enabled: utils.Ptr(defaultEnabled), | ||
| Rrule: utils.Ptr(defaultRrule), | ||
| BackupProperties: &serverbackup.BackupProperties{ | ||
| Name: utils.Ptr("example-backup-name"), | ||
| RetentionPeriod: utils.Ptr(int64(14)), | ||
| VolumeIds: nil, | ||
| }, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(schedule) | ||
| } | ||
| return schedule | ||
| } | ||
| func fixturePayload(mods ...func(payload *serverbackup.UpdateBackupSchedulePayload)) serverbackup.UpdateBackupSchedulePayload { | ||
| payload := serverbackup.UpdateBackupSchedulePayload{ | ||
| Name: utils.Ptr("example-backup-schedule-name"), | ||
| Enabled: utils.Ptr(defaultEnabled), | ||
| Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), | ||
| BackupProperties: &serverbackup.BackupProperties{ | ||
| Name: utils.Ptr("example-backup-name"), | ||
| RetentionPeriod: utils.Ptr(int64(14)), | ||
| VolumeIds: nil, | ||
| }, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func fixtureRequest(mods ...func(request *serverbackup.ApiUpdateBackupScheduleRequest)) serverbackup.ApiUpdateBackupScheduleRequest { | ||
| request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testServerId, testBackupScheduleId) | ||
| request = request.UpdateBackupSchedulePayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "backup schedule id invalid 1", | ||
| argValues: []string{""}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| err = cmd.ValidateFlagGroups() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flag groups: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest serverbackup.ApiUpdateBackupScheduleRequest | ||
| isValid bool | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| isValid: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request, err := buildRequest(testCtx, tt.model, testClient, *fixtureBackupSchedule()) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error building request: %v", err) | ||
| } | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| const ( | ||
| scheduleIdArg = "SCHEDULE_ID" | ||
| backupScheduleNameFlag = "backup-schedule-name" | ||
| enabledFlag = "enabled" | ||
| rruleFlag = "rrule" | ||
| backupNameFlag = "backup-name" | ||
| backupVolumeIdsFlag = "backup-volume-ids" | ||
| backupRetentionPeriodFlag = "backup-retention-period" | ||
| serverIdFlag = "server-id" | ||
| defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1" | ||
| defaultRetentionPeriod = 14 | ||
| defaultEnabled = true | ||
| defaultVolumeIds = "" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| BackupScheduleId string | ||
| BackupScheduleName *string | ||
| Enabled *bool | ||
| Rrule *string | ||
| BackupName *string | ||
| BackupRetentionPeriod *int64 | ||
| BackupVolumeIds *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("update %s", scheduleIdArg), | ||
| Short: "Updates a Server Backup Schedule", | ||
| Long: "Updates a Server Backup Schedule.", | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Update the retention period of the backup schedule "zzz" of server "xxx"`, | ||
| "$ stackit beta server backup schedule update zzz --server-id=xxx --backup-retention-period=20"), | ||
| examples.NewExample( | ||
| `Update the backup name of the backup schedule "zzz" of server "xxx"`, | ||
| "$ stackit beta server backup schedule update zzz --server-id=xxx --backup-name=newname"), | ||
| ), | ||
| Args: args.SingleArg(scheduleIdArg, nil), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| currentBackupSchedule, err := apiClient.GetBackupScheduleExecute(ctx, model.ProjectId, model.ServerId, model.BackupScheduleId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get current server backup schedule: %v", err) | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update Server Backup Schedule %q?", model.BackupScheduleId) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req, err := buildRequest(ctx, model, apiClient, *currentBackupSchedule) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("update Server Backup Schedule: %w", err) | ||
| } | ||
| return outputResult(p, model, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| cmd.Flags().StringP(backupScheduleNameFlag, "n", "", "Backup schedule name") | ||
| cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name") | ||
| cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") | ||
| cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server backup schedule enabled") | ||
| cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "Backup RRULE (recurrence rule)") | ||
| cmd.Flags().StringP(backupVolumeIdsFlag, "i", defaultVolumeIds, "Backup volume ids, as comma separated UUID values.") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| scheduleId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| BackupScheduleId: scheduleId, | ||
| ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), | ||
| BackupRetentionPeriod: flags.FlagToInt64Pointer(p, cmd, backupRetentionPeriodFlag), | ||
| BackupScheduleName: flags.FlagToStringPointer(p, cmd, backupScheduleNameFlag), | ||
| BackupName: flags.FlagToStringPointer(p, cmd, backupNameFlag), | ||
| Rrule: flags.FlagToStringPointer(p, cmd, rruleFlag), | ||
| Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag), | ||
| BackupVolumeIds: flags.FlagToStringPointer(p, cmd, backupVolumeIdsFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient, old serverbackup.BackupSchedule) (serverbackup.ApiUpdateBackupScheduleRequest, error) { | ||
| req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, model.ServerId, model.BackupScheduleId) | ||
| if model.BackupName != nil { | ||
| old.BackupProperties.Name = model.BackupName | ||
| } | ||
| if model.BackupRetentionPeriod != nil { | ||
| old.BackupProperties.RetentionPeriod = model.BackupRetentionPeriod | ||
| } | ||
| if model.BackupVolumeIds != nil { | ||
| if *model.BackupVolumeIds == "" { | ||
| old.BackupProperties.VolumeIds = nil | ||
| } else { | ||
| ids := strings.Split(*model.BackupVolumeIds, ",") | ||
| old.BackupProperties.VolumeIds = &ids | ||
| } | ||
| } | ||
| if model.Enabled != nil { | ||
| old.Enabled = model.Enabled | ||
| } | ||
| if model.BackupScheduleName != nil { | ||
| old.Name = model.BackupScheduleName | ||
| } | ||
| if model.Rrule != nil { | ||
| old.Rrule = model.Rrule | ||
| } | ||
| req = req.UpdateBackupSchedulePayload(serverbackup.UpdateBackupSchedulePayload{ | ||
| Enabled: old.Enabled, | ||
| Name: old.Name, | ||
| Rrule: old.Rrule, | ||
| BackupProperties: old.BackupProperties, | ||
| }) | ||
| return req, nil | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, resp *serverbackup.BackupSchedule) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal update server backup schedule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal update server backup schedule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Info("Updated server backup schedule %d\n", *resp.Id) | ||
| return nil | ||
| } | ||
| } |
| package server | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "server", | ||
| Short: "Provides functionality for Server", | ||
| Long: "Provides functionality for Server.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(backup.NewCmd(p)) | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &sqlserverflex.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() | ||
| var testDatabaseName = "my-database" | ||
| var testOwner = "owner" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testDatabaseName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| instanceIdFlag: testInstanceId, | ||
| ownerFlag: testOwner, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| DatabaseName: testDatabaseName, | ||
| InstanceId: testInstanceId, | ||
| Owner: testOwner, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateDatabaseRequest)) sqlserverflex.ApiCreateDatabaseRequest { | ||
| request := testClient.CreateDatabase(testCtx, testProjectId, testInstanceId) | ||
| payload := sqlserverflex.CreateDatabasePayload{ | ||
| Name: &testDatabaseName, | ||
| Options: utils.Ptr(map[string]string{ | ||
| "owner": testOwner, | ||
| }), | ||
| } | ||
| request = request.CreateDatabasePayload(payload) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, instanceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "owner missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, ownerFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "database name invalid 1", | ||
| argValues: []string{""}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest sqlserverflex.ApiCreateDatabaseRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| databaseNameArg = "DATABASE_NAME" | ||
| instanceIdFlag = "instance-id" | ||
| ownerFlag = "owner" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| DatabaseName string | ||
| InstanceId string | ||
| Owner string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("create %s", databaseNameArg), | ||
| Short: "Creates a SQLServer Flex database", | ||
| Long: fmt.Sprintf("%s\n%s", | ||
| "Creates a SQLServer Flex database.", | ||
| `This operation cannot be triggered asynchronously (the "--async" flag will have no effect).`, | ||
| ), | ||
| Args: args.SingleArg(databaseNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create a SQLServer Flex database with name "my-database" on instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex database create my-database --instance-id xxx --owner some-username"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create database %q? (This cannot be undone)", model.DatabaseName) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| s := spinner.New(p) | ||
| s.Start("Creating database") | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| s.StopWithError() | ||
| return fmt.Errorf("create SQLServer Flex database: %w", err) | ||
| } | ||
| s.Stop() | ||
| return outputResult(p, model, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "SQLServer Flex instance ID") | ||
| cmd.Flags().String(ownerFlag, "", "Username of the owner user") | ||
| err := flags.MarkFlagsRequired(cmd, instanceIdFlag, ownerFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| databaseName := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| DatabaseName: databaseName, | ||
| InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), | ||
| Owner: flags.FlagToStringValue(p, cmd, ownerFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiCreateDatabaseRequest { | ||
| req := apiClient.CreateDatabase(ctx, model.ProjectId, model.InstanceId) | ||
| payload := sqlserverflex.CreateDatabasePayload{ | ||
| Name: &model.DatabaseName, | ||
| Options: utils.Ptr(map[string]string{ | ||
| "owner": model.Owner, | ||
| }), | ||
| } | ||
| req = req.CreateDatabasePayload(payload) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, resp *sqlserverflex.CreateDatabaseResponse) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal SQLServer Flex database: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal SQLServer Flex database: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Outputf("Created database %q\n", model.DatabaseName) | ||
| return nil | ||
| } | ||
| } |
| package database | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "database", | ||
| Short: "Provides functionality for SQLServer Flex databases", | ||
| Long: "Provides functionality for SQLServer Flex databases.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(create.NewCmd(p)) | ||
| cmd.AddCommand(delete.NewCmd(p)) | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &sqlserverflex.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() | ||
| var testDatabaseName = "my-database" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testDatabaseName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| instanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| DatabaseName: testDatabaseName, | ||
| InstanceId: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteDatabaseRequest)) sqlserverflex.ApiDeleteDatabaseRequest { | ||
| request := testClient.DeleteDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, instanceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "database name invalid 1", | ||
| argValues: []string{""}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest sqlserverflex.ApiDeleteDatabaseRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| const ( | ||
| databaseNameArg = "DATABASE_NAME" | ||
| instanceIdFlag = "instance-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| DatabaseName string | ||
| InstanceId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("delete %s", databaseNameArg), | ||
| Short: "Deletes a SQLServer Flex database", | ||
| Long: fmt.Sprintf("%s\n%s", | ||
| "Deletes a SQLServer Flex database.", | ||
| `This operation cannot be triggered asynchronously (the "--async" flag will have no effect).`, | ||
| ), | ||
| Args: args.SingleArg(databaseNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete a SQLServer Flex database with name "my-database" of instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex database delete my-database --instance-id xxx"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete database %q? (This cannot be undone)", model.DatabaseName) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| s := spinner.New(p) | ||
| s.Start("Deleting database") | ||
| err = req.Execute() | ||
| if err != nil { | ||
| s.StopWithError() | ||
| return fmt.Errorf("delete SQLServer Flex database: %w", err) | ||
| } | ||
| s.Stop() | ||
| p.Info("Deleted database %q\n", model.DatabaseName) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "SQLServer Flex instance ID") | ||
| err := flags.MarkFlagsRequired(cmd, instanceIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| databaseName := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| DatabaseName: databaseName, | ||
| InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiDeleteDatabaseRequest { | ||
| req := apiClient.DeleteDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName) | ||
| return req | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &sqlserverflex.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() | ||
| var testUserId = "my-user-id" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testUserId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| instanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| InstanceId: testInstanceId, | ||
| UserId: testUserId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteUserRequest)) sqlserverflex.ApiDeleteUserRequest { | ||
| request := testClient.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, instanceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest sqlserverflex.ApiDeleteUserRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" | ||
| sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| const ( | ||
| userIdArg = "USER_ID" | ||
| instanceIdFlag = "instance-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| InstanceId string | ||
| UserId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("delete %s", userIdArg), | ||
| Short: "Deletes a SQLServer Flex user", | ||
| Long: fmt.Sprintf("%s\n%s", | ||
| "Deletes a SQLServer Flex user by ID. You can get the IDs of users for an instance by running:", | ||
| " $ stackit beta sqlserverflex user list --instance-id <INSTANCE_ID>", | ||
| ), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete a SQLServer Flex user with ID "xxx" for instance with ID "yyy"`, | ||
| "$ stackit beta sqlserverflex user delete xxx --instance-id yyy"), | ||
| ), | ||
| Args: args.SingleArg(userIdArg, nil), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get instance name: %v", err) | ||
| instanceLabel = model.InstanceId | ||
| } | ||
| userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get user name: %v", err) | ||
| userLabel = model.UserId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("delete SQLServer Flex user: %w", err) | ||
| } | ||
| p.Info("Deleted user %q of instance %q\n", userLabel, instanceLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") | ||
| err := flags.MarkFlagsRequired(cmd, instanceIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| userId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), | ||
| UserId: userId, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiDeleteUserRequest { | ||
| req := apiClient.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId) | ||
| return req | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &sqlserverflex.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() | ||
| var testUserId = "my-user-id" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testUserId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| instanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| InstanceId: testInstanceId, | ||
| UserId: testUserId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *sqlserverflex.ApiGetUserRequest)) sqlserverflex.ApiGetUserRequest { | ||
| request := testClient.GetUser(testCtx, testProjectId, testInstanceId, testUserId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, instanceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest sqlserverflex.ApiGetUserRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| const ( | ||
| userIdArg = "USER_ID" | ||
| instanceIdFlag = "instance-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| InstanceId string | ||
| UserId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("describe %s", userIdArg), | ||
| Short: "Shows details of a SQLServer Flex user", | ||
| Long: fmt.Sprintf("%s\n%s\n%s", | ||
| "Shows details of a SQLServer Flex user.", | ||
| `The user password is only visible upon creation. You can reset it by running:`, | ||
| " $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID", | ||
| ), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy"`, | ||
| "$ stackit beta sqlserverflex user describe xxx --instance-id yyy"), | ||
| examples.NewExample( | ||
| `Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy" in JSON format`, | ||
| "$ stackit beta sqlserverflex user describe xxx --instance-id yyy --output-format json"), | ||
| ), | ||
| Args: args.SingleArg(userIdArg, nil), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("get SQLServer Flex user: %w", err) | ||
| } | ||
| return outputResult(p, model.OutputFormat, *resp.Item) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") | ||
| err := flags.MarkFlagsRequired(cmd, instanceIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| userId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), | ||
| UserId: userId, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiGetUserRequest { | ||
| req := apiClient.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, user sqlserverflex.InstanceResponseUser) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(user, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal SQLServer Flex user: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal SQLServer Flex user: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.AddRow("ID", *user.Id) | ||
| table.AddSeparator() | ||
| table.AddRow("USERNAME", *user.Username) | ||
| if user.Roles != nil && len(*user.Roles) != 0 { | ||
| table.AddSeparator() | ||
| table.AddRow("ROLES", strings.Join(*user.Roles, "\n")) | ||
| } | ||
| if user.Database != nil && *user.Database != "" { | ||
| table.AddSeparator() | ||
| table.AddRow("DATABASE", *user.Database) | ||
| } | ||
| if user.Host != nil && *user.Host != "" { | ||
| table.AddSeparator() | ||
| table.AddRow("HOST", *user.Host) | ||
| } | ||
| if user.Port != nil { | ||
| table.AddSeparator() | ||
| table.AddRow("PORT", *user.Port) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "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" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &sqlserverflex.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| instanceIdFlag: testInstanceId, | ||
| limitFlag: "10", | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| InstanceId: utils.Ptr(testInstanceId), | ||
| Limit: utils.Ptr(int64(10)), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *sqlserverflex.ApiListUsersRequest)) sqlserverflex.ApiListUsersRequest { | ||
| request := testClient.ListUsers(testCtx, testProjectId, testInstanceId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, instanceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "instance id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[instanceIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| cmd := &cobra.Command{} | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| configureFlags(cmd) | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| p := print.NewPrinter() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest sqlserverflex.ApiListUsersRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" | ||
| sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" | ||
| ) | ||
| const ( | ||
| instanceIdFlag = "instance-id" | ||
| limitFlag = "limit" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| InstanceId *string | ||
| Limit *int64 | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists all SQLServer Flex users of an instance", | ||
| Long: "Lists all SQLServer Flex users of an instance.", | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all SQLServer Flex users of instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex user list --instance-id xxx"), | ||
| examples.NewExample( | ||
| `List all SQLServer Flex users of instance with ID "xxx" in JSON format`, | ||
| "$ stackit beta sqlserverflex user list --instance-id xxx --output-format json"), | ||
| examples.NewExample( | ||
| `List up to 10 SQLServer Flex users of instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex user list --instance-id xxx --limit 10"), | ||
| ), | ||
| Args: args.NoArgs, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("get SQLServer Flex users: %w", err) | ||
| } | ||
| if resp.Items == nil || len(*resp.Items) == 0 { | ||
| instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get instance name: %v", err) | ||
| instanceLabel = *model.InstanceId | ||
| } | ||
| p.Info("No users found for instance %q\n", instanceLabel) | ||
| return nil | ||
| } | ||
| users := *resp.Items | ||
| // Truncate output | ||
| if model.Limit != nil && len(users) > int(*model.Limit) { | ||
| users = users[:*model.Limit] | ||
| } | ||
| return outputResult(p, model.OutputFormat, users) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| err := flags.MarkFlagsRequired(cmd, instanceIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), | ||
| Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiListUsersRequest { | ||
| req := apiClient.ListUsers(ctx, model.ProjectId, *model.InstanceId) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, users []sqlserverflex.InstanceListUser) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(users, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal SQLServer Flex user list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal SQLServer Flex user list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("ID", "USERNAME") | ||
| for i := range users { | ||
| user := users[i] | ||
| table.AddRow(*user.Id, *user.Username) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package auth | ||
| import ( | ||
| "testing" | ||
| "github.com/spf13/viper" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| ) | ||
| func TestGetIDPEndpoint(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| idpCustomEndpoint string | ||
| isValid bool | ||
| expected string | ||
| }{ | ||
| { | ||
| name: "custom endpoint specified", | ||
| idpCustomEndpoint: "https://example.stackit.cloud", | ||
| isValid: true, | ||
| expected: "https://example.stackit.cloud", | ||
| }, | ||
| { | ||
| name: "custom endpoint outside STACKIT", | ||
| idpCustomEndpoint: "https://www.very-suspicious-website.com/", | ||
| isValid: false, | ||
| }, | ||
| { | ||
| name: "custom endpoint not specified", | ||
| idpCustomEndpoint: "", | ||
| isValid: true, | ||
| expected: defaultIDPEndpoint, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| viper.Reset() | ||
| viper.Set(config.IdentityProviderCustomEndpointKey, tt.idpCustomEndpoint) | ||
| got, err := getIDPEndpoint() | ||
| if tt.isValid && err != nil { | ||
| t.Fatalf("expected no error, got %v", err) | ||
| } | ||
| if !tt.isValid && err == nil { | ||
| t.Fatalf("expected error, got none") | ||
| } | ||
| if got != tt.expected { | ||
| t.Fatalf("expected idp endpoint %q, got %q", tt.expected, got) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package auth | ||
| import ( | ||
| "fmt" | ||
| "github.com/spf13/viper" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| ) | ||
| func getIDPEndpoint() (string, error) { | ||
| idpEndpoint := defaultIDPEndpoint | ||
| customIDPEndpoint := viper.GetString(config.IdentityProviderCustomEndpointKey) | ||
| if customIDPEndpoint != "" { | ||
| idpEndpoint = customIDPEndpoint | ||
| } | ||
| err := utils.ValidateSTACKITURL(idpEndpoint) | ||
| if err != nil { | ||
| return "", fmt.Errorf("validate custom identity provider endpoint: %w", err) | ||
| } | ||
| return idpEndpoint, nil | ||
| } |
| package client | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/auth" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/spf13/viper" | ||
| sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| func ConfigureClient(p *print.Printer) (*serverbackup.APIClient, error) { | ||
| var err error | ||
| var apiClient *serverbackup.APIClient | ||
| var cfgOptions []sdkConfig.ConfigurationOption | ||
| authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "configure authentication: %v", err) | ||
| return nil, &errors.AuthError{} | ||
| } | ||
| cfgOptions = append(cfgOptions, authCfgOption) | ||
| customEndpoint := viper.GetString(config.ServerBackupCustomEndpointKey) | ||
| if customEndpoint != "" { | ||
| cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) | ||
| } else { | ||
| cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| cfgOptions = append(cfgOptions, | ||
| sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), | ||
| ) | ||
| } | ||
| apiClient, err = serverbackup.NewAPIClient(cfgOptions...) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "create new API client: %v", err) | ||
| return nil, &errors.AuthError{} | ||
| } | ||
| return apiClient, nil | ||
| } |
| package utils | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| var ( | ||
| testProjectId = uuid.NewString() | ||
| testServerId = uuid.NewString() | ||
| ) | ||
| type serverbackupClientMocked struct { | ||
| listBackupSchedulesFails bool | ||
| listBackupSchedulesResp *serverbackup.ListBackupSchedules200Response | ||
| listBackupsFails bool | ||
| listBackupsResp *serverbackup.ListBackups200Response | ||
| } | ||
| func (m *serverbackupClientMocked) ListBackupSchedulesExecute(_ context.Context, _, _ string) (*serverbackup.ListBackupSchedules200Response, error) { | ||
| if m.listBackupSchedulesFails { | ||
| return nil, fmt.Errorf("could not list backup schedules") | ||
| } | ||
| return m.listBackupSchedulesResp, nil | ||
| } | ||
| func (m *serverbackupClientMocked) ListBackupsExecute(_ context.Context, _, _ string) (*serverbackup.ListBackups200Response, error) { | ||
| if m.listBackupsFails { | ||
| return nil, fmt.Errorf("could not list backups") | ||
| } | ||
| return m.listBackupsResp, nil | ||
| } | ||
| func TestCanDisableBackupService(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| listBackupsFails bool | ||
| listBackupSchedulesFails bool | ||
| listBackups *serverbackup.ListBackups200Response | ||
| listBackupSchedules *serverbackup.ListBackupSchedules200Response | ||
| isValid bool // isValid ==> err == nil | ||
| expectedOutput bool // expectedCanDisable | ||
| }{ | ||
| { | ||
| description: "base-ok-can-disable-backups-service-no-backups-no-backup-schedules", | ||
| listBackupsFails: false, | ||
| listBackupSchedulesFails: false, | ||
| listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}}, | ||
| listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}}, | ||
| isValid: true, | ||
| expectedOutput: true, | ||
| }, | ||
| { | ||
| description: "not-ok-api-error-list-backups", | ||
| listBackupsFails: true, | ||
| listBackupSchedulesFails: false, | ||
| listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}}, | ||
| listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}}, | ||
| isValid: false, | ||
| expectedOutput: false, | ||
| }, | ||
| { | ||
| description: "not-ok-api-error-list-backup-schedules", | ||
| listBackupsFails: true, | ||
| listBackupSchedulesFails: false, | ||
| listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}}, | ||
| listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}}, | ||
| isValid: false, | ||
| expectedOutput: false, | ||
| }, | ||
| { | ||
| description: "not-ok-has-backups-cannot-disable", | ||
| listBackupsFails: false, | ||
| listBackupSchedulesFails: false, | ||
| listBackups: &serverbackup.ListBackups200Response{ | ||
| Items: &[]serverbackup.Backup{ | ||
| { | ||
| CreatedAt: utils.Ptr("test timestamp"), | ||
| ExpireAt: utils.Ptr("test timestamp"), | ||
| Id: utils.Ptr("5"), | ||
| LastRestoredAt: utils.Ptr("test timestamp"), | ||
| Name: utils.Ptr("test name"), | ||
| Size: utils.Ptr(int64(5)), | ||
| Status: utils.Ptr("test status"), | ||
| VolumeBackups: nil, | ||
| }, | ||
| }, | ||
| }, | ||
| listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}}, | ||
| isValid: true, | ||
| expectedOutput: false, | ||
| }, | ||
| { | ||
| description: "not-ok-has-backups-schedules-cannot-disable", | ||
| listBackupsFails: false, | ||
| listBackupSchedulesFails: false, | ||
| listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}}, | ||
| listBackupSchedules: &serverbackup.ListBackupSchedules200Response{ | ||
| Items: &[]serverbackup.BackupSchedule{ | ||
| { | ||
| BackupProperties: nil, | ||
| Enabled: utils.Ptr(false), | ||
| Id: utils.Ptr(int64(5)), | ||
| Name: utils.Ptr("some name"), | ||
| Rrule: utils.Ptr("some rrule"), | ||
| }, | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| client := &serverbackupClientMocked{ | ||
| listBackupsFails: tt.listBackupsFails, | ||
| listBackupSchedulesFails: tt.listBackupSchedulesFails, | ||
| listBackupsResp: tt.listBackups, | ||
| listBackupSchedulesResp: tt.listBackupSchedules, | ||
| } | ||
| output, err := CanDisableBackupService(context.Background(), client, testProjectId, testServerId) | ||
| 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 %t, got %t", tt.expectedOutput, output) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package utils | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" | ||
| ) | ||
| type ServerBackupClient interface { | ||
| ListBackupSchedulesExecute(ctx context.Context, projectId, serverId string) (*serverbackup.ListBackupSchedules200Response, error) | ||
| ListBackupsExecute(ctx context.Context, projectId, serverId string) (*serverbackup.ListBackups200Response, error) | ||
| } | ||
| func CanDisableBackupService(ctx context.Context, client ServerBackupClient, projectId, serverId string) (bool, error) { | ||
| schedules, err := client.ListBackupSchedulesExecute(ctx, projectId, serverId) | ||
| if err != nil { | ||
| return false, fmt.Errorf("list backup schedules: %w", err) | ||
| } | ||
| if *schedules.Items != nil && len(*schedules.Items) > 0 { | ||
| return false, nil | ||
| } | ||
| backups, err := client.ListBackupsExecute(ctx, projectId, serverId) | ||
| if err != nil { | ||
| return false, fmt.Errorf("list backups: %w", err) | ||
| } | ||
| if *backups.Items != nil && len(*backups.Items) > 0 { | ||
| return false, nil | ||
| } | ||
| // no backups and no backup schedules found for this server => can disable backup service | ||
| return true, nil | ||
| } |
@@ -8,2 +8,3 @@ ## stackit auth login | ||
| Logs in to the STACKIT CLI using a user account. | ||
| The authentication is done via a web-based authorization flow, where the command will open a browser window in which you can login to your STACKIT account. | ||
@@ -10,0 +11,0 @@ ``` |
| ## stackit beta sqlserverflex instance create | ||
| Creates an SQLServer Flex instance | ||
| Creates a SQLServer Flex instance | ||
| ### Synopsis | ||
| Creates an SQLServer Flex instance. | ||
| Creates a SQLServer Flex instance. | ||
@@ -16,9 +16,9 @@ ``` | ||
| ``` | ||
| Create an SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values | ||
| Create a SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values | ||
| $ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 | ||
| Create an SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values | ||
| Create a SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values | ||
| $ stackit beta sqlserverflex instance create --name my-instance --flavor-id xxx | ||
| Create an SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values | ||
| Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values | ||
| $ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24 | ||
@@ -25,0 +25,0 @@ ``` |
| ## stackit beta sqlserverflex instance delete | ||
| Deletes an SQLServer Flex instance | ||
| Deletes a SQLServer Flex instance | ||
| ### Synopsis | ||
| Deletes an SQLServer Flex instance. | ||
| Deletes a SQLServer Flex instance. | ||
@@ -16,3 +16,3 @@ ``` | ||
| ``` | ||
| Delete an SQLServer Flex instance with ID "xxx" | ||
| Delete a SQLServer Flex instance with ID "xxx" | ||
| $ stackit beta sqlserverflex instance delete xxx | ||
@@ -19,0 +19,0 @@ ``` |
| ## stackit beta sqlserverflex instance describe | ||
| Shows details of an SQLServer Flex instance | ||
| Shows details of a SQLServer Flex instance | ||
| ### Synopsis | ||
| Shows details of an SQLServer Flex instance. | ||
| Shows details of a SQLServer Flex instance. | ||
@@ -16,6 +16,6 @@ ``` | ||
| ``` | ||
| Get details of an SQLServer Flex instance with ID "xxx" | ||
| Get details of a SQLServer Flex instance with ID "xxx" | ||
| $ stackit beta sqlserverflex instance describe xxx | ||
| Get details of an SQLServer Flex instance with ID "xxx" in JSON format | ||
| Get details of a SQLServer Flex instance with ID "xxx" in JSON format | ||
| $ stackit beta sqlserverflex instance describe xxx --output-format json | ||
@@ -22,0 +22,0 @@ ``` |
| ## stackit beta sqlserverflex instance update | ||
| Updates an SQLServer Flex instance | ||
| Updates a SQLServer Flex instance | ||
| ### Synopsis | ||
| Updates an SQLServer Flex instance. | ||
| Updates a SQLServer Flex instance. | ||
@@ -16,6 +16,6 @@ ``` | ||
| ``` | ||
| Update the name of an SQLServer Flex instance with ID "xxx" | ||
| Update the name of a SQLServer Flex instance with ID "xxx" | ||
| $ stackit beta sqlserverflex instance update xxx --name my-new-name | ||
| Update the backup schedule of an SQLServer Flex instance with ID "xxx" | ||
| Update the backup schedule of a SQLServer Flex instance with ID "xxx" | ||
| $ stackit beta sqlserverflex instance update xxx --backup-schedule "30 0 * * *" | ||
@@ -22,0 +22,0 @@ ``` |
@@ -32,7 +32,7 @@ ## stackit beta sqlserverflex instance | ||
| * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex | ||
| * [stackit beta sqlserverflex instance create](./stackit_beta_sqlserverflex_instance_create.md) - Creates an SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance delete](./stackit_beta_sqlserverflex_instance_delete.md) - Deletes an SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance describe](./stackit_beta_sqlserverflex_instance_describe.md) - Shows details of an SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance create](./stackit_beta_sqlserverflex_instance_create.md) - Creates a SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance delete](./stackit_beta_sqlserverflex_instance_delete.md) - Deletes a SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance describe](./stackit_beta_sqlserverflex_instance_describe.md) - Shows details of a SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance list](./stackit_beta_sqlserverflex_instance_list.md) - Lists all SQLServer Flex instances | ||
| * [stackit beta sqlserverflex instance update](./stackit_beta_sqlserverflex_instance_update.md) - Updates an SQLServer Flex instance | ||
| * [stackit beta sqlserverflex instance update](./stackit_beta_sqlserverflex_instance_update.md) - Updates a SQLServer Flex instance | ||
| ## stackit beta sqlserverflex user create | ||
| Creates an SQLServer Flex user | ||
| Creates a SQLServer Flex user | ||
| ### Synopsis | ||
| Creates an SQLServer Flex user for an instance. | ||
| Creates a SQLServer Flex user for an instance. | ||
| The password is only visible upon creation and cannot be retrieved later. | ||
@@ -20,7 +20,7 @@ Alternatively, you can reset the password and access the new one by running: | ||
| ``` | ||
| Create an SQLServer Flex user for instance with ID "xxx" and specify the username, role and database | ||
| Create a SQLServer Flex user for instance with ID "xxx" and specify the username, role and database | ||
| $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles my-role --database my-database | ||
| Create an SQLServer Flex user for instance with ID "xxx", specifying multiple roles | ||
| $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2 | ||
| Create a SQLServer Flex user for instance with ID "xxx", specifying multiple roles | ||
| $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2" | ||
| ``` | ||
@@ -31,3 +31,2 @@ | ||
| ``` | ||
| --database string Default database for the user | ||
| -h, --help Help for "stackit beta sqlserverflex user create" | ||
@@ -34,0 +33,0 @@ --instance-id string ID of the instance |
| ## stackit beta sqlserverflex user reset-password | ||
| Resets the password of an SQLServer Flex user | ||
| Resets the password of a SQLServer Flex user | ||
| ### Synopsis | ||
| Resets the password of an SQLServer Flex user. | ||
| sThe new password is visible after resetting and cannot be retrieved later. | ||
| Resets the password of a SQLServer Flex user. | ||
| The new password is visible after resetting and cannot be retrieved later. | ||
@@ -17,3 +17,3 @@ ``` | ||
| ``` | ||
| Reset the password of an SQLServer Flex user with ID "xxx" of instance with ID "yyy" | ||
| Reset the password of a SQLServer Flex user with ID "xxx" of instance with ID "yyy" | ||
| $ stackit beta sqlserverflex user reset-password xxx --instance-id yyy | ||
@@ -20,0 +20,0 @@ ``` |
@@ -32,4 +32,7 @@ ## stackit beta sqlserverflex user | ||
| * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex | ||
| * [stackit beta sqlserverflex user create](./stackit_beta_sqlserverflex_user_create.md) - Creates an SQLServer Flex user | ||
| * [stackit beta sqlserverflex user reset-password](./stackit_beta_sqlserverflex_user_reset-password.md) - Resets the password of an SQLServer Flex user | ||
| * [stackit beta sqlserverflex user create](./stackit_beta_sqlserverflex_user_create.md) - Creates a SQLServer Flex user | ||
| * [stackit beta sqlserverflex user delete](./stackit_beta_sqlserverflex_user_delete.md) - Deletes a SQLServer Flex user | ||
| * [stackit beta sqlserverflex user describe](./stackit_beta_sqlserverflex_user_describe.md) - Shows details of a SQLServer Flex user | ||
| * [stackit beta sqlserverflex user list](./stackit_beta_sqlserverflex_user_list.md) - Lists all SQLServer Flex users of an instance | ||
| * [stackit beta sqlserverflex user reset-password](./stackit_beta_sqlserverflex_user_reset-password.md) - Resets the password of a SQLServer Flex user | ||
@@ -32,2 +32,3 @@ ## stackit beta sqlserverflex | ||
| * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands | ||
| * [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases | ||
| * [stackit beta sqlserverflex instance](./stackit_beta_sqlserverflex_instance.md) - Provides functionality for SQLServer Flex instances | ||
@@ -34,0 +35,0 @@ * [stackit beta sqlserverflex options](./stackit_beta_sqlserverflex_options.md) - Lists SQL Server Flex options |
@@ -43,3 +43,4 @@ ## stackit beta | ||
| * [stackit](./stackit.md) - Manage STACKIT resources using the command line | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for Server | ||
| * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex | ||
@@ -32,21 +32,23 @@ ## stackit config set | ||
| ``` | ||
| --argus-custom-endpoint string Argus API base URL, used in calls to this API | ||
| --authorization-custom-endpoint string Authorization API base URL, used in calls to this API | ||
| --dns-custom-endpoint string DNS API base URL, used in calls to this API | ||
| -h, --help Help for "stackit config set" | ||
| --load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API | ||
| --logme-custom-endpoint string LogMe API base URL, used in calls to this API | ||
| --mariadb-custom-endpoint string MariaDB API base URL, used in calls to this API | ||
| --mongodbflex-custom-endpoint string MongoDB Flex API base URL, used in calls to this API | ||
| --object-storage-custom-endpoint string Object Storage API base URL, used in calls to this API | ||
| --opensearch-custom-endpoint string OpenSearch API base URL, used in calls to this API | ||
| --postgresflex-custom-endpoint string PostgreSQL Flex API base URL, used in calls to this API | ||
| --rabbitmq-custom-endpoint string RabbitMQ API base URL, used in calls to this API | ||
| --redis-custom-endpoint string Redis API base URL, used in calls to this API | ||
| --resource-manager-custom-endpoint string Resource Manager API base URL, used in calls to this API | ||
| --secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API | ||
| --service-account-custom-endpoint string Service Account API base URL, used in calls to this API | ||
| --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect) | ||
| --ske-custom-endpoint string SKE API base URL, used in calls to this API | ||
| --sqlserverflex-custom-endpoint string SQLServer Flex API base URL, used in calls to this API | ||
| --argus-custom-endpoint string Argus API base URL, used in calls to this API | ||
| --authorization-custom-endpoint string Authorization API base URL, used in calls to this API | ||
| --dns-custom-endpoint string DNS API base URL, used in calls to this API | ||
| -h, --help Help for "stackit config set" | ||
| --identity-provider-custom-endpoint string Identity Provider base URL, used for user authentication | ||
| --load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API | ||
| --logme-custom-endpoint string LogMe API base URL, used in calls to this API | ||
| --mariadb-custom-endpoint string MariaDB API base URL, used in calls to this API | ||
| --mongodbflex-custom-endpoint string MongoDB Flex API base URL, used in calls to this API | ||
| --object-storage-custom-endpoint string Object Storage API base URL, used in calls to this API | ||
| --opensearch-custom-endpoint string OpenSearch API base URL, used in calls to this API | ||
| --postgresflex-custom-endpoint string PostgreSQL Flex API base URL, used in calls to this API | ||
| --rabbitmq-custom-endpoint string RabbitMQ API base URL, used in calls to this API | ||
| --redis-custom-endpoint string Redis API base URL, used in calls to this API | ||
| --resource-manager-custom-endpoint string Resource Manager API base URL, used in calls to this API | ||
| --secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API | ||
| --serverbackup-custom-endpoint string Server Backup API base URL, used in calls to this API | ||
| --service-account-custom-endpoint string Service Account API base URL, used in calls to this API | ||
| --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect) | ||
| --ske-custom-endpoint string SKE API base URL, used in calls to this API | ||
| --sqlserverflex-custom-endpoint string SQLServer Flex API base URL, used in calls to this API | ||
| ``` | ||
@@ -53,0 +55,0 @@ |
@@ -29,25 +29,27 @@ ## stackit config unset | ||
| ``` | ||
| --argus-custom-endpoint Argus API base URL. If unset, uses the default base URL | ||
| --async Configuration option to run commands asynchronously | ||
| --authorization-custom-endpoint Authorization API base URL. If unset, uses the default base URL | ||
| --dns-custom-endpoint DNS API base URL. If unset, uses the default base URL | ||
| -h, --help Help for "stackit config unset" | ||
| --load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL | ||
| --logme-custom-endpoint LogMe API base URL. If unset, uses the default base URL | ||
| --mariadb-custom-endpoint MariaDB API base URL. If unset, uses the default base URL | ||
| --mongodbflex-custom-endpoint MongoDB Flex API base URL. If unset, uses the default base URL | ||
| --object-storage-custom-endpoint Object Storage API base URL. If unset, uses the default base URL | ||
| --opensearch-custom-endpoint OpenSearch API base URL. If unset, uses the default base URL | ||
| --output-format Output format | ||
| --postgresflex-custom-endpoint PostgreSQL Flex API base URL. If unset, uses the default base URL | ||
| --project-id Project ID | ||
| --rabbitmq-custom-endpoint RabbitMQ API base URL. If unset, uses the default base URL | ||
| --redis-custom-endpoint Redis API base URL. If unset, uses the default base URL | ||
| --resource-manager-custom-endpoint Resource Manager API base URL. If unset, uses the default base URL | ||
| --secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL | ||
| --service-account-custom-endpoint SKE API base URL. If unset, uses the default base URL | ||
| --session-time-limit Maximum time before authentication is required again. If unset, defaults to 2h | ||
| --ske-custom-endpoint SKE API base URL. If unset, uses the default base URL | ||
| --sqlserverflex-custom-endpoint SQLServer Flex API base URL. If unset, uses the default base URL | ||
| --verbosity Verbosity of the CLI | ||
| --argus-custom-endpoint Argus API base URL. If unset, uses the default base URL | ||
| --async Configuration option to run commands asynchronously | ||
| --authorization-custom-endpoint Authorization API base URL. If unset, uses the default base URL | ||
| --dns-custom-endpoint DNS API base URL. If unset, uses the default base URL | ||
| -h, --help Help for "stackit config unset" | ||
| --identity-provider-custom-endpoint Identity Provider base URL. If unset, uses the default base URL | ||
| --load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL | ||
| --logme-custom-endpoint LogMe API base URL. If unset, uses the default base URL | ||
| --mariadb-custom-endpoint MariaDB API base URL. If unset, uses the default base URL | ||
| --mongodbflex-custom-endpoint MongoDB Flex API base URL. If unset, uses the default base URL | ||
| --object-storage-custom-endpoint Object Storage API base URL. If unset, uses the default base URL | ||
| --opensearch-custom-endpoint OpenSearch API base URL. If unset, uses the default base URL | ||
| --output-format Output format | ||
| --postgresflex-custom-endpoint PostgreSQL Flex API base URL. If unset, uses the default base URL | ||
| --project-id Project ID | ||
| --rabbitmq-custom-endpoint RabbitMQ API base URL. If unset, uses the default base URL | ||
| --redis-custom-endpoint Redis API base URL. If unset, uses the default base URL | ||
| --resource-manager-custom-endpoint Resource Manager API base URL. If unset, uses the default base URL | ||
| --secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL | ||
| --serverbackup-custom-endpoint Server Backup base URL. If unset, uses the default base URL | ||
| --service-account-custom-endpoint SKE API base URL. If unset, uses the default base URL | ||
| --session-time-limit Maximum time before authentication is required again. If unset, defaults to 2h | ||
| --ske-custom-endpoint SKE API base URL. If unset, uses the default base URL | ||
| --sqlserverflex-custom-endpoint SQLServer Flex API base URL. If unset, uses the default base URL | ||
| --verbosity Verbosity of the CLI | ||
| ``` | ||
@@ -54,0 +56,0 @@ |
+7
-6
@@ -23,12 +23,13 @@ module github.com/stackitcloud/stackit-cli | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.15.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.16.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.17.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0 | ||
| github.com/zalando/go-keyring v0.2.5 | ||
| golang.org/x/mod v0.18.0 | ||
| golang.org/x/mod v0.19.0 | ||
| golang.org/x/oauth2 v0.21.0 | ||
| golang.org/x/term v0.21.0 | ||
| golang.org/x/term v0.22.0 | ||
| golang.org/x/text v0.16.0 | ||
@@ -79,3 +80,3 @@ k8s.io/apimachinery v0.29.2 | ||
| github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.13.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.15.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.16.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.15.0 | ||
@@ -88,3 +89,3 @@ github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.10.0 | ||
| golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect | ||
| golang.org/x/sys v0.21.0 // indirect | ||
| golang.org/x/sys v0.22.0 // indirect | ||
| gopkg.in/ini.v1 v1.67.0 // indirect | ||
@@ -91,0 +92,0 @@ gopkg.in/yaml.v2 v2.4.0 // indirect |
+14
-12
@@ -136,4 +136,4 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= | ||
| github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.13.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.15.0 h1:7gii3PZshOesHPCYlPycilXglk28imITIqjewySZwZ4= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.15.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.16.0 h1:Cz4zQnEax6L3Y9gL7jtETPmiTERB7WNQtIzZ1UWeNNk= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.16.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.15.0 h1:eYYyVUTS9Gjovg3z9+r6ctvsm1p1J4fHLa5QJbWHi0A= | ||
@@ -147,4 +147,4 @@ github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.15.0/go.mod h1:kPetkX9hNm9HkRyiKQL/tlgdi8frZdMP8afg0mEvQ9s= | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0/go.mod h1:ZecMIf9oYj2DGZqWh93l97WdVaRdLl+tW5Fq3YKGwBM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.0 h1:PZAqXd8TVyTZo8qty4bM2sSoLlLG+Nc9tcpxbQhO+GY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.0/go.mod h1:SdrqGLCkilL6wl1+jcxmLtks2IocgIg+bsyeyYUIzR4= | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.15.0 h1:05wQYhO37Z4y8xAD+4OTYz6rYu6eJEmwMfCG4tjETEc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.15.0/go.mod h1:SdrqGLCkilL6wl1+jcxmLtks2IocgIg+bsyeyYUIzR4= | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.15.0 h1:Q7JxjVwb+9ugAX71AXdbfPL87HHmIIwb9LNahn6H/2o= | ||
@@ -158,6 +158,8 @@ github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.15.0/go.mod h1:eSgnPBknTJh7t+jVKN+xzeAh+Cg1USOlH3QCyfvG20g= | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0/go.mod h1:LX0Mcyr7/QP77zf7e05fHCJO38RMuTxr7nEDUDZ3oPQ= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0 h1:fYCBNvh4tqE+DXYDfbJEjC3n/I78zTZajdcPTPB/yig= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0/go.mod h1:ZYI3wj/NnhhWi25ugbdcniwnY/7mF6zN582c5HPe00o= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 h1:JB1O0E9+L50ZaO36uz7azurvUuB5JdX5s2ZXuIdb9t8= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0/go.mod h1:Ni9RBJvcaXRIrDIuQBpJcuQvCQSj27crQSyc+WM4p0c= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.16.0 h1:trrJuRMzgXu6fiiMZiUx6+A1FNKEFhA1vGq5cr5Qn3U= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.16.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.17.0 h1:4S3MwNmpMfjzBz9JtKbXvkos7j+7hGeFMf7XsjMLL/g= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.17.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ= | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0 h1:aIXxXx6u4+6C02MPb+hdItigeKeen7m+hEEG+Ej9sNs= | ||
@@ -193,4 +195,4 @@ github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0/go.mod h1:fQJOQMfasStZ8J9iGX0vTjyJoQtLqMXJ5Npb03QJk84= | ||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||
| golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= | ||
| golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||
| golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= | ||
| golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||
@@ -212,6 +214,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= | ||
| golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
| golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= | ||
| golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= | ||
| golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= | ||
| golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
| golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= | ||
| golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= | ||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
@@ -218,0 +220,0 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
@@ -18,4 +18,6 @@ package login | ||
| Short: "Logs in to the STACKIT CLI", | ||
| Long: "Logs in to the STACKIT CLI using a user account.", | ||
| Args: args.NoArgs, | ||
| Long: fmt.Sprintf("%s\n%s", | ||
| "Logs in to the STACKIT CLI using a user account.", | ||
| "The authentication is done via a web-based authorization flow, where the command will open a browser window in which you can login to your STACKIT account."), | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
@@ -22,0 +24,0 @@ examples.NewExample( |
@@ -6,2 +6,3 @@ package beta | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" | ||
@@ -40,2 +41,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cmd.AddCommand(sqlserverflex.NewCmd(p)) | ||
| cmd.AddCommand(server.NewCmd(p)) | ||
| } |
@@ -60,14 +60,14 @@ package create | ||
| Use: "create", | ||
| Short: "Creates an SQLServer Flex instance", | ||
| Long: "Creates an SQLServer Flex instance.", | ||
| Short: "Creates a SQLServer Flex instance", | ||
| Long: "Creates a SQLServer Flex instance.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create an SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values`, | ||
| `Create a SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values`, | ||
| `$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4`), | ||
| examples.NewExample( | ||
| `Create an SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values`, | ||
| `Create a SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values`, | ||
| `$ stackit beta sqlserverflex instance create --name my-instance --flavor-id xxx`), | ||
| examples.NewExample( | ||
| `Create an SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values`, | ||
| `Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values`, | ||
| `$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24`), | ||
@@ -96,3 +96,3 @@ ), | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an SQLServer Flex instance for project %q?", projectLabel) | ||
| prompt := fmt.Sprintf("Are you sure you want to create a SQLServer Flex instance for project %q?", projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
@@ -99,0 +99,0 @@ if err != nil { |
@@ -34,8 +34,8 @@ package delete | ||
| Use: fmt.Sprintf("delete %s", instanceIdArg), | ||
| Short: "Deletes an SQLServer Flex instance", | ||
| Long: "Deletes an SQLServer Flex instance.", | ||
| Short: "Deletes a SQLServer Flex instance", | ||
| Long: "Deletes a SQLServer Flex instance.", | ||
| Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete an SQLServer Flex instance with ID "xxx"`, | ||
| `Delete a SQLServer Flex instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex instance delete xxx"), | ||
@@ -42,0 +42,0 @@ ), |
@@ -35,11 +35,11 @@ package describe | ||
| Use: fmt.Sprintf("describe %s", instanceIdArg), | ||
| Short: "Shows details of an SQLServer Flex instance", | ||
| Long: "Shows details of an SQLServer Flex instance.", | ||
| Short: "Shows details of a SQLServer Flex instance", | ||
| Long: "Shows details of a SQLServer Flex instance.", | ||
| Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get details of an SQLServer Flex instance with ID "xxx"`, | ||
| `Get details of a SQLServer Flex instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex instance describe xxx"), | ||
| examples.NewExample( | ||
| `Get details of an SQLServer Flex instance with ID "xxx" in JSON format`, | ||
| `Get details of a SQLServer Flex instance with ID "xxx" in JSON format`, | ||
| "$ stackit beta sqlserverflex instance describe xxx --output-format json"), | ||
@@ -46,0 +46,0 @@ ), |
@@ -54,10 +54,10 @@ package update | ||
| Use: fmt.Sprintf("update %s", instanceIdArg), | ||
| Short: "Updates an SQLServer Flex instance", | ||
| Long: "Updates an SQLServer Flex instance.", | ||
| Short: "Updates a SQLServer Flex instance", | ||
| Long: "Updates a SQLServer Flex instance.", | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Update the name of an SQLServer Flex instance with ID "xxx"`, | ||
| `Update the name of a SQLServer Flex instance with ID "xxx"`, | ||
| "$ stackit beta sqlserverflex instance update xxx --name my-new-name"), | ||
| examples.NewExample( | ||
| `Update the backup schedule of an SQLServer Flex instance with ID "xxx"`, | ||
| `Update the backup schedule of a SQLServer Flex instance with ID "xxx"`, | ||
| `$ stackit beta sqlserverflex instance update xxx --backup-schedule "30 0 * * *"`), | ||
@@ -64,0 +64,0 @@ ), |
@@ -156,3 +156,3 @@ package options | ||
| "You can get the available flavor IDs by running:", | ||
| " $ stackit sqlserverflex options --flavors") | ||
| " $ stackit beta sqlserverflex options --flavors") | ||
| } | ||
@@ -164,3 +164,3 @@ | ||
| "You can get the available instances and their IDs by running:", | ||
| " $ stackit sqlserverflex instance list") | ||
| " $ stackit beta sqlserverflex instance list") | ||
| } | ||
@@ -167,0 +167,0 @@ |
| package sqlserverflex | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/instance" | ||
@@ -27,2 +28,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/options" | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(database.NewCmd(p)) | ||
| cmd.AddCommand(instance.NewCmd(p)) | ||
@@ -29,0 +31,0 @@ cmd.AddCommand(options.NewCmd(p)) |
@@ -32,3 +32,2 @@ package create | ||
| usernameFlag: "johndoe", | ||
| databaseFlag: "default", | ||
| rolesFlag: "read", | ||
@@ -50,3 +49,2 @@ } | ||
| Username: utils.Ptr("johndoe"), | ||
| Database: utils.Ptr("default"), | ||
| Roles: utils.Ptr([]string{"read"}), | ||
@@ -64,3 +62,2 @@ } | ||
| Username: utils.Ptr("johndoe"), | ||
| Database: utils.Ptr("default"), | ||
| Roles: utils.Ptr([]sqlserverflex.Role{"read"}), | ||
@@ -97,12 +94,2 @@ }) | ||
| { | ||
| description: "no database specified", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, databaseFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Database = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "no roles specified", | ||
@@ -224,4 +211,3 @@ flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| expectedRequest: fixtureRequest().CreateUserPayload(sqlserverflex.CreateUserPayload{ | ||
| Database: utils.Ptr("default"), | ||
| Roles: utils.Ptr([]sqlserverflex.Role{"read"}), | ||
| Roles: utils.Ptr([]sqlserverflex.Role{"read"}), | ||
| }), | ||
@@ -228,0 +214,0 @@ }, |
@@ -24,3 +24,2 @@ package create | ||
| usernameFlag = "username" | ||
| databaseFlag = "database" | ||
| rolesFlag = "roles" | ||
@@ -34,3 +33,2 @@ ) | ||
| Username *string | ||
| Database *string | ||
| Roles *[]string | ||
@@ -42,5 +40,5 @@ } | ||
| Use: "create", | ||
| Short: "Creates an SQLServer Flex user", | ||
| Short: "Creates a SQLServer Flex user", | ||
| Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", | ||
| "Creates an SQLServer Flex user for an instance.", | ||
| "Creates a SQLServer Flex user for an instance.", | ||
| "The password is only visible upon creation and cannot be retrieved later.", | ||
@@ -53,7 +51,7 @@ "Alternatively, you can reset the password and access the new one by running:", | ||
| examples.NewExample( | ||
| `Create an SQLServer Flex user for instance with ID "xxx" and specify the username, role and database`, | ||
| `Create a SQLServer Flex user for instance with ID "xxx" and specify the username, role and database`, | ||
| "$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles my-role --database my-database"), | ||
| examples.NewExample( | ||
| `Create an SQLServer Flex user for instance with ID "xxx", specifying multiple roles`, | ||
| `$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2`), | ||
| `Create a SQLServer Flex user for instance with ID "xxx", specifying multiple roles`, | ||
| `$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2"`), | ||
| ), | ||
@@ -107,3 +105,2 @@ Args: args.NoArgs, | ||
| cmd.Flags().String(usernameFlag, "", "Username of the user") | ||
| cmd.Flags().String(databaseFlag, "", "Default database for the user") | ||
| cmd.Flags().StringSlice(rolesFlag, []string{}, "Roles of the user") | ||
@@ -125,3 +122,2 @@ | ||
| Username: flags.FlagToStringPointer(p, cmd, usernameFlag), | ||
| Database: flags.FlagToStringPointer(p, cmd, databaseFlag), | ||
| Roles: flags.FlagToStringSlicePointer(p, cmd, rolesFlag), | ||
@@ -154,3 +150,2 @@ } | ||
| Username: model.Username, | ||
| Database: model.Database, | ||
| Roles: &roles, | ||
@@ -186,5 +181,2 @@ }) | ||
| } | ||
| if user.Database != nil && *user.Database != "" { | ||
| p.Outputf("Database: %s\n", *user.Database) | ||
| } | ||
| if user.Host != nil && *user.Host != "" { | ||
@@ -191,0 +183,0 @@ p.Outputf("Host: %s\n", *user.Host) |
@@ -24,3 +24,3 @@ package resetpassword | ||
| var testInstanceId = uuid.NewString() | ||
| var testUserId = uuid.NewString() | ||
| var testUserId = "my-user-id" | ||
@@ -27,0 +27,0 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { |
@@ -38,5 +38,5 @@ package resetpassword | ||
| Use: fmt.Sprintf("reset-password %s", userIdArg), | ||
| Short: "Resets the password of an SQLServer Flex user", | ||
| Long: fmt.Sprintf("%s\ns%s", | ||
| "Resets the password of an SQLServer Flex user.", | ||
| Short: "Resets the password of a SQLServer Flex user", | ||
| Long: fmt.Sprintf("%s\n%s", | ||
| "Resets the password of a SQLServer Flex user.", | ||
| "The new password is visible after resetting and cannot be retrieved later.", | ||
@@ -46,3 +46,3 @@ ), | ||
| examples.NewExample( | ||
| `Reset the password of an SQLServer Flex user with ID "xxx" of instance with ID "yyy"`, | ||
| `Reset the password of a SQLServer Flex user with ID "xxx" of instance with ID "yyy"`, | ||
| "$ stackit beta sqlserverflex user reset-password xxx --instance-id yyy"), | ||
@@ -49,0 +49,0 @@ ), |
@@ -5,2 +5,5 @@ package user | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/list" | ||
| resetpassword "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/reset-password" | ||
@@ -28,3 +31,6 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cmd.AddCommand(create.NewCmd(p)) | ||
| cmd.AddCommand(delete.NewCmd(p)) | ||
| cmd.AddCommand(describe.NewCmd(p)) | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| cmd.AddCommand(resetpassword.NewCmd(p)) | ||
| } |
@@ -20,3 +20,4 @@ package set | ||
| const ( | ||
| sessionTimeLimitFlag = "session-time-limit" | ||
| sessionTimeLimitFlag = "session-time-limit" | ||
| identityProviderCustomEndpointFlag = "identity-provider-custom-endpoint" | ||
@@ -37,2 +38,3 @@ argusCustomEndpointFlag = "argus-custom-endpoint" | ||
| secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint" | ||
| serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" | ||
| serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" | ||
@@ -128,3 +130,3 @@ skeCustomEndpointFlag = "ske-custom-endpoint" | ||
| cmd.Flags().String(sessionTimeLimitFlag, "", "Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect)") | ||
| cmd.Flags().String(identityProviderCustomEndpointFlag, "", "Identity Provider base URL, used for user authentication") | ||
| cmd.Flags().String(argusCustomEndpointFlag, "", "Argus API base URL, used in calls to this API") | ||
@@ -145,7 +147,13 @@ cmd.Flags().String(authorizationCustomEndpointFlag, "", "Authorization API base URL, used in calls to this API") | ||
| cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account API base URL, used in calls to this API") | ||
| cmd.Flags().String(serverBackupCustomEndpointFlag, "", "Server Backup API base URL, used in calls to this API") | ||
| cmd.Flags().String(skeCustomEndpointFlag, "", "SKE API base URL, used in calls to this API") | ||
| cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API") | ||
| err := viper.BindPFlag(config.ArgusCustomEndpointKey, cmd.Flags().Lookup(argusCustomEndpointFlag)) | ||
| err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag)) | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.IdentityProviderCustomEndpointKey, cmd.Flags().Lookup(identityProviderCustomEndpointFlag)) | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.ArgusCustomEndpointKey, cmd.Flags().Lookup(argusCustomEndpointFlag)) | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.AuthorizationCustomEndpointKey, cmd.Flags().Lookup(authorizationCustomEndpointFlag)) | ||
@@ -177,2 +185,4 @@ cobra.CheckErr(err) | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.ServerBackupCustomEndpointKey, cmd.Flags().Lookup(serverBackupCustomEndpointFlag)) | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.ServiceAccountCustomEndpointKey, cmd.Flags().Lookup(serviceAccountCustomEndpointFlag)) | ||
@@ -179,0 +189,0 @@ cobra.CheckErr(err) |
@@ -14,8 +14,10 @@ package unset | ||
| flagValues := map[string]bool{ | ||
| asyncFlag: true, | ||
| outputFormatFlag: true, | ||
| projectIdFlag: true, | ||
| sessionTimeLimitFlag: true, | ||
| verbosityFlag: true, | ||
| asyncFlag: true, | ||
| outputFormatFlag: true, | ||
| projectIdFlag: true, | ||
| verbosityFlag: true, | ||
| sessionTimeLimitFlag: true, | ||
| identityProviderCustomEndpointFlag: true, | ||
| argusCustomEndpointFlag: true, | ||
@@ -34,2 +36,3 @@ authorizationCustomEndpointFlag: true, | ||
| serviceAccountCustomEndpointFlag: true, | ||
| serverBackupCustomEndpointFlag: true, | ||
| skeCustomEndpointFlag: true, | ||
@@ -46,8 +49,10 @@ sqlServerFlexCustomEndpointFlag: true, | ||
| model := &inputModel{ | ||
| Async: true, | ||
| OutputFormat: true, | ||
| ProjectId: true, | ||
| SessionTimeLimit: true, | ||
| Verbosity: true, | ||
| Async: true, | ||
| OutputFormat: true, | ||
| ProjectId: true, | ||
| Verbosity: true, | ||
| SessionTimeLimit: true, | ||
| IdentityProviderCustomEndpoint: true, | ||
| ArgusCustomEndpoint: true, | ||
@@ -66,2 +71,3 @@ AuthorizationCustomEndpoint: true, | ||
| ServiceAccountCustomEndpoint: true, | ||
| ServerBackupCustomEndpoint: true, | ||
| SKECustomEndpoint: true, | ||
@@ -97,5 +103,7 @@ SQLServerFlexCustomEndpoint: true, | ||
| model.ProjectId = false | ||
| model.SessionTimeLimit = false | ||
| model.Verbosity = false | ||
| model.SessionTimeLimit = false | ||
| model.IdentityProviderCustomEndpoint = false | ||
| model.ArgusCustomEndpoint = false | ||
@@ -114,2 +122,3 @@ model.AuthorizationCustomEndpoint = false | ||
| model.ServiceAccountCustomEndpoint = false | ||
| model.ServerBackupCustomEndpoint = false | ||
| model.SKECustomEndpoint = false | ||
@@ -140,2 +149,12 @@ model.SQLServerFlexCustomEndpoint = false | ||
| { | ||
| description: "identity provider custom endpoint empty", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]bool) { | ||
| flagValues[identityProviderCustomEndpointFlag] = false | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.IdentityProviderCustomEndpoint = false | ||
| }), | ||
| }, | ||
| { | ||
| description: "argus custom endpoint empty", | ||
@@ -200,2 +219,12 @@ flagValues: fixtureFlagValues(func(flagValues map[string]bool) { | ||
| }, | ||
| { | ||
| description: "serverbackup custom endpoint empty", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]bool) { | ||
| flagValues[serverBackupCustomEndpointFlag] = false | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.ServerBackupCustomEndpoint = false | ||
| }), | ||
| }, | ||
| } | ||
@@ -202,0 +231,0 @@ for _, tt := range tests { |
@@ -23,3 +23,4 @@ package unset | ||
| sessionTimeLimitFlag = "session-time-limit" | ||
| sessionTimeLimitFlag = "session-time-limit" | ||
| identityProviderCustomEndpointFlag = "identity-provider-custom-endpoint" | ||
@@ -41,2 +42,3 @@ argusCustomEndpointFlag = "argus-custom-endpoint" | ||
| serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" | ||
| serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" | ||
| skeCustomEndpointFlag = "ske-custom-endpoint" | ||
@@ -47,8 +49,10 @@ sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" | ||
| type inputModel struct { | ||
| Async bool | ||
| OutputFormat bool | ||
| ProjectId bool | ||
| SessionTimeLimit bool | ||
| Verbosity bool | ||
| Async bool | ||
| OutputFormat bool | ||
| ProjectId bool | ||
| Verbosity bool | ||
| SessionTimeLimit bool | ||
| IdentityProviderCustomEndpoint bool | ||
| ArgusCustomEndpoint bool | ||
@@ -68,2 +72,3 @@ AuthorizationCustomEndpoint bool | ||
| SecretsManagerCustomEndpoint bool | ||
| ServerBackupCustomEndpoint bool | ||
| ServiceAccountCustomEndpoint bool | ||
@@ -103,7 +108,11 @@ SKECustomEndpoint bool | ||
| } | ||
| if model.Verbosity { | ||
| viper.Set(config.VerbosityKey, globalflags.VerbosityDefault) | ||
| } | ||
| if model.SessionTimeLimit { | ||
| viper.Set(config.SessionTimeLimitKey, config.SessionTimeLimitDefault) | ||
| } | ||
| if model.Verbosity { | ||
| viper.Set(config.VerbosityKey, globalflags.VerbosityDefault) | ||
| if model.IdentityProviderCustomEndpoint { | ||
| viper.Set(config.IdentityProviderCustomEndpointKey, "") | ||
| } | ||
@@ -156,2 +165,5 @@ | ||
| } | ||
| if model.ServerBackupCustomEndpoint { | ||
| viper.Set(config.ServerBackupCustomEndpointKey, "") | ||
| } | ||
| if model.SKECustomEndpoint { | ||
@@ -179,5 +191,7 @@ viper.Set(config.SKECustomEndpointKey, "") | ||
| cmd.Flags().Bool(outputFormatFlag, false, "Output format") | ||
| cmd.Flags().Bool(sessionTimeLimitFlag, false, fmt.Sprintf("Maximum time before authentication is required again. If unset, defaults to %s", config.SessionTimeLimitDefault)) | ||
| cmd.Flags().Bool(verbosityFlag, false, "Verbosity of the CLI") | ||
| cmd.Flags().Bool(sessionTimeLimitFlag, false, fmt.Sprintf("Maximum time before authentication is required again. If unset, defaults to %s", config.SessionTimeLimitDefault)) | ||
| cmd.Flags().Bool(identityProviderCustomEndpointFlag, false, "Identity Provider base URL. If unset, uses the default base URL") | ||
| cmd.Flags().Bool(argusCustomEndpointFlag, false, "Argus API base URL. If unset, uses the default base URL") | ||
@@ -198,2 +212,3 @@ cmd.Flags().Bool(authorizationCustomEndpointFlag, false, "Authorization API base URL. If unset, uses the default base URL") | ||
| cmd.Flags().Bool(serviceAccountCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL") | ||
| cmd.Flags().Bool(serverBackupCustomEndpointFlag, false, "Server Backup base URL. If unset, uses the default base URL") | ||
| cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL") | ||
@@ -205,8 +220,10 @@ cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL") | ||
| model := inputModel{ | ||
| Async: flags.FlagToBoolValue(p, cmd, asyncFlag), | ||
| OutputFormat: flags.FlagToBoolValue(p, cmd, outputFormatFlag), | ||
| ProjectId: flags.FlagToBoolValue(p, cmd, projectIdFlag), | ||
| SessionTimeLimit: flags.FlagToBoolValue(p, cmd, sessionTimeLimitFlag), | ||
| Verbosity: flags.FlagToBoolValue(p, cmd, verbosityFlag), | ||
| Async: flags.FlagToBoolValue(p, cmd, asyncFlag), | ||
| OutputFormat: flags.FlagToBoolValue(p, cmd, outputFormatFlag), | ||
| ProjectId: flags.FlagToBoolValue(p, cmd, projectIdFlag), | ||
| Verbosity: flags.FlagToBoolValue(p, cmd, verbosityFlag), | ||
| SessionTimeLimit: flags.FlagToBoolValue(p, cmd, sessionTimeLimitFlag), | ||
| IdentityProviderCustomEndpoint: flags.FlagToBoolValue(p, cmd, identityProviderCustomEndpointFlag), | ||
| ArgusCustomEndpoint: flags.FlagToBoolValue(p, cmd, argusCustomEndpointFlag), | ||
@@ -227,2 +244,3 @@ AuthorizationCustomEndpoint: flags.FlagToBoolValue(p, cmd, authorizationCustomEndpointFlag), | ||
| ServiceAccountCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceAccountCustomEndpointFlag), | ||
| ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag), | ||
| SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag), | ||
@@ -229,0 +247,0 @@ SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag), |
@@ -9,3 +9,2 @@ package curl | ||
| "net/http/httputil" | ||
| "net/url" | ||
| "os" | ||
@@ -21,2 +20,3 @@ "strings" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
@@ -72,3 +72,3 @@ "github.com/spf13/cobra" | ||
| ), | ||
| Args: args.SingleArg(urlArg, validateURL), | ||
| Args: args.SingleArg(urlArg, utils.ValidateSTACKITURL), | ||
| RunE: func(cmd *cobra.Command, args []string) (err error) { | ||
@@ -119,17 +119,2 @@ model, err := parseInput(p, cmd, args) | ||
| func validateURL(value string) error { | ||
| urlStruct, err := url.Parse(value) | ||
| if err != nil { | ||
| return fmt.Errorf("parse URL: %w", err) | ||
| } | ||
| urlHost := urlStruct.Hostname() | ||
| if urlHost == "" { | ||
| return fmt.Errorf("bad url") | ||
| } | ||
| if !strings.HasSuffix(urlHost, "stackit.cloud") { | ||
| return fmt.Errorf("only urls belonging to STACKIT are permitted, hostname must end in stackit.cloud") | ||
| } | ||
| return nil | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
@@ -136,0 +121,0 @@ requestMethodOptions := []string{ |
@@ -48,6 +48,6 @@ package describe | ||
| `Get details of a MongoDB Flex user with ID "xxx" of instance with ID "yyy"`, | ||
| "$ stackit mongodbflex user list xxx --instance-id yyy"), | ||
| "$ stackit mongodbflex user describe xxx --instance-id yyy"), | ||
| examples.NewExample( | ||
| `Get details of a MongoDB Flex user with ID "xxx" of instance with ID "yyy" in JSON format`, | ||
| "$ stackit mongodbflex user list xxx --instance-id yyy --output-format json"), | ||
| "$ stackit mongodbflex user describe xxx --instance-id yyy --output-format json"), | ||
| ), | ||
@@ -54,0 +54,0 @@ Args: args.SingleArg(userIdArg, utils.ValidateUUID), |
@@ -15,2 +15,3 @@ package create | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
@@ -64,2 +65,13 @@ | ||
| // Check if the project is enabled before trying to create | ||
| enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId) | ||
| if err != nil { | ||
| return fmt.Errorf("check if Object Storage is enabled: %w", err) | ||
| } | ||
| if !enabled { | ||
| return &errors.ServiceDisabledError{ | ||
| Service: "object-storage", | ||
| } | ||
| } | ||
| // Call API | ||
@@ -66,0 +78,0 @@ req := buildRequest(ctx, model, apiClient) |
@@ -47,6 +47,6 @@ package describe | ||
| `Get details of a PostgreSQL Flex user with ID "xxx" of instance with ID "yyy"`, | ||
| "$ stackit postgresflex user list xxx --instance-id yyy"), | ||
| "$ stackit postgresflex user describe xxx --instance-id yyy"), | ||
| examples.NewExample( | ||
| `Get details of a PostgreSQL Flex user with ID "xxx" of instance with ID "yyy" in JSON format`, | ||
| "$ stackit postgresflex user list xxx --instance-id yyy --output-format json"), | ||
| "$ stackit postgresflex user describe xxx --instance-id yyy --output-format json"), | ||
| ), | ||
@@ -53,0 +53,0 @@ Args: args.SingleArg(userIdArg, nil), |
@@ -90,3 +90,3 @@ package create | ||
| // Check if SKE is enabled for this project | ||
| // Check if the project is enabled before trying to create | ||
| enabled, err := skeUtils.ProjectEnabled(ctx, apiClient, model.ProjectId) | ||
@@ -97,3 +97,5 @@ if err != nil { | ||
| if !enabled { | ||
| return fmt.Errorf("SKE isn't enabled for this project, please run 'stackit ske enable'") | ||
| return &errors.ServiceDisabledError{ | ||
| Service: "ske", | ||
| } | ||
| } | ||
@@ -100,0 +102,0 @@ |
@@ -26,4 +26,5 @@ package auth | ||
| const ( | ||
| authDomain = "auth.01.idp.eu01.stackit.cloud/oauth" | ||
| clientId = "stackit-cli-client-id" | ||
| defaultIDPEndpoint = "https://accounts.stackit.cloud/oauth/v2" | ||
| cliClientID = "stackit-cli-0000-0000-000000000001" | ||
| loginSuccessPath = "/login-successful" | ||
@@ -33,2 +34,7 @@ stackitLandingPage = "https://www.stackit.de" | ||
| loginSuccessfulHTMLFile = "login-successful.html" | ||
| // The IDP doesn't support wildcards for the port, | ||
| // so we configure a range of ports from 8000 to 8020 | ||
| defaultPort = 8000 | ||
| configuredPortRange = 20 | ||
| ) | ||
@@ -45,2 +51,14 @@ | ||
| func AuthorizeUser(p *print.Printer, isReauthentication bool) error { | ||
| idpEndpoint, err := getIDPEndpoint() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if idpEndpoint != defaultIDPEndpoint { | ||
| p.Warn("You are using a custom identity provider (%s) for authentication.\n", idpEndpoint) | ||
| err := p.PromptForEnter("Press Enter to proceed with the login...") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if isReauthentication { | ||
@@ -53,18 +71,28 @@ err := p.PromptForEnter("Your session has expired, press Enter to login again...") | ||
| listener, err := net.Listen("tcp", ":0") | ||
| if err != nil { | ||
| return fmt.Errorf("bind port for login redirect: %w", err) | ||
| var redirectURL string | ||
| var listener net.Listener | ||
| var listenerErr error | ||
| var port int | ||
| for i := range configuredPortRange { | ||
| port = defaultPort + i | ||
| portString := fmt.Sprintf(":%s", strconv.Itoa(port)) | ||
| p.Debug(print.DebugLevel, "trying to bind port %d for login redirect", port) | ||
| listener, listenerErr = net.Listen("tcp", portString) | ||
| if listenerErr == nil { | ||
| redirectURL = fmt.Sprintf("http://localhost:%d", port) | ||
| p.Debug(print.DebugLevel, "bound port %d for login redirect", port) | ||
| break | ||
| } | ||
| p.Debug(print.DebugLevel, "unable to bind port %d for login redirect: %s", port, listenerErr) | ||
| } | ||
| address, ok := listener.Addr().(*net.TCPAddr) | ||
| if !ok { | ||
| return fmt.Errorf("assert listener address type to TCP address") | ||
| if listenerErr != nil { | ||
| return fmt.Errorf("unable to bind port for login redirect, tried from port %d to %d: %w", defaultPort, port, err) | ||
| } | ||
| redirectURL := fmt.Sprintf("http://localhost:%d", address.Port) | ||
| conf := &oauth2.Config{ | ||
| ClientID: clientId, | ||
| ClientID: cliClientID, | ||
| Endpoint: oauth2.Endpoint{ | ||
| AuthURL: fmt.Sprintf("https://%s/authorize", authDomain), | ||
| AuthURL: fmt.Sprintf("%s/authorize", idpEndpoint), | ||
| }, | ||
| Scopes: []string{"openid"}, | ||
| Scopes: []string{"openid offline_access email"}, | ||
| RedirectURL: redirectURL, | ||
@@ -92,3 +120,3 @@ } | ||
| // Close the server only if there was an error | ||
| // Otherwise, it will redirect to the succesfull login page | ||
| // Otherwise, it will redirect to the successful login page | ||
| defer func() { | ||
@@ -111,3 +139,3 @@ if errServer != nil { | ||
| // Trade the authorization code and the code verifier for access and refresh tokens | ||
| accessToken, refreshToken, err := getUserAccessAndRefreshTokens(authDomain, clientId, codeVerifier, code, redirectURL) | ||
| accessToken, refreshToken, err := getUserAccessAndRefreshTokens(idpEndpoint, cliClientID, codeVerifier, code, redirectURL) | ||
| if err != nil { | ||
@@ -193,3 +221,3 @@ errServer = fmt.Errorf("retrieve tokens: %w", err) | ||
| p.Debug(print.DebugLevel, "opening browser for authentication") | ||
| p.Debug(print.DebugLevel, "using authentication server on %s", authDomain) | ||
| p.Debug(print.DebugLevel, "using authentication server on %s", idpEndpoint) | ||
@@ -221,3 +249,3 @@ // Open a browser window to the authorizationURL | ||
| // Set the authUrl and form-encoded data for the POST to the access token endpoint | ||
| authUrl := fmt.Sprintf("https://%s/token", authDomain) | ||
| authUrl := fmt.Sprintf("%s/token", authDomain) | ||
| data := fmt.Sprintf( | ||
@@ -224,0 +252,0 @@ "grant_type=authorization_code&client_id=%s"+ |
@@ -31,7 +31,11 @@ package auth | ||
| } | ||
| if reqURL == fmt.Sprintf("%s/token", authDomain) { | ||
| idpEndpoint, err := getIDPEndpoint() | ||
| if err != nil { | ||
| rt.t.Fatalf("get IDP endpoint for test: %v", err) | ||
| } | ||
| if fmt.Sprintf("https://%s", reqURL) == fmt.Sprintf("%s/token", idpEndpoint) { | ||
| return rt.roundTripRefreshTokens() | ||
| } | ||
| rt.t.Fatalf("unexpected request to \"%s\"", reqURL) | ||
| return nil, fmt.Errorf("unexpected request to \"%s\"", reqURL) | ||
| rt.t.Fatalf("unexpected request to %q", reqURL) | ||
| return nil, fmt.Errorf("unexpected request to %q", reqURL) | ||
| } | ||
@@ -167,2 +171,3 @@ | ||
| refreshTokenExpiresAt: time.Now().Add(-time.Hour), | ||
| refreshTokensFails: true, // Fails because refresh token is expired | ||
| isValid: true, | ||
@@ -195,5 +200,6 @@ expectedReautorizeUserCalled: true, | ||
| refreshTokenInvalid: true, | ||
| isValid: false, | ||
| expectedReautorizeUserCalled: false, | ||
| expectedTokensRefreshed: false, | ||
| refreshTokensFails: true, // Fails because refresh token is invalid | ||
| isValid: true, | ||
| expectedReautorizeUserCalled: true, | ||
| expectedTokensRefreshed: true, // Refreshed during reauthorization | ||
| }, | ||
@@ -213,2 +219,3 @@ { | ||
| refreshTokenExpiresAt: time.Now().Add(-time.Hour), | ||
| refreshTokensFails: true, // Fails because refresh token is expired | ||
| authorizeUserFails: true, | ||
@@ -309,16 +316,12 @@ isValid: false, | ||
| if !tt.isValid && err == nil { | ||
| if err == nil { | ||
| t.Errorf("should have failed") | ||
| } | ||
| if requestSent { | ||
| t.Errorf("request was sent") | ||
| t.Logf("request was sent") | ||
| } | ||
| t.Errorf("should have failed") | ||
| } | ||
| if tt.isValid && err != nil { | ||
| if err != nil { | ||
| t.Errorf("shouldn't have failed: %v", err) | ||
| } | ||
| if !requestSent { | ||
| t.Errorf("request wasn't sent") | ||
| t.Logf("request wasn't sent") | ||
| } | ||
| t.Errorf("shouldn't have failed: %v", err) | ||
| } | ||
@@ -325,0 +328,0 @@ if authorizeUserCalled && !tt.expectedReautorizeUserCalled { |
@@ -46,9 +46,8 @@ package auth | ||
| accessTokenValid := false | ||
| if accessTokenExpired, err := tokenExpired(utf.accessToken); err != nil { | ||
| accessTokenExpired, err := tokenExpired(utf.accessToken) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("check if access token has expired: %w", err) | ||
| } else if !accessTokenExpired { | ||
| accessTokenValid = true | ||
| } else if refreshTokenExpired, err := tokenExpired(utf.refreshToken); err != nil { | ||
| return nil, fmt.Errorf("check if refresh token has expired: %w", err) | ||
| } else if !refreshTokenExpired { | ||
| } else { | ||
| utf.printer.Debug(print.DebugLevel, "access token expired, refreshing...") | ||
@@ -59,3 +58,3 @@ err = refreshTokens(utf) | ||
| } else { | ||
| utf.printer.Debug(print.ErrorLevel, "refresh access token: %v", err) | ||
| utf.printer.Debug(print.ErrorLevel, "refresh access token: %w", err) | ||
| } | ||
@@ -162,5 +161,10 @@ } | ||
| func buildRequestToRefreshTokens(utf *userTokenFlow) (*http.Request, error) { | ||
| idpEndpoint, err := getIDPEndpoint() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| req, err := http.NewRequest( | ||
| http.MethodPost, | ||
| fmt.Sprintf("https://%s/token", authDomain), | ||
| fmt.Sprintf("%s/token", idpEndpoint), | ||
| http.NoBody, | ||
@@ -173,3 +177,3 @@ ) | ||
| reqQuery.Set("grant_type", "refresh_token") | ||
| reqQuery.Set("client_id", clientId) | ||
| reqQuery.Set("client_id", cliClientID) | ||
| reqQuery.Set("refresh_token", utf.refreshToken) | ||
@@ -179,5 +183,2 @@ reqQuery.Set("token_format", "jwt") | ||
| // without this header, the API returns error "An Authentication object was not found in the SecurityContext" | ||
| req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
| return req, nil | ||
@@ -184,0 +185,0 @@ } |
@@ -20,2 +20,4 @@ package config | ||
| IdentityProviderCustomEndpointKey = "identity_provider_custom_endpoint" | ||
| ArgusCustomEndpointKey = "argus_custom_endpoint" | ||
@@ -36,2 +38,3 @@ AuthorizationCustomEndpointKey = "authorization_custom_endpoint" | ||
| ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" | ||
| ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint" | ||
| SKECustomEndpointKey = "ske_custom_endpoint" | ||
@@ -65,2 +68,4 @@ SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint" | ||
| IdentityProviderCustomEndpointKey, | ||
| DNSCustomEndpointKey, | ||
@@ -82,2 +87,3 @@ LoadBalancerCustomEndpointKey, | ||
| ServiceAccountCustomEndpointKey, | ||
| ServerBackupCustomEndpointKey, | ||
| SKECustomEndpointKey, | ||
@@ -142,2 +148,3 @@ SQLServerFlexCustomEndpointKey, | ||
| viper.SetDefault(SessionTimeLimitKey, SessionTimeLimitDefault) | ||
| viper.SetDefault(IdentityProviderCustomEndpointKey, "") | ||
| viper.SetDefault(DNSCustomEndpointKey, "") | ||
@@ -153,2 +160,3 @@ viper.SetDefault(ArgusCustomEndpointKey, "") | ||
| viper.SetDefault(ServiceAccountCustomEndpointKey, "") | ||
| viper.SetDefault(ServerBackupCustomEndpointKey, "") | ||
| viper.SetDefault(SKECustomEndpointKey, "") | ||
@@ -155,0 +163,0 @@ viper.SetDefault(SQLServerFlexCustomEndpointKey, "") |
@@ -136,2 +136,7 @@ package errors | ||
| $ %s --help` | ||
| SERVICE_DISABLED = `This service isn't enabled for the current project. | ||
| To enable it, run: | ||
| $ stackit %s enable` | ||
| ) | ||
@@ -368,1 +373,9 @@ | ||
| } | ||
| type ServiceDisabledError struct { | ||
| Service string | ||
| } | ||
| func (e *ServiceDisabledError) Error() string { | ||
| return fmt.Sprintf(SERVICE_DISABLED, e.Service) | ||
| } |
@@ -6,2 +6,3 @@ package projectname | ||
| "fmt" | ||
| "os" | ||
@@ -45,3 +46,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| // (So next time we can just pull it from there) | ||
| if !isProjectIdSetInFlags(p, cmd) { | ||
| if !(isProjectIdSetInFlags(p, cmd) || isProjectIdSetInEnvVar()) { | ||
| viper.Set(config.ProjectNameKey, projectName) | ||
@@ -62,3 +63,4 @@ err = config.Write() | ||
| // - Project name in the config file is not empty | ||
| projectIdSet := isProjectIdSetInFlags(p, cmd) | ||
| projectIdSetInFlags := isProjectIdSetInFlags(p, cmd) | ||
| projectIdSetInEnv := isProjectIdSetInEnvVar() | ||
| projectName := viper.GetString(config.ProjectNameKey) | ||
@@ -69,3 +71,3 @@ projectNameSet := false | ||
| } | ||
| return !projectIdSet && projectNameSet | ||
| return !projectIdSetInFlags && !projectIdSetInEnv && projectNameSet | ||
| } | ||
@@ -84,1 +86,7 @@ | ||
| } | ||
| func isProjectIdSetInEnvVar() bool { | ||
| // Reads the project Id from the environment variable PROJECT_ID | ||
| _, projectIdSetInEnv := os.LookupEnv("STACKIT_PROJECT_ID") | ||
| return projectIdSetInEnv | ||
| } |
@@ -15,2 +15,3 @@ package utils | ||
| "github.com/stackitcloud/stackit-sdk-go/core/config" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" | ||
@@ -31,2 +32,4 @@ ) | ||
| type objectStorageClientMocked struct { | ||
| serviceDisabled bool | ||
| getServiceStatusFails bool | ||
| listCredentialsGroupsFails bool | ||
@@ -37,2 +40,12 @@ listCredentialsGroupsResp *objectstorage.ListCredentialsGroupsResponse | ||
| func (m *objectStorageClientMocked) GetServiceStatusExecute(_ context.Context, _ string) (*objectstorage.ProjectStatus, error) { | ||
| if m.getServiceStatusFails { | ||
| return nil, fmt.Errorf("could not get service status") | ||
| } | ||
| if m.serviceDisabled { | ||
| return nil, &oapierror.GenericOpenAPIError{StatusCode: 404} | ||
| } | ||
| return &objectstorage.ProjectStatus{}, nil | ||
| } | ||
| func (m *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) { | ||
@@ -49,2 +62,54 @@ if m.listCredentialsGroupsFails { | ||
| func TestProjectEnabled(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| serviceDisabled bool | ||
| getProjectFails bool | ||
| isValid bool | ||
| expectedOutput bool | ||
| }{ | ||
| { | ||
| description: "project enabled", | ||
| isValid: true, | ||
| expectedOutput: true, | ||
| }, | ||
| { | ||
| description: "project disabled (404)", | ||
| serviceDisabled: true, | ||
| isValid: true, | ||
| expectedOutput: false, | ||
| }, | ||
| { | ||
| description: "get project fails", | ||
| getProjectFails: true, | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| client := &objectStorageClientMocked{ | ||
| serviceDisabled: tt.serviceDisabled, | ||
| getServiceStatusFails: tt.getProjectFails, | ||
| } | ||
| output, err := ProjectEnabled(context.Background(), client, testProjectId) | ||
| if tt.isValid && err != nil { | ||
| fmt.Printf("failed on valid input: %v", err) | ||
| 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 %t, got %t", tt.expectedOutput, output) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestGetCredentialsGroupName(t *testing.T) { | ||
@@ -51,0 +116,0 @@ tests := []struct { |
@@ -6,3 +6,5 @@ package utils | ||
| "fmt" | ||
| "net/http" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" | ||
@@ -12,2 +14,3 @@ ) | ||
| type ObjectStorageClient interface { | ||
| GetServiceStatusExecute(ctx context.Context, projectId string) (*objectstorage.ProjectStatus, error) | ||
| ListCredentialsGroupsExecute(ctx context.Context, projectId string) (*objectstorage.ListCredentialsGroupsResponse, error) | ||
@@ -17,2 +20,17 @@ ListAccessKeys(ctx context.Context, projectId string) objectstorage.ApiListAccessKeysRequest | ||
| func ProjectEnabled(ctx context.Context, apiClient ObjectStorageClient, projectId string) (bool, error) { | ||
| _, err := apiClient.GetServiceStatusExecute(ctx, projectId) | ||
| if err != nil { | ||
| oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped | ||
| if !ok { | ||
| return false, err | ||
| } | ||
| if oapiErr.StatusCode == http.StatusNotFound { | ||
| return false, nil | ||
| } | ||
| return false, err | ||
| } | ||
| return true, nil | ||
| } | ||
| func GetCredentialsGroupName(ctx context.Context, apiClient ObjectStorageClient, projectId, credentialsGroupId string) (string, error) { | ||
@@ -19,0 +37,0 @@ resp, err := apiClient.ListCredentialsGroupsExecute(ctx, projectId) |
@@ -14,2 +14,3 @@ package utils | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/ske" | ||
@@ -27,2 +28,3 @@ ) | ||
| type skeClientMocked struct { | ||
| serviceDisabled bool | ||
| getServiceStatusFails bool | ||
@@ -40,2 +42,5 @@ getServiceStatusResp *ske.ProjectResponse | ||
| } | ||
| if m.serviceDisabled { | ||
| return nil, &oapierror.GenericOpenAPIError{StatusCode: 404} | ||
| } | ||
| return m.getServiceStatusResp, nil | ||
@@ -61,2 +66,3 @@ } | ||
| description string | ||
| serviceDisabled bool | ||
| getProjectFails bool | ||
@@ -74,2 +80,8 @@ getProjectResp *ske.ProjectResponse | ||
| { | ||
| description: "project disabled (404)", | ||
| serviceDisabled: true, | ||
| isValid: true, | ||
| expectedOutput: false, | ||
| }, | ||
| { | ||
| description: "project disabled 1", | ||
@@ -96,2 +108,3 @@ getProjectResp: &ske.ProjectResponse{State: ske.PROJECTSTATE_CREATING.Ptr()}, | ||
| client := &skeClientMocked{ | ||
| serviceDisabled: tt.serviceDisabled, | ||
| getServiceStatusFails: tt.getProjectFails, | ||
@@ -98,0 +111,0 @@ getServiceStatusResp: tt.getProjectResp, |
@@ -6,2 +6,3 @@ package utils | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
@@ -13,2 +14,3 @@ "path/filepath" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/ske" | ||
@@ -42,3 +44,10 @@ "golang.org/x/mod/semver" | ||
| if err != nil { | ||
| return false, fmt.Errorf("get SKE status: %w", err) | ||
| oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped | ||
| if !ok { | ||
| return false, err | ||
| } | ||
| if oapiErr.StatusCode == http.StatusNotFound { | ||
| return false, nil | ||
| } | ||
| return false, err | ||
| } | ||
@@ -45,0 +54,0 @@ return *project.State == ske.PROJECTSTATE_CREATED, nil |
@@ -39,2 +39,8 @@ package spinner | ||
| func (s *Spinner) StopWithError() { | ||
| s.done <- true | ||
| close(s.done) | ||
| s.printer.Info("\r%s ✗ \n", s.message) | ||
| } | ||
| func (s *Spinner) animate() { | ||
@@ -41,0 +47,0 @@ i := 0 |
| package utils | ||
| import "testing" | ||
| import ( | ||
| "testing" | ||
| ) | ||
@@ -46,1 +48,37 @@ func TestConvertInt64PToFloat64P(t *testing.T) { | ||
| } | ||
| func TestValidateSTACKITURL(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| isValid bool | ||
| }{ | ||
| { | ||
| name: "STACKIT URL", | ||
| input: "https://example.stackit.cloud", | ||
| isValid: true, | ||
| }, | ||
| { | ||
| name: "non-STACKIT URL", | ||
| input: "https://www.very-suspicious-website.com/", | ||
| isValid: false, | ||
| }, | ||
| { | ||
| name: "invalid URL", | ||
| input: "", | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidateSTACKITURL(tt.input) | ||
| if tt.isValid && err != nil { | ||
| t.Errorf("expected URL to be valid, got error: %v", err) | ||
| } | ||
| if !tt.isValid && err == nil { | ||
| t.Errorf("expected URL to be invalid, got no error") | ||
| } | ||
| }) | ||
| } | ||
| } |
@@ -5,2 +5,4 @@ package utils | ||
| "fmt" | ||
| "net/url" | ||
| "strings" | ||
@@ -52,1 +54,16 @@ "github.com/google/uuid" | ||
| } | ||
| func ValidateSTACKITURL(value string) error { | ||
| urlStruct, err := url.Parse(value) | ||
| if err != nil { | ||
| return fmt.Errorf("parse url: %w", err) | ||
| } | ||
| urlHost := urlStruct.Hostname() | ||
| if urlHost == "" { | ||
| return fmt.Errorf("bad url") | ||
| } | ||
| if !strings.HasSuffix(urlHost, "stackit.cloud") { | ||
| return fmt.Errorf(`only urls belonging to STACKIT are allowed, hostname must end in "stackit.cloud"`) | ||
| } | ||
| return nil | ||
| } |
+4
-1
@@ -10,2 +10,5 @@ ROOT_DIR ?= $(shell git rev-parse --show-toplevel) | ||
| fmt: | ||
| @gofmt -s -w . | ||
| # Setup and tool initialization tasks | ||
@@ -37,2 +40,2 @@ project-help: | ||
| @echo "Generating docs..." | ||
| @go run $(SCRIPTS_BASE)/generate.go | ||
| @go run $(SCRIPTS_BASE)/generate.go |
+10
-0
@@ -17,2 +17,11 @@ <div align="center"> | ||
| <a name="warning-new-stackit-idp"></a> | ||
| > [!WARNING] | ||
| > Starting on July 9 2024, the new [STACKIT Identity Provider (IDP)](https://docs.stackit.cloud/stackit/en/release-notes-23101442.html#ReleaseNotes-2024-06-21-identity-provider) will be available. | ||
| > | ||
| > On this date, we will release a new version of the STACKIT CLI that will use the new IDP for user authentication. This also means that the **user authentication on STACKIT CLI versions released before July 9 2024 is no longer guaranteed to work for all services**. | ||
| > | ||
| > Please make sure to **update your STACKIT CLI to the latest version after July 9 2024** to ensure that you can continue to use all STACKIT services. | ||
| ## Installation | ||
@@ -78,2 +87,3 @@ | ||
| | Secrets Manager | `secrets-manager` | :white_check_mark: | | ||
| | Server Backup Management | `beta server backup` | :white_check_mark: | | ||
| | Service Account | `service-account` | :white_check_mark: | | ||
@@ -80,0 +90,0 @@ | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | |