diff --git a/Makefile b/Makefile index ab57bb905..9a6d08844 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ GOLANG_CI_ARGS ?= --allow-parallel-runners --timeout=5m --config=${GOLANG_CI_YAM build: @go build -o ./bin/stackit +fmt: + @gofmt -s -w . + # Setup and tool initialization tasks project-help: @$(SCRIPTS_BASE)/project.sh help @@ -33,4 +36,4 @@ test: # Generate docs generate-docs: @echo "Generating docs..." - @go run $(SCRIPTS_BASE)/generate.go \ No newline at end of file + @go run $(SCRIPTS_BASE)/generate.go diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index 3fbc2395a..015a04b3f 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -40,5 +40,6 @@ stackit beta [flags] ### SEE ALSO * [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 diff --git a/docs/stackit_beta_server.md b/docs/stackit_beta_server.md new file mode 100644 index 000000000..d06925458 --- /dev/null +++ b/docs/stackit_beta_server.md @@ -0,0 +1,33 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup.md b/docs/stackit_beta_server_backup.md new file mode 100644 index 000000000..e2ae7ffda --- /dev/null +++ b/docs/stackit_beta_server_backup.md @@ -0,0 +1,35 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_disable.md b/docs/stackit_beta_server_backup_disable.md new file mode 100644 index 000000000..b2175dc86 --- /dev/null +++ b/docs/stackit_beta_server_backup_disable.md @@ -0,0 +1,40 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_enable.md b/docs/stackit_beta_server_backup_enable.md new file mode 100644 index 000000000..6a297f2ae --- /dev/null +++ b/docs/stackit_beta_server_backup_enable.md @@ -0,0 +1,40 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_schedule.md b/docs/stackit_beta_server_backup_schedule.md new file mode 100644 index 000000000..43655d725 --- /dev/null +++ b/docs/stackit_beta_server_backup_schedule.md @@ -0,0 +1,37 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_schedule_create.md b/docs/stackit_beta_server_backup_schedule_create.md new file mode 100644 index 000000000..42c05ca97 --- /dev/null +++ b/docs/stackit_beta_server_backup_schedule_create.md @@ -0,0 +1,49 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_schedule_delete.md b/docs/stackit_beta_server_backup_schedule_delete.md new file mode 100644 index 000000000..4df85027d --- /dev/null +++ b/docs/stackit_beta_server_backup_schedule_delete.md @@ -0,0 +1,40 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_schedule_describe.md b/docs/stackit_beta_server_backup_schedule_describe.md new file mode 100644 index 000000000..0dcd83a5f --- /dev/null +++ b/docs/stackit_beta_server_backup_schedule_describe.md @@ -0,0 +1,43 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_schedule_list.md b/docs/stackit_beta_server_backup_schedule_list.md new file mode 100644 index 000000000..5c53c99ae --- /dev/null +++ b/docs/stackit_beta_server_backup_schedule_list.md @@ -0,0 +1,44 @@ +## 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 + diff --git a/docs/stackit_beta_server_backup_schedule_update.md b/docs/stackit_beta_server_backup_schedule_update.md new file mode 100644 index 000000000..d238045cd --- /dev/null +++ b/docs/stackit_beta_server_backup_schedule_update.md @@ -0,0 +1,49 @@ +## 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 + diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 11c7861b7..0d9821f87 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -44,6 +44,7 @@ stackit config set [flags] --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 diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index b07b392a1..63c0d9779 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -44,6 +44,7 @@ stackit config unset [flags] --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 diff --git a/go.mod b/go.mod index 0c7fa1bcd..406c6cd17 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.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/sqlserverflex v0.2.0 diff --git a/go.sum b/go.sum index 0cecea262..16ccea2aa 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0 h1:qCbvGq github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0/go.mod h1:p16qz/pAW8b1gEhqMpIgJfutRPeDPqQLlbVGyCo3f8o= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0 h1:pJBG455kmtbQFpCxcBfBK8wOuEnmsMv3h90LFcdj3q0= 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= diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index ed6873b0e..e513b94e3 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -3,6 +3,7 @@ package beta import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -36,4 +37,5 @@ func NewCmd(p *print.Printer) *cobra.Command { func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(sqlserverflex.NewCmd(p)) + cmd.AddCommand(server.NewCmd(p)) } diff --git a/internal/cmd/beta/server/backup/backup.go b/internal/cmd/beta/server/backup/backup.go new file mode 100644 index 000000000..54bcf1f3b --- /dev/null +++ b/internal/cmd/beta/server/backup/backup.go @@ -0,0 +1,30 @@ +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)) +} diff --git a/internal/cmd/beta/server/backup/disable/disable.go b/internal/cmd/beta/server/backup/disable/disable.go new file mode 100644 index 000000000..a40b23fa0 --- /dev/null +++ b/internal/cmd/beta/server/backup/disable/disable.go @@ -0,0 +1,118 @@ +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 +} diff --git a/internal/cmd/beta/server/backup/disable/disable_test.go b/internal/cmd/beta/server/backup/disable/disable_test.go new file mode 100644 index 000000000..17f9583b2 --- /dev/null +++ b/internal/cmd/beta/server/backup/disable/disable_test.go @@ -0,0 +1,173 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/backup/enable/enable.go b/internal/cmd/beta/server/backup/enable/enable.go new file mode 100644 index 000000000..885f0e6c3 --- /dev/null +++ b/internal/cmd/beta/server/backup/enable/enable.go @@ -0,0 +1,112 @@ +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 +} diff --git a/internal/cmd/beta/server/backup/enable/enable_test.go b/internal/cmd/beta/server/backup/enable/enable_test.go new file mode 100644 index 000000000..83b06f9d5 --- /dev/null +++ b/internal/cmd/beta/server/backup/enable/enable_test.go @@ -0,0 +1,173 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/backup/schedule/create/create.go b/internal/cmd/beta/server/backup/schedule/create/create.go new file mode 100644 index 000000000..b87a4b0e5 --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/create/create.go @@ -0,0 +1,188 @@ +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 + } +} diff --git a/internal/cmd/beta/server/backup/schedule/create/create_test.go b/internal/cmd/beta/server/backup/schedule/create/create_test.go new file mode 100644 index 000000000..eb772508c --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/create/create_test.go @@ -0,0 +1,220 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/backup/schedule/delete/delete.go b/internal/cmd/beta/server/backup/schedule/delete/delete.go new file mode 100644 index 000000000..60e30643a --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/delete/delete.go @@ -0,0 +1,113 @@ +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 +} diff --git a/internal/cmd/beta/server/backup/schedule/delete/delete_test.go b/internal/cmd/beta/server/backup/schedule/delete/delete_test.go new file mode 100644 index 000000000..875f1f2db --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/delete/delete_test.go @@ -0,0 +1,209 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/backup/schedule/describe/describe.go b/internal/cmd/beta/server/backup/schedule/describe/describe.go new file mode 100644 index 000000000..27557b2c6 --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/describe/describe.go @@ -0,0 +1,159 @@ +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 + } +} diff --git a/internal/cmd/beta/server/backup/schedule/describe/describe_test.go b/internal/cmd/beta/server/backup/schedule/describe/describe_test.go new file mode 100644 index 000000000..985989f62 --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/describe/describe_test.go @@ -0,0 +1,211 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/backup/schedule/list/list.go b/internal/cmd/beta/server/backup/schedule/list/list.go new file mode 100644 index 000000000..9ddde5860 --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/list/list.go @@ -0,0 +1,166 @@ +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 + } +} diff --git a/internal/cmd/beta/server/backup/schedule/list/list_test.go b/internal/cmd/beta/server/backup/schedule/list/list_test.go new file mode 100644 index 000000000..e8667ba02 --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/list/list_test.go @@ -0,0 +1,188 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/backup/schedule/schedule.go b/internal/cmd/beta/server/backup/schedule/schedule.go new file mode 100644 index 000000000..73f026c5b --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/schedule.go @@ -0,0 +1,34 @@ +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)) +} diff --git a/internal/cmd/beta/server/backup/schedule/update/update.go b/internal/cmd/beta/server/backup/schedule/update/update.go new file mode 100644 index 000000000..732062d70 --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/update/update.go @@ -0,0 +1,215 @@ +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 + } +} diff --git a/internal/cmd/beta/server/backup/schedule/update/update_test.go b/internal/cmd/beta/server/backup/schedule/update/update_test.go new file mode 100644 index 000000000..95ec1fc1a --- /dev/null +++ b/internal/cmd/beta/server/backup/schedule/update/update_test.go @@ -0,0 +1,277 @@ +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) + } + }) + } +} diff --git a/internal/cmd/beta/server/server.go b/internal/cmd/beta/server/server.go new file mode 100644 index 000000000..8208d5b5d --- /dev/null +++ b/internal/cmd/beta/server/server.go @@ -0,0 +1,26 @@ +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)) +} diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index f2ad14ecd..70d3a5f54 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -33,6 +33,7 @@ const ( redisCustomEndpointFlag = "redis-custom-endpoint" resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint" + serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" @@ -139,6 +140,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(resourceManagerCustomEndpointFlag, "", "Resource Manager API base URL, used in calls to this API") cmd.Flags().String(secretsManagerCustomEndpointFlag, "", "Secrets Manager 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") @@ -170,6 +172,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.SecretsManagerCustomEndpointKey, cmd.Flags().Lookup(secretsManagerCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.ServerBackupCustomEndpointKey, cmd.Flags().Lookup(serverBackupCustomEndpointFlag)) + cobra.CheckErr(err) err = viper.BindPFlag(config.ServiceAccountCustomEndpointKey, cmd.Flags().Lookup(serviceAccountCustomEndpointFlag)) cobra.CheckErr(err) err = viper.BindPFlag(config.SKECustomEndpointKey, cmd.Flags().Lookup(skeCustomEndpointFlag)) diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index b49d213da..b5d8e494f 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -37,6 +37,7 @@ const ( resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint" serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" + serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" ) @@ -62,6 +63,7 @@ type inputModel struct { RedisCustomEndpoint bool ResourceManagerCustomEndpoint bool SecretsManagerCustomEndpoint bool + ServerBackupCustomEndpoint bool ServiceAccountCustomEndpoint bool SKECustomEndpoint bool SQLServerFlexCustomEndpoint bool @@ -148,6 +150,9 @@ func NewCmd(p *print.Printer) *cobra.Command { if model.ServiceAccountCustomEndpoint { viper.Set(config.ServiceAccountCustomEndpointKey, "") } + if model.ServerBackupCustomEndpoint { + viper.Set(config.ServerBackupCustomEndpointKey, "") + } if model.SKECustomEndpoint { viper.Set(config.SKECustomEndpointKey, "") } @@ -188,6 +193,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(resourceManagerCustomEndpointFlag, false, "Resource Manager API base URL. If unset, uses the default base URL") cmd.Flags().Bool(secretsManagerCustomEndpointFlag, false, "Secrets Manager 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") cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL") } @@ -215,6 +221,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { ResourceManagerCustomEndpoint: flags.FlagToBoolValue(p, cmd, resourceManagerCustomEndpointFlag), SecretsManagerCustomEndpoint: flags.FlagToBoolValue(p, cmd, secretsManagerCustomEndpointFlag), ServiceAccountCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceAccountCustomEndpointFlag), + ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag), SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag), SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag), } diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 92d760471..7c89f4dd5 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -30,6 +30,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool resourceManagerCustomEndpointFlag: true, secretsManagerCustomEndpointFlag: true, serviceAccountCustomEndpointFlag: true, + serverBackupCustomEndpointFlag: true, skeCustomEndpointFlag: true, sqlServerFlexCustomEndpointFlag: true, } @@ -60,6 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ResourceManagerCustomEndpoint: true, SecretsManagerCustomEndpoint: true, ServiceAccountCustomEndpoint: true, + ServerBackupCustomEndpoint: true, SKECustomEndpoint: true, SQLServerFlexCustomEndpoint: true, } @@ -106,6 +108,7 @@ func TestParseInput(t *testing.T) { model.ResourceManagerCustomEndpoint = false model.SecretsManagerCustomEndpoint = false model.ServiceAccountCustomEndpoint = false + model.ServerBackupCustomEndpoint = false model.SKECustomEndpoint = false model.SQLServerFlexCustomEndpoint = false }), @@ -190,6 +193,16 @@ func TestParseInput(t *testing.T) { model.ResourceManagerCustomEndpoint = false }), }, + { + 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 + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index a27b13eff..8d174e399 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -32,6 +32,7 @@ const ( ResourceManagerEndpointKey = "resource_manager_custom_endpoint" SecretsManagerCustomEndpointKey = "secrets_manager_custom_endpoint" ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" + ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint" SKECustomEndpointKey = "ske_custom_endpoint" SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint" @@ -76,6 +77,7 @@ var ConfigKeys = []string{ ResourceManagerEndpointKey, SecretsManagerCustomEndpointKey, ServiceAccountCustomEndpointKey, + ServerBackupCustomEndpointKey, SKECustomEndpointKey, SQLServerFlexCustomEndpointKey, } @@ -145,6 +147,7 @@ func setConfigDefaults() { viper.SetDefault(ResourceManagerEndpointKey, "") viper.SetDefault(SecretsManagerCustomEndpointKey, "") viper.SetDefault(ServiceAccountCustomEndpointKey, "") + viper.SetDefault(ServerBackupCustomEndpointKey, "") viper.SetDefault(SKECustomEndpointKey, "") viper.SetDefault(SQLServerFlexCustomEndpointKey, "") } diff --git a/internal/pkg/services/serverbackup/client/client.go b/internal/pkg/services/serverbackup/client/client.go new file mode 100644 index 000000000..87a96bc94 --- /dev/null +++ b/internal/pkg/services/serverbackup/client/client.go @@ -0,0 +1,46 @@ +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 +} diff --git a/internal/pkg/services/serverbackup/utils/utils.go b/internal/pkg/services/serverbackup/utils/utils.go new file mode 100644 index 000000000..1dc32dfec --- /dev/null +++ b/internal/pkg/services/serverbackup/utils/utils.go @@ -0,0 +1,34 @@ +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 +} diff --git a/internal/pkg/services/serverbackup/utils/utils_test.go b/internal/pkg/services/serverbackup/utils/utils_test.go new file mode 100644 index 000000000..a25ce9ba5 --- /dev/null +++ b/internal/pkg/services/serverbackup/utils/utils_test.go @@ -0,0 +1,145 @@ +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) + } + }) + } +}