From 2438512651a88abd88c4b82c754adf10c26b178d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Fri, 10 May 2024 15:05:15 +0100 Subject: [PATCH 1/8] initial update schedule implementation --- internal/cmd/mongodbflex/backup/backup.go | 32 +++ .../cmd/mongodbflex/backup/restore/restore.go | 1 + .../backup/update-schedule/update_schedule.go | 113 +++++++++ .../update-schedule/update_schedule_test.go | 224 ++++++++++++++++++ .../mongodbflex/instance/describe/describe.go | 2 + 5 files changed, 372 insertions(+) create mode 100644 internal/cmd/mongodbflex/backup/backup.go create mode 100644 internal/cmd/mongodbflex/backup/restore/restore.go create mode 100644 internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go create mode 100644 internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go diff --git a/internal/cmd/mongodbflex/backup/backup.go b/internal/cmd/mongodbflex/backup/backup.go new file mode 100644 index 000000000..2e1b1619d --- /dev/null +++ b/internal/cmd/mongodbflex/backup/backup.go @@ -0,0 +1,32 @@ +package backup + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/list" + restorejobs "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore-jobs" + updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/update-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 MongoDB Flex instance backups", + Long: "Provides functionality for MongoDB Flex instance backups.", + 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(restorejobs.NewCmd(p)) + cmd.AddCommand(updateschedule.NewCmd(p)) +} diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go new file mode 100644 index 000000000..9b70a15f0 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -0,0 +1 @@ +package restore diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go new file mode 100644 index 000000000..a900d2c2b --- /dev/null +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go @@ -0,0 +1,113 @@ +package updateschedule + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + mongoDBflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" + scheduleFlag = "schedule" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + InstanceId *string + BackupSchedule *string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "update-schedule", + Short: "Updates backup schedule for a MongoDB Flex instance", + Long: `Updates backup schedule for a MongoDB Flex instance. The current backup schedule can be seen in the output of the "stackit mongodbflex instance describe" command.`, + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update the backup schedule of a MongoDB Flex instance with ID "xxx"`, + "$ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *'"), + ), + + 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 + } + + instanceLabel, err := mongoDBflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = *model.InstanceId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("update backup schedule of MongoDB Flex instance: %w", err) + } + + cmd.Printf("Updated backup schedule of instance %q\n", instanceLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().String(scheduleFlag, "", "Backup schedule, in the cron scheduling system format e.g. '0 0 * * *'") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag, scheduleFlag) + 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{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + BackupSchedule: flags.FlagToStringPointer(p, cmd, scheduleFlag), + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiUpdateBackupScheduleRequest { + req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, *model.InstanceId) + req = req.UpdateBackupSchedulePayload(mongodbflex.UpdateBackupSchedulePayload{ + BackupSchedule: model.BackupSchedule, + }) + return req +} diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go new file mode 100644 index 000000000..797373ca0 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go @@ -0,0 +1,224 @@ +package updateschedule + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testSchedule = "0 0 * * *" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + scheduleFlag: testSchedule, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: utils.Ptr(testInstanceId), + BackupSchedule: &testSchedule, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixturePayload(mods ...func(payload *mongodbflex.UpdateBackupSchedulePayload)) mongodbflex.UpdateBackupSchedulePayload { + payload := mongodbflex.UpdateBackupSchedulePayload{ + BackupSchedule: utils.Ptr(testSchedule), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiUpdateBackupScheduleRequest)) mongodbflex.ApiUpdateBackupScheduleRequest { + request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId) + request = request.UpdateBackupSchedulePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +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: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "backup schedule missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, scheduleFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd(nil) + 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(nil, 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 mongodbflex.ApiUpdateBackupScheduleRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "required fields only", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: utils.Ptr(testInstanceId), + }, + expectedRequest: testClient.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId). + UpdateBackupSchedulePayload(mongodbflex.UpdateBackupSchedulePayload{}), + }, + } + + 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/mongodbflex/instance/describe/describe.go b/internal/cmd/mongodbflex/instance/describe/describe.go index 6d7fda1d5..ec1311991 100644 --- a/internal/cmd/mongodbflex/instance/describe/describe.go +++ b/internal/cmd/mongodbflex/instance/describe/describe.go @@ -141,6 +141,8 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I table.AddSeparator() table.AddRow("RAM", *instance.Flavor.Memory) table.AddSeparator() + table.AddRow("BACKUP SCHEDULE", *instance.BackupSchedule) + table.AddSeparator() err = table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) From aeb006779d08b981fc458c2e1a662f48301da7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Mon, 13 May 2024 11:21:21 +0100 Subject: [PATCH 2/8] implement update and list schedule commands, add testing --- docs/stackit_mongodbflex_backup.md | 37 ++++ ...stackit_mongodbflex_backup_restore-jobs.md | 47 +++++ docs/stackit_mongodbflex_backup_schedule.md | 43 ++++ ...ckit_mongodbflex_backup_update-schedule.md | 51 +++++ internal/cmd/mongodbflex/backup/backup.go | 2 + .../mongodbflex/backup/schedule/schedule.go | 151 ++++++++++++++ .../backup/schedule/schedule_test.go | 195 ++++++++++++++++++ .../backup/update-schedule/update_schedule.go | 152 ++++++++++++-- .../update-schedule/update_schedule_test.go | 120 ++++++++++- 9 files changed, 772 insertions(+), 26 deletions(-) create mode 100644 docs/stackit_mongodbflex_backup.md create mode 100644 docs/stackit_mongodbflex_backup_restore-jobs.md create mode 100644 docs/stackit_mongodbflex_backup_schedule.md create mode 100644 docs/stackit_mongodbflex_backup_update-schedule.md create mode 100644 internal/cmd/mongodbflex/backup/schedule/schedule.go create mode 100644 internal/cmd/mongodbflex/backup/schedule/schedule_test.go diff --git a/docs/stackit_mongodbflex_backup.md b/docs/stackit_mongodbflex_backup.md new file mode 100644 index 000000000..e50efa668 --- /dev/null +++ b/docs/stackit_mongodbflex_backup.md @@ -0,0 +1,37 @@ +## stackit mongodbflex backup + +Provides functionality for MongoDB Flex instance backups + +### Synopsis + +Provides functionality for MongoDB Flex instance backups. + +``` +stackit mongodbflex backup [flags] +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex +* [stackit mongodbflex backup describe](./stackit_mongodbflex_backup_describe.md) - Shows details of a backup for a MongoDB Flex instance +* [stackit mongodbflex backup list](./stackit_mongodbflex_backup_list.md) - Lists all backups which are available for a MongoDB Flex instance +* [stackit mongodbflex backup restore-jobs](./stackit_mongodbflex_backup_restore-jobs.md) - Lists all restore jobs which have been run for a MongoDB Flex instance +* [stackit mongodbflex backup schedule](./stackit_mongodbflex_backup_schedule.md) - Shows details of the backup schedule and retention policy of a MongoDB Flex instance +* [stackit mongodbflex backup update-schedule](./stackit_mongodbflex_backup_update-schedule.md) - Updates the backup schedule and retention policy for a MongoDB Flex instance + diff --git a/docs/stackit_mongodbflex_backup_restore-jobs.md b/docs/stackit_mongodbflex_backup_restore-jobs.md new file mode 100644 index 000000000..af5626306 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_restore-jobs.md @@ -0,0 +1,47 @@ +## stackit mongodbflex backup restore-jobs + +Lists all restore jobs which have been run for a MongoDB Flex instance + +### Synopsis + +Lists all restore jobs which have been run for a MongoDB Flex instance. + +``` +stackit mongodbflex backup restore-jobs [flags] +``` + +### Examples + +``` + List all restore jobs of instance with ID "xxx" + $ stackit mongodbflex backup restore-jobs --instance-id xxx + + List all restore jobs of instance with ID "xxx" in JSON format + $ stackit mongodbflex backup restore-jobs --instance-id xxx --output-format json + + List up to 10 restore jobs of instance with ID "xxx" + $ stackit mongodbflex backup restore-jobs --instance-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup restore-jobs" + --instance-id string Instance ID + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_schedule.md b/docs/stackit_mongodbflex_backup_schedule.md new file mode 100644 index 000000000..8993c63c4 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_schedule.md @@ -0,0 +1,43 @@ +## stackit mongodbflex backup schedule + +Shows details of the backup schedule and retention policy of a MongoDB Flex instance + +### Synopsis + +Shows details of the backup schedule and retention policy of a MongoDB Flex instance. + +``` +stackit mongodbflex backup schedule [flags] +``` + +### Examples + +``` + Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" + $ stackit mongodbflex backup schedule --instance-id xxx + + Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format + $ stackit mongodbflex backup schedule --instance-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup schedule" + --instance-id string Instance ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_update-schedule.md b/docs/stackit_mongodbflex_backup_update-schedule.md new file mode 100644 index 000000000..38e1e69dc --- /dev/null +++ b/docs/stackit_mongodbflex_backup_update-schedule.md @@ -0,0 +1,51 @@ +## stackit mongodbflex backup update-schedule + +Updates the backup schedule and retention policy for a MongoDB Flex instance + +### Synopsis + +Updates the backup schedule and retention policy for a MongoDB Flex instance. +The current backup schedule and retention policy can be seen in the output of the "stackit mongodbflex backup schedule" command. +The backup schedule is defined in the cron scheduling system format e.g. '0 0 * * *'. +See below for more detail on the retention policy options. + +``` +stackit mongodbflex backup update-schedule [flags] +``` + +### Examples + +``` + Update the backup schedule of a MongoDB Flex instance with ID "xxx" + $ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *' + + Update the retention days for snapshots of a MongoDB Flex instance with ID "xxx" to 5 days + $ stackit mongodbflex backup update-schedule --instance-id xxx --save-snapshot-days 5 +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup update-schedule" + --instance-id string Instance ID + --save-daily-snapshot-days int Number of days to retain daily snapshots. Should be less than or equal to the number of days of the selected weekly or monthly value. + --save-monthly-snapshot-months int Number of months to retain monthly snapshots + --save-snapshot-days int Number of days to retain snapshots. Should be less than or equal to the value of the daily backup. + --save-weekly-snapshot-weeks int Number of weeks to retain weekly snapshots. Should be less than or equal to the number of weeks of the selected monthly value. + --schedule string Backup schedule, in the cron scheduling system format e.g. '0 0 * * *' +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/internal/cmd/mongodbflex/backup/backup.go b/internal/cmd/mongodbflex/backup/backup.go index 2e1b1619d..aee739024 100644 --- a/internal/cmd/mongodbflex/backup/backup.go +++ b/internal/cmd/mongodbflex/backup/backup.go @@ -4,6 +4,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/list" restorejobs "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore-jobs" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/schedule" updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/update-schedule" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/print" @@ -29,4 +30,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(describe.NewCmd(p)) cmd.AddCommand(restorejobs.NewCmd(p)) cmd.AddCommand(updateschedule.NewCmd(p)) + cmd.AddCommand(schedule.NewCmd(p)) } diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule.go b/internal/cmd/mongodbflex/backup/schedule/schedule.go new file mode 100644 index 000000000..6a68c75d2 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/schedule/schedule.go @@ -0,0 +1,151 @@ +package schedule + +import ( + "context" + "encoding/json" + "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/mongodbflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "schedule", + Short: "Shows details of the backup schedule and retention policy of a MongoDB Flex instance", + Long: "Shows details of the backup schedule and retention policy of a MongoDB Flex instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Get details of the backup schedule of a MongoDB Flex instance with ID "xxx"`, + "$ stackit mongodbflex backup schedule --instance-id xxx"), + examples.NewExample( + `Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format`, + "$ stackit mongodbflex backup schedule --instance-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("read MongoDB Flex instance: %w", err) + } + + return outputResult(p, model.OutputFormat, resp.Item) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: *flags.FlagToStringPointer(p, cmd, instanceIdFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { + req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.Instance) error { + switch outputFormat { + case print.JSONOutputFormat: + output := struct { + BackupSchedule string `json:"backup_schedule"` + DailySnaphotRetentionDays string `json:"daily_snapshot_retention_days"` + MonthlySnapshotRetentionMonths string `json:"monthly_snapshot_retention_months"` + PointInTimeWindowHours string `json:"point_in_time_window_hours"` + SnapshotRetentionDays string `json:"snapshot_retention_days"` + WeeklySnapshotRetentionWeeks string `json:"weekly_snapshot_retention_weeks"` + }{ + BackupSchedule: *instance.BackupSchedule, + DailySnaphotRetentionDays: (*instance.Options)["dailySnapshotRetentionDays"], + MonthlySnapshotRetentionMonths: (*instance.Options)["monthlySnapshotRetentionDays"], + PointInTimeWindowHours: (*instance.Options)["pointInTimeWindowHours"], + SnapshotRetentionDays: (*instance.Options)["snapshotRetentionDays"], + WeeklySnapshotRetentionWeeks: (*instance.Options)["weeklySnapshotRetentionWeeks"], + } + details, err := json.MarshalIndent(output, "", " ") + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backup schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("BACKUP SCHEDULE", *instance.BackupSchedule) + table.AddSeparator() + table.AddRow("DAILY SNAPSHOT RETENTION DAYS", (*instance.Options)["dailySnapshotRetentionDays"]) + table.AddSeparator() + table.AddRow("MONTHLY SNAPSHOT RETENTION MONTHS", (*instance.Options)["monthlySnapshotRetentionMonths"]) + table.AddSeparator() + table.AddRow("POINT IN TIME WINDOW HOURS", (*instance.Options)["pointInTimeWindowHours"]) + table.AddSeparator() + table.AddRow("SNAPSHOT RETENTION DAYS", (*instance.Options)["snapshotRetentionDays"]) + table.AddSeparator() + table.AddRow("WEEKLY SNAPSHOT RETENTION WEEKS", (*instance.Options)["weeklySnapshotRetentionWeeks"]) + table.AddSeparator() + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule_test.go b/internal/cmd/mongodbflex/backup/schedule/schedule_test.go new file mode 100644 index 000000000..e25957058 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/schedule/schedule_test.go @@ -0,0 +1,195 @@ +package schedule + +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/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { + request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + p := print.NewPrinter() + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiGetInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go index a900d2c2b..cf296ea92 100644 --- a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go @@ -3,6 +3,7 @@ package updateschedule import ( "context" "fmt" + "strconv" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -13,31 +14,56 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" mongoDBflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( - instanceIdFlag = "instance-id" - scheduleFlag = "schedule" + instanceIdFlag = "instance-id" + scheduleFlag = "schedule" + snapshotRetentionDaysFlag = "save-snapshot-days" + dailySnapshotRetentionDaysFlag = "save-daily-snapshot-days" + weeklySnapshotRetentionWeeksFlag = "save-weekly-snapshot-weeks" + monthlySnapshotRetentionMonthsFlag = "save-monthly-snapshot-months" + + // Default values for the backup schedule options + defaultBackupSchedule = "0 0/6 * * *" + defaultSnapshotRetentionDays int64 = 3 + defaultDailySnapshotRetentionDays int64 = 0 + defaultWeeklySnapshotRetentionWeeks int64 = 3 + defaultMonthlySnapshotRetentionMonths int64 = 1 + defaultPointInTimeWindowHours int64 = 30 ) type inputModel struct { *globalflags.GlobalFlagModel - InstanceId *string - BackupSchedule *string + InstanceId *string + BackupSchedule *string + SnapshotRetentionDays *int64 + DailySnaphotRetentionDays *int64 + WeeklySnapshotRetentionWeeks *int64 + MonthlySnapshotRetentionMonths *int64 } func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "update-schedule", - Short: "Updates backup schedule for a MongoDB Flex instance", - Long: `Updates backup schedule for a MongoDB Flex instance. The current backup schedule can be seen in the output of the "stackit mongodbflex instance describe" command.`, - Args: args.NoArgs, + Short: "Updates the backup schedule and retention policy for a MongoDB Flex instance", + Long: fmt.Sprintf("%s\n%s\n%s\n%s", + "Updates the backup schedule and retention policy for a MongoDB Flex instance.", + `The current backup schedule and retention policy can be seen in the output of the "stackit mongodbflex backup schedule" command.`, + "The backup schedule is defined in the cron scheduling system format e.g. '0 0 * * *'.", + "See below for more detail on the retention policy options.", + ), + Args: args.NoArgs, Example: examples.Build( examples.NewExample( `Update the backup schedule of a MongoDB Flex instance with ID "xxx"`, "$ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *'"), + examples.NewExample( + `Update the retention days for snapshots of a MongoDB Flex instance with ID "xxx" to 5 days`, + "$ stackit mongodbflex backup update-schedule --instance-id xxx --save-snapshot-days 5"), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -68,8 +94,17 @@ func NewCmd(p *print.Printer) *cobra.Command { } } + // Get current instance + getReq := buildGetInstanceRequest(ctx, model, apiClient) + getResp, err := getReq.Execute() + if err != nil { + return fmt.Errorf("get MongoDB Flex instance %q: %w", instanceLabel, err) + } + + instance := getResp.Item + // Call API - req := buildRequest(ctx, model, apiClient) + req := buildUpdateBackupScheduleRequest(ctx, model, instance, apiClient) _, err = req.Execute() if err != nil { return fmt.Errorf("update backup schedule of MongoDB Flex instance: %w", err) @@ -86,8 +121,12 @@ func NewCmd(p *print.Printer) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") cmd.Flags().String(scheduleFlag, "", "Backup schedule, in the cron scheduling system format e.g. '0 0 * * *'") + cmd.Flags().Int64(snapshotRetentionDaysFlag, 0, "Number of days to retain snapshots. Should be less than or equal to the value of the daily backup.") + cmd.Flags().Int64(dailySnapshotRetentionDaysFlag, 0, "Number of days to retain daily snapshots. Should be less than or equal to the number of days of the selected weekly or monthly value.") + cmd.Flags().Int64(weeklySnapshotRetentionWeeksFlag, 0, "Number of weeks to retain weekly snapshots. Should be less than or equal to the number of weeks of the selected monthly value.") + cmd.Flags().Int64(monthlySnapshotRetentionMonthsFlag, 0, "Number of months to retain monthly snapshots") - err := flags.MarkFlagsRequired(cmd, instanceIdFlag, scheduleFlag) + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) } @@ -97,17 +136,98 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return nil, &cliErr.ProjectIdError{} } + schedule := flags.FlagToStringPointer(p, cmd, scheduleFlag) + snapshotRetentionDays := flags.FlagToInt64Pointer(p, cmd, snapshotRetentionDaysFlag) + dailySnapshotRetentionDays := flags.FlagToInt64Pointer(p, cmd, dailySnapshotRetentionDaysFlag) + weeklySnapshotRetentionWeeks := flags.FlagToInt64Pointer(p, cmd, weeklySnapshotRetentionWeeksFlag) + monthlySnapshotRetentionMonths := flags.FlagToInt64Pointer(p, cmd, monthlySnapshotRetentionMonthsFlag) + + if schedule == nil && snapshotRetentionDays == nil && dailySnapshotRetentionDays == nil && weeklySnapshotRetentionWeeks == nil && monthlySnapshotRetentionMonths == nil { + return nil, &cliErr.EmptyUpdateError{} + } + return &inputModel{ - GlobalFlagModel: globalFlags, - InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), - BackupSchedule: flags.FlagToStringPointer(p, cmd, scheduleFlag), + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + BackupSchedule: schedule, + DailySnaphotRetentionDays: dailySnapshotRetentionDays, + MonthlySnapshotRetentionMonths: monthlySnapshotRetentionMonths, + SnapshotRetentionDays: snapshotRetentionDays, + WeeklySnapshotRetentionWeeks: weeklySnapshotRetentionWeeks, }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiUpdateBackupScheduleRequest { +func buildUpdateBackupScheduleRequest(ctx context.Context, model *inputModel, instance *mongodbflex.Instance, apiClient *mongodbflex.APIClient) mongodbflex.ApiUpdateBackupScheduleRequest { req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, *model.InstanceId) - req = req.UpdateBackupSchedulePayload(mongodbflex.UpdateBackupSchedulePayload{ - BackupSchedule: model.BackupSchedule, - }) + + payload := getUpdateBackupSchedulePayload(instance) + + if model.BackupSchedule != nil { + payload.BackupSchedule = model.BackupSchedule + } + if model.DailySnaphotRetentionDays != nil { + payload.DailySnapshotRetentionDays = model.DailySnaphotRetentionDays + } + if model.MonthlySnapshotRetentionMonths != nil { + payload.MonthlySnapshotRetentionMonths = model.MonthlySnapshotRetentionMonths + } + if model.SnapshotRetentionDays != nil { + payload.SnapshotRetentionDays = model.SnapshotRetentionDays + } + if model.WeeklySnapshotRetentionWeeks != nil { + payload.WeeklySnapshotRetentionWeeks = model.WeeklySnapshotRetentionWeeks + } + + req = req.UpdateBackupSchedulePayload(payload) + return req +} + +// getUpdateBackupSchedulePayload creates a payload for the UpdateBackupSchedule API call +// it will use the values already set in the instance object +// falls back to default values if the values are not set +func getUpdateBackupSchedulePayload(instance *mongodbflex.Instance) mongodbflex.UpdateBackupSchedulePayload { + options := make(map[string]string) + if instance == nil || instance.Options != nil { + options = *instance.Options + } + + backupSchedule := instance.BackupSchedule + if backupSchedule == nil { + backupSchedule = utils.Ptr(defaultBackupSchedule) + } + dailySnapshotRetentionDays, err := strconv.ParseInt(options["dailySnapshotRetentionDays"], 10, 64) + if err != nil { + dailySnapshotRetentionDays = defaultDailySnapshotRetentionDays + } + weeklySnapshotRetentionWeeks, err := strconv.ParseInt(options["weeklySnapshotRetentionWeeks"], 10, 64) + if err != nil { + weeklySnapshotRetentionWeeks = defaultWeeklySnapshotRetentionWeeks + } + monthlySnapshotRetentionMonths, err := strconv.ParseInt(options["monthlySnapshotRetentionMonths"], 10, 64) + if err != nil { + monthlySnapshotRetentionMonths = defaultMonthlySnapshotRetentionMonths + } + pointInTimeWindowHours, err := strconv.ParseInt(options["pointInTimeWindowHours"], 10, 64) + if err != nil { + pointInTimeWindowHours = defaultPointInTimeWindowHours + } + snapshotRetentionDays, err := strconv.ParseInt(options["snapshotRetentionDays"], 10, 64) + if err != nil { + snapshotRetentionDays = defaultSnapshotRetentionDays + } + + defaultPayload := mongodbflex.UpdateBackupSchedulePayload{ + BackupSchedule: backupSchedule, + DailySnapshotRetentionDays: &dailySnapshotRetentionDays, + MonthlySnapshotRetentionMonths: &monthlySnapshotRetentionMonths, + PointInTimeWindowHours: &pointInTimeWindowHours, + SnapshotRetentionDays: &snapshotRetentionDays, + WeeklySnapshotRetentionWeeks: &weeklySnapshotRetentionWeeks, + } + return defaultPayload +} + +func buildGetInstanceRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { + req := apiClient.GetInstance(ctx, model.ProjectId, *model.InstanceId) return req } diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go index 797373ca0..1f6cc5aa0 100644 --- a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go @@ -21,7 +21,7 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &mongodbflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testSchedule = "0 0 * * *" +var testSchedule = "0 0/6 * * *" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -52,7 +52,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixturePayload(mods ...func(payload *mongodbflex.UpdateBackupSchedulePayload)) mongodbflex.UpdateBackupSchedulePayload { payload := mongodbflex.UpdateBackupSchedulePayload{ - BackupSchedule: utils.Ptr(testSchedule), + BackupSchedule: utils.Ptr(testSchedule), + SnapshotRetentionDays: utils.Ptr(int64(3)), + DailySnapshotRetentionDays: utils.Ptr(int64(0)), + WeeklySnapshotRetentionWeeks: utils.Ptr(int64(3)), + MonthlySnapshotRetentionMonths: utils.Ptr(int64(1)), + PointInTimeWindowHours: utils.Ptr(int64(30)), } for _, mod := range mods { mod(&payload) @@ -60,7 +65,7 @@ func fixturePayload(mods ...func(payload *mongodbflex.UpdateBackupSchedulePayloa return payload } -func fixtureRequest(mods ...func(request *mongodbflex.ApiUpdateBackupScheduleRequest)) mongodbflex.ApiUpdateBackupScheduleRequest { +func fixtureUpdateBackupScheduleRequest(mods ...func(request *mongodbflex.ApiUpdateBackupScheduleRequest)) mongodbflex.ApiUpdateBackupScheduleRequest { request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId) request = request.UpdateBackupSchedulePayload(fixturePayload()) for _, mod := range mods { @@ -69,6 +74,31 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiUpdateBackupScheduleReq return request } +func fixtureGetInstanceRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { + request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureInstance(mods ...func(instance *mongodbflex.Instance)) *mongodbflex.Instance { + instance := mongodbflex.Instance{ + BackupSchedule: &testSchedule, + Options: &map[string]string{ + "dailySnapshotRetentionDays": "0", + "weeklySnapshotRetentionWeeks": "3", + "monthlySnapshotRetentionMonths": "1", + "pointInTimeWindowHours": "30", + "snapshotRetentionDays": "3", + }, + } + for _, mod := range mods { + mod(&instance) + } + return &instance +} + func TestParseInput(t *testing.T) { tests := []struct { description string @@ -184,33 +214,103 @@ func TestParseInput(t *testing.T) { } } -func TestBuildRequest(t *testing.T) { +func TestBuildGetInstanceRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest mongodbflex.ApiUpdateBackupScheduleRequest + expectedRequest mongodbflex.ApiGetInstanceRequest }{ { description: "base", model: fixtureInputModel(), - expectedRequest: fixtureRequest(), + expectedRequest: fixtureGetInstanceRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildGetInstanceRequest(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) + } + }) + } +} + +func TestBuildUpdateBackupScheduleRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + instance *mongodbflex.Instance + expectedRequest mongodbflex.ApiUpdateBackupScheduleRequest + }{ + { + description: "update backup schedule, read retention policy from instance", + model: fixtureInputModel(), + instance: fixtureInstance(), + expectedRequest: fixtureUpdateBackupScheduleRequest(), + }, + { + description: "update retention policy, read backup schedule from instance", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: utils.Ptr(testInstanceId), + DailySnaphotRetentionDays: utils.Ptr(int64(2)), + }, + instance: fixtureInstance(), + expectedRequest: fixtureUpdateBackupScheduleRequest().UpdateBackupSchedulePayload( + fixturePayload(func(payload *mongodbflex.UpdateBackupSchedulePayload) { + payload.DailySnapshotRetentionDays = utils.Ptr(int64(2)) + }), + ), + }, + { + description: "update backup schedule and retention policy", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: utils.Ptr(testInstanceId), + BackupSchedule: utils.Ptr("0 0/6 5 2 1"), + DailySnaphotRetentionDays: utils.Ptr(int64(2)), + WeeklySnapshotRetentionWeeks: utils.Ptr(int64(2)), + MonthlySnapshotRetentionMonths: utils.Ptr(int64(2)), + SnapshotRetentionDays: utils.Ptr(int64(2)), + }, + instance: fixtureInstance(), + expectedRequest: fixtureUpdateBackupScheduleRequest().UpdateBackupSchedulePayload( + fixturePayload(func(payload *mongodbflex.UpdateBackupSchedulePayload) { + payload.BackupSchedule = utils.Ptr("0 0/6 5 2 1") + payload.DailySnapshotRetentionDays = utils.Ptr(int64(2)) + payload.WeeklySnapshotRetentionWeeks = utils.Ptr(int64(2)) + payload.MonthlySnapshotRetentionMonths = utils.Ptr(int64(2)) + payload.SnapshotRetentionDays = utils.Ptr(int64(2)) + }), + ), }, { - description: "required fields only", + description: "no fields set, empty instance (use defaults)", model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, }, InstanceId: utils.Ptr(testInstanceId), }, - expectedRequest: testClient.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId). - UpdateBackupSchedulePayload(mongodbflex.UpdateBackupSchedulePayload{}), + instance: &mongodbflex.Instance{}, + expectedRequest: fixtureUpdateBackupScheduleRequest(), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildUpdateBackupScheduleRequest(testCtx, tt.model, tt.instance, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), From 0d08248b20bc155d97d5d8ac7a5372eee7bb1935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 14 May 2024 08:54:10 +0100 Subject: [PATCH 3/8] restore command and testing --- internal/cmd/mongodbflex/backup/backup.go | 2 + .../cmd/mongodbflex/backup/restore/restore.go | 175 ++++++++++ .../backup/restore/restore_test.go | 310 ++++++++++++++++++ 3 files changed, 487 insertions(+) create mode 100644 internal/cmd/mongodbflex/backup/restore/restore_test.go diff --git a/internal/cmd/mongodbflex/backup/backup.go b/internal/cmd/mongodbflex/backup/backup.go index aee739024..f24270ba0 100644 --- a/internal/cmd/mongodbflex/backup/backup.go +++ b/internal/cmd/mongodbflex/backup/backup.go @@ -3,6 +3,7 @@ package backup import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore" restorejobs "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore-jobs" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/schedule" updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/update-schedule" @@ -31,4 +32,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(restorejobs.NewCmd(p)) cmd.AddCommand(updateschedule.NewCmd(p)) cmd.AddCommand(schedule.NewCmd(p)) + cmd.AddCommand(restore.NewCmd(p)) } diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go index 9b70a15f0..4b2f1fc44 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore.go +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -1 +1,176 @@ package restore + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + mongodbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" + backupInstanceIdFlag = "backup-instance-id" + backupIdFlag = "backup-id" + timestampFlag = "timestamp" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + BackupInstanceId string + BackupId string + Timestamp string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "restore", + Short: "Restores a MongoDB Flex instance from a backup", + Long: fmt.Sprintf("%s\n%s\n%s", + "Restores a MongoDB Flex instance from a backup of an instance.", + "The backup can be specified by either a backup id or a timestamp.", + "The target instance can be specified, otherwise the target instance will be the same as the backup.", + ), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Restores a MongoDB Flex instance with id "yyy" using backup with id "zzz"`, + `$ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz`), + examples.NewExample( + `Restores a MongoDB Flex instance with id "yyy" using backup with timestamp "zzz"`, + `$ stackit mongodbflex backup restore --instance-id yyy --timestamp zzz`), + examples.NewExample( + `Restores a MongoDB Flex instance with id "yyy" using backup from instance with id "zzz" with backup id "aaa"`, + `$ stackit mongodbflex backup restore --instance-id yyy --backup-instance-id zzz --backup-id aaa`), + ), + 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 + } + + instanceLabel, err := mongodbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.ProjectId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to restore MongoDB Flex instance %q?", instanceLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // If backupInstanceId is not provided, the target is the same instance as the backup + if model.BackupInstanceId == "" { + model.BackupInstanceId = model.InstanceId + } + + isRestoreOperation := getIsRestoreOperation(model.BackupId, model.Timestamp) + + // If backupId is provided, restore the instance from the backup with the backupId + if isRestoreOperation { + req := buildRestoreRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("restore MongoDB Flex instance: %w", err) + } + + p.Outputf("Restored instance %q with backup %q\n", model.InstanceId, model.BackupId) + return nil + } + + // Else, if timestamp is provided, clone the instance from the backup with the timestep + req := buildCloneRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("clone MongoDB Flex instance: %w", err) + } + p.Outputf("Cloned instance %q from backup with timestamp %q\n", model.InstanceId, model.Timestamp) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance id") + cmd.Flags().Var(flags.UUIDFlag(), backupInstanceIdFlag, "Backup instance id") + cmd.Flags().String(backupIdFlag, "", "Backup id") + cmd.Flags().String(timestampFlag, "", "Timestamp of the backup") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) + + cmd.MarkFlagsOneRequired(backupIdFlag, timestampFlag) + cmd.MarkFlagsMutuallyExclusive(backupIdFlag, timestampFlag) +} + +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, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + BackupInstanceId: flags.FlagToStringValue(p, cmd, backupInstanceIdFlag), + BackupId: flags.FlagToStringValue(p, cmd, backupIdFlag), + Timestamp: flags.FlagToStringValue(p, cmd, timestampFlag), + } + + 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 buildRestoreRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiRestoreInstanceRequest { + req := apiClient.RestoreInstance(ctx, model.ProjectId, model.InstanceId) + req = req.RestoreInstancePayload(mongodbflex.RestoreInstancePayload{ + BackupId: &model.BackupId, + InstanceId: &model.BackupInstanceId, + }) + return req +} + +func buildCloneRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiCloneInstanceRequest { + req := apiClient.CloneInstance(ctx, model.ProjectId, model.InstanceId) + req = req.CloneInstancePayload(mongodbflex.CloneInstancePayload{ + Timestamp: &model.Timestamp, + InstanceId: &model.BackupInstanceId, + }) + return req +} + +func getIsRestoreOperation(backupId, timestamp string) bool { + return backupId != "" && timestamp == "" +} diff --git a/internal/cmd/mongodbflex/backup/restore/restore_test.go b/internal/cmd/mongodbflex/backup/restore/restore_test.go new file mode 100644 index 000000000..63e06d9af --- /dev/null +++ b/internal/cmd/mongodbflex/backup/restore/restore_test.go @@ -0,0 +1,310 @@ +package restore + +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/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +const ( + testBackupId = "backupID" + testTimestamp = "2021-01-01T00:00:00Z" +) + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} + +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testBackupInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + backupIdFlag: testBackupId, + backupInstanceIdFlag: testBackupInstanceId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + BackupId: testBackupId, + BackupInstanceId: testBackupInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRestoreRequest(mods ...func(request mongodbflex.ApiRestoreInstanceRequest)) mongodbflex.ApiRestoreInstanceRequest { + request := testClient.RestoreInstance(testCtx, testProjectId, testInstanceId) + request = request.RestoreInstancePayload(mongodbflex.RestoreInstancePayload{ + BackupId: utils.Ptr(testBackupId), + InstanceId: utils.Ptr(testBackupInstanceId), + }) + for _, mod := range mods { + mod(request) + } + return request +} + +func fixtureCloneRequest(mods ...func(request mongodbflex.ApiCloneInstanceRequest)) mongodbflex.ApiCloneInstanceRequest { + request := testClient.CloneInstance(testCtx, testProjectId, testInstanceId) + request = request.CloneInstancePayload(mongodbflex.CloneInstancePayload{ + Timestamp: utils.Ptr(testTimestamp), + InstanceId: utils.Ptr(testBackupInstanceId), + }) + for _, mod := range mods { + mod(request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "backup instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[backupInstanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "backup instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[backupInstanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "timestamp and backup id both provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[timestampFlag] = testTimestamp + }), + isValid: false, + }, + { + description: "timestamp and backup id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, backupIdFlag) + }), + 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) + } + + err = cmd.ValidateFlagGroups() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flag groups: %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 TestBuildRestoreRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiRestoreInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRestoreRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRestoreRequest(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) + } + }) + } +} + +func TestBuildCloneRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiCloneInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(func(model *inputModel) { + model.BackupId = "" + model.Timestamp = testTimestamp + }), + expectedRequest: fixtureCloneRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildCloneRequest(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) + } + }) + } +} + +func TestGetIsRestoreOperation(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected bool + }{ + { + description: "true", + model: fixtureInputModel(), + expected: true, + }, + { + description: "false", + model: fixtureInputModel(func(model *inputModel) { + model.BackupId = "" + model.Timestamp = testTimestamp + }), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + result := getIsRestoreOperation(tt.model.BackupId, tt.model.Timestamp) + if result != tt.expected { + t.Fatalf("Data does not match: %t", result) + } + }) + } +} From 5467d9ab3bec671c6e7b417fc9e0904cad9dca9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 14 May 2024 10:53:17 +0100 Subject: [PATCH 4/8] add waiters --- .../cmd/mongodbflex/backup/restore/restore.go | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go index 4b2f1fc44..58aa00fc1 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore.go +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -12,9 +12,11 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" mongodbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" ) const ( @@ -47,11 +49,11 @@ func NewCmd(p *print.Printer) *cobra.Command { `Restores a MongoDB Flex instance with id "yyy" using backup with id "zzz"`, `$ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz`), examples.NewExample( - `Restores a MongoDB Flex instance with id "yyy" using backup with timestamp "zzz"`, + `Clone a MongoDB Flex instance with id "yyy" via point-in-time restore to timestamp "zzz"`, `$ stackit mongodbflex backup restore --instance-id yyy --timestamp zzz`), examples.NewExample( `Restores a MongoDB Flex instance with id "yyy" using backup from instance with id "zzz" with backup id "aaa"`, - `$ stackit mongodbflex backup restore --instance-id yyy --backup-instance-id zzz --backup-id aaa`), + `$ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id aaa`), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -96,16 +98,37 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("restore MongoDB Flex instance: %w", err) } + if !model.Async { + s := spinner.New(p) + s.Start("Restoring instance") + _, err = wait.RestoreInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId, model.BackupId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for MongoDB Flex instance restoration: %w", err) + } + s.Stop() + } + p.Outputf("Restored instance %q with backup %q\n", model.InstanceId, model.BackupId) return nil } - // Else, if timestamp is provided, clone the instance from the backup with the timestep + // Else, if timestamp is provided, clone the instance from a point-in-time snapshot req := buildCloneRequest(ctx, model, apiClient) _, err = req.Execute() if err != nil { return fmt.Errorf("clone MongoDB Flex instance: %w", err) } + + if !model.Async { + s := spinner.New(p) + s.Start("Cloning instance") + _, err = wait.CloneInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for MongoDB Flex instance cloning: %w", err) + } + s.Stop() + } + p.Outputf("Cloned instance %q from backup with timestamp %q\n", model.InstanceId, model.Timestamp) return nil }, From b556f87cc1bfce57bb1621a8462d9b6af068b7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 14 May 2024 10:57:29 +0100 Subject: [PATCH 5/8] generate docs --- docs/stackit_mongodbflex_backup.md | 1 + docs/stackit_mongodbflex_backup_restore.md | 51 +++++++++++++++++++ .../cmd/mongodbflex/backup/restore/restore.go | 10 ++-- 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 docs/stackit_mongodbflex_backup_restore.md diff --git a/docs/stackit_mongodbflex_backup.md b/docs/stackit_mongodbflex_backup.md index e50efa668..19e304c76 100644 --- a/docs/stackit_mongodbflex_backup.md +++ b/docs/stackit_mongodbflex_backup.md @@ -31,6 +31,7 @@ stackit mongodbflex backup [flags] * [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex * [stackit mongodbflex backup describe](./stackit_mongodbflex_backup_describe.md) - Shows details of a backup for a MongoDB Flex instance * [stackit mongodbflex backup list](./stackit_mongodbflex_backup_list.md) - Lists all backups which are available for a MongoDB Flex instance +* [stackit mongodbflex backup restore](./stackit_mongodbflex_backup_restore.md) - Restores a MongoDB Flex instance from a backup * [stackit mongodbflex backup restore-jobs](./stackit_mongodbflex_backup_restore-jobs.md) - Lists all restore jobs which have been run for a MongoDB Flex instance * [stackit mongodbflex backup schedule](./stackit_mongodbflex_backup_schedule.md) - Shows details of the backup schedule and retention policy of a MongoDB Flex instance * [stackit mongodbflex backup update-schedule](./stackit_mongodbflex_backup_update-schedule.md) - Updates the backup schedule and retention policy for a MongoDB Flex instance diff --git a/docs/stackit_mongodbflex_backup_restore.md b/docs/stackit_mongodbflex_backup_restore.md new file mode 100644 index 000000000..05c4f376c --- /dev/null +++ b/docs/stackit_mongodbflex_backup_restore.md @@ -0,0 +1,51 @@ +## stackit mongodbflex backup restore + +Restores a MongoDB Flex instance from a backup + +### Synopsis + +Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot. +The backup is specified by a backup id and the point-in-time snapshot is specified by a timestamp. +The instance to apply the backup to can be specified, otherwise it will be the same as the backup. + +``` +stackit mongodbflex backup restore [flags] +``` + +### Examples + +``` + Restores a MongoDB Flex instance with id "yyy" using backup with id "zzz" + $ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz + + Clone a MongoDB Flex instance with id "yyy" via point-in-time restore to timestamp "zzz" + $ stackit mongodbflex backup restore --instance-id yyy --timestamp zzz + + Restores a MongoDB Flex instance with id "yyy" using backup from instance with id "zzz" with backup id "aaa" + $ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id aaa +``` + +### Options + +``` + --backup-id string Backup id + --backup-instance-id string Instance id of the target instance to restore the backup to + -h, --help Help for "stackit mongodbflex backup restore" + --instance-id string Instance id + --timestamp string Timestamp of the snapshot to clone the instance from +``` + +### 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go index 58aa00fc1..43e15c647 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore.go +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -39,9 +39,9 @@ func NewCmd(p *print.Printer) *cobra.Command { Use: "restore", Short: "Restores a MongoDB Flex instance from a backup", Long: fmt.Sprintf("%s\n%s\n%s", - "Restores a MongoDB Flex instance from a backup of an instance.", - "The backup can be specified by either a backup id or a timestamp.", - "The target instance can be specified, otherwise the target instance will be the same as the backup.", + "Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot.", + "The backup is specified by a backup id and the point-in-time snapshot is specified by a timestamp.", + "The instance to apply the backup to can be specified, otherwise it will be the same as the backup.", ), Args: args.NoArgs, Example: examples.Build( @@ -139,9 +139,9 @@ func NewCmd(p *print.Printer) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance id") - cmd.Flags().Var(flags.UUIDFlag(), backupInstanceIdFlag, "Backup instance id") + cmd.Flags().Var(flags.UUIDFlag(), backupInstanceIdFlag, "Instance id of the target instance to restore the backup to") cmd.Flags().String(backupIdFlag, "", "Backup id") - cmd.Flags().String(timestampFlag, "", "Timestamp of the backup") + cmd.Flags().String(timestampFlag, "", "Timestamp of the snapshot to clone the instance from") err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) From 0bf272c13860ca60b58c4d935c9e405e8bc30c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 14 May 2024 15:06:10 +0100 Subject: [PATCH 6/8] merge changes --- docs/stackit_mongodbflex_backup.md | 38 -------------- ...stackit_mongodbflex_backup_restore-jobs.md | 47 ----------------- docs/stackit_mongodbflex_backup_restore.md | 51 ------------------- docs/stackit_mongodbflex_backup_schedule.md | 43 ---------------- ...ckit_mongodbflex_backup_update-schedule.md | 51 ------------------- go.mod | 2 +- go.sum | 4 +- internal/cmd/mongodbflex/backup/backup.go | 6 --- 8 files changed, 3 insertions(+), 239 deletions(-) delete mode 100644 docs/stackit_mongodbflex_backup.md delete mode 100644 docs/stackit_mongodbflex_backup_restore-jobs.md delete mode 100644 docs/stackit_mongodbflex_backup_restore.md delete mode 100644 docs/stackit_mongodbflex_backup_schedule.md delete mode 100644 docs/stackit_mongodbflex_backup_update-schedule.md diff --git a/docs/stackit_mongodbflex_backup.md b/docs/stackit_mongodbflex_backup.md deleted file mode 100644 index 19e304c76..000000000 --- a/docs/stackit_mongodbflex_backup.md +++ /dev/null @@ -1,38 +0,0 @@ -## stackit mongodbflex backup - -Provides functionality for MongoDB Flex instance backups - -### Synopsis - -Provides functionality for MongoDB Flex instance backups. - -``` -stackit mongodbflex backup [flags] -``` - -### Options - -``` - -h, --help Help for "stackit mongodbflex 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"] - -p, --project-id string Project ID - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex -* [stackit mongodbflex backup describe](./stackit_mongodbflex_backup_describe.md) - Shows details of a backup for a MongoDB Flex instance -* [stackit mongodbflex backup list](./stackit_mongodbflex_backup_list.md) - Lists all backups which are available for a MongoDB Flex instance -* [stackit mongodbflex backup restore](./stackit_mongodbflex_backup_restore.md) - Restores a MongoDB Flex instance from a backup -* [stackit mongodbflex backup restore-jobs](./stackit_mongodbflex_backup_restore-jobs.md) - Lists all restore jobs which have been run for a MongoDB Flex instance -* [stackit mongodbflex backup schedule](./stackit_mongodbflex_backup_schedule.md) - Shows details of the backup schedule and retention policy of a MongoDB Flex instance -* [stackit mongodbflex backup update-schedule](./stackit_mongodbflex_backup_update-schedule.md) - Updates the backup schedule and retention policy for a MongoDB Flex instance - diff --git a/docs/stackit_mongodbflex_backup_restore-jobs.md b/docs/stackit_mongodbflex_backup_restore-jobs.md deleted file mode 100644 index af5626306..000000000 --- a/docs/stackit_mongodbflex_backup_restore-jobs.md +++ /dev/null @@ -1,47 +0,0 @@ -## stackit mongodbflex backup restore-jobs - -Lists all restore jobs which have been run for a MongoDB Flex instance - -### Synopsis - -Lists all restore jobs which have been run for a MongoDB Flex instance. - -``` -stackit mongodbflex backup restore-jobs [flags] -``` - -### Examples - -``` - List all restore jobs of instance with ID "xxx" - $ stackit mongodbflex backup restore-jobs --instance-id xxx - - List all restore jobs of instance with ID "xxx" in JSON format - $ stackit mongodbflex backup restore-jobs --instance-id xxx --output-format json - - List up to 10 restore jobs of instance with ID "xxx" - $ stackit mongodbflex backup restore-jobs --instance-id xxx --limit 10 -``` - -### Options - -``` - -h, --help Help for "stackit mongodbflex backup restore-jobs" - --instance-id string Instance ID - --limit int Maximum number of entries to list -``` - -### Options inherited from parent commands - -``` - -y, --assume-yes If set, skips all confirmation prompts - --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none"] - -p, --project-id string Project ID - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups - diff --git a/docs/stackit_mongodbflex_backup_restore.md b/docs/stackit_mongodbflex_backup_restore.md deleted file mode 100644 index 05c4f376c..000000000 --- a/docs/stackit_mongodbflex_backup_restore.md +++ /dev/null @@ -1,51 +0,0 @@ -## stackit mongodbflex backup restore - -Restores a MongoDB Flex instance from a backup - -### Synopsis - -Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot. -The backup is specified by a backup id and the point-in-time snapshot is specified by a timestamp. -The instance to apply the backup to can be specified, otherwise it will be the same as the backup. - -``` -stackit mongodbflex backup restore [flags] -``` - -### Examples - -``` - Restores a MongoDB Flex instance with id "yyy" using backup with id "zzz" - $ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz - - Clone a MongoDB Flex instance with id "yyy" via point-in-time restore to timestamp "zzz" - $ stackit mongodbflex backup restore --instance-id yyy --timestamp zzz - - Restores a MongoDB Flex instance with id "yyy" using backup from instance with id "zzz" with backup id "aaa" - $ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id aaa -``` - -### Options - -``` - --backup-id string Backup id - --backup-instance-id string Instance id of the target instance to restore the backup to - -h, --help Help for "stackit mongodbflex backup restore" - --instance-id string Instance id - --timestamp string Timestamp of the snapshot to clone the instance from -``` - -### 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"] - -p, --project-id string Project ID - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups - diff --git a/docs/stackit_mongodbflex_backup_schedule.md b/docs/stackit_mongodbflex_backup_schedule.md deleted file mode 100644 index 8993c63c4..000000000 --- a/docs/stackit_mongodbflex_backup_schedule.md +++ /dev/null @@ -1,43 +0,0 @@ -## stackit mongodbflex backup schedule - -Shows details of the backup schedule and retention policy of a MongoDB Flex instance - -### Synopsis - -Shows details of the backup schedule and retention policy of a MongoDB Flex instance. - -``` -stackit mongodbflex backup schedule [flags] -``` - -### Examples - -``` - Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" - $ stackit mongodbflex backup schedule --instance-id xxx - - Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format - $ stackit mongodbflex backup schedule --instance-id xxx --output-format json -``` - -### Options - -``` - -h, --help Help for "stackit mongodbflex backup schedule" - --instance-id string Instance ID -``` - -### Options inherited from parent commands - -``` - -y, --assume-yes If set, skips all confirmation prompts - --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none"] - -p, --project-id string Project ID - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups - diff --git a/docs/stackit_mongodbflex_backup_update-schedule.md b/docs/stackit_mongodbflex_backup_update-schedule.md deleted file mode 100644 index 38e1e69dc..000000000 --- a/docs/stackit_mongodbflex_backup_update-schedule.md +++ /dev/null @@ -1,51 +0,0 @@ -## stackit mongodbflex backup update-schedule - -Updates the backup schedule and retention policy for a MongoDB Flex instance - -### Synopsis - -Updates the backup schedule and retention policy for a MongoDB Flex instance. -The current backup schedule and retention policy can be seen in the output of the "stackit mongodbflex backup schedule" command. -The backup schedule is defined in the cron scheduling system format e.g. '0 0 * * *'. -See below for more detail on the retention policy options. - -``` -stackit mongodbflex backup update-schedule [flags] -``` - -### Examples - -``` - Update the backup schedule of a MongoDB Flex instance with ID "xxx" - $ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *' - - Update the retention days for snapshots of a MongoDB Flex instance with ID "xxx" to 5 days - $ stackit mongodbflex backup update-schedule --instance-id xxx --save-snapshot-days 5 -``` - -### Options - -``` - -h, --help Help for "stackit mongodbflex backup update-schedule" - --instance-id string Instance ID - --save-daily-snapshot-days int Number of days to retain daily snapshots. Should be less than or equal to the number of days of the selected weekly or monthly value. - --save-monthly-snapshot-months int Number of months to retain monthly snapshots - --save-snapshot-days int Number of days to retain snapshots. Should be less than or equal to the value of the daily backup. - --save-weekly-snapshot-weeks int Number of weeks to retain weekly snapshots. Should be less than or equal to the number of weeks of the selected monthly value. - --schedule string Backup schedule, in the cron scheduling system format e.g. '0 0 * * *' -``` - -### Options inherited from parent commands - -``` - -y, --assume-yes If set, skips all confirmation prompts - --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none"] - -p, --project-id string Project ID - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups - diff --git a/go.mod b/go.mod index dddd46e3b..1c3c74579 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.12.0 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.2.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1 - github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.12.0 + github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0 diff --git a/go.sum b/go.sum index 3ebc4b5ad..2e009d334 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0 h1:vvQFCN5sKZA9tdz github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c= github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.14.0 h1:tK6imWrbZ5TgQJbukWCUz7yDgcvvFMX8wamxkPTLuDo= github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.14.0/go.mod h1:kPetkX9hNm9HkRyiKQL/tlgdi8frZdMP8afg0mEvQ9s= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.12.0 h1:/m6N/CdsFxomexsowU7PwT1S4UTmI39PnEvvWGsDh1s= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.12.0/go.mod h1:iFerEzGmkg6R13ldFUyHUWHm0ac9cS4ftTDLhP0k/dU= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.13.0 h1:Dhanx9aV5VRfpHg22Li07661FbRT5FR9/M6FowN08a8= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.13.0/go.mod h1:iFerEzGmkg6R13ldFUyHUWHm0ac9cS4ftTDLhP0k/dU= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0 h1:rWgy4/eCIgyA2dUuc4a30pldmS6taQDwiLqoeZmyeP8= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0/go.mod h1:dkVMJI88eJ3Xs0ZV15r4tUpgitUGJXcvrX3RL4Zq2bQ= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0 h1:zkhm0r0OZ5NbHJFrm+7B+h11QL0bNLC53nzXhqCaLWo= diff --git a/internal/cmd/mongodbflex/backup/backup.go b/internal/cmd/mongodbflex/backup/backup.go index f24270ba0..3f5a4d347 100644 --- a/internal/cmd/mongodbflex/backup/backup.go +++ b/internal/cmd/mongodbflex/backup/backup.go @@ -1,10 +1,7 @@ package backup import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/describe" - "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/list" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore" - restorejobs "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore-jobs" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/schedule" updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/update-schedule" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,9 +24,6 @@ func NewCmd(p *print.Printer) *cobra.Command { } func addSubcommands(cmd *cobra.Command, p *print.Printer) { - cmd.AddCommand(list.NewCmd(p)) - cmd.AddCommand(describe.NewCmd(p)) - cmd.AddCommand(restorejobs.NewCmd(p)) cmd.AddCommand(updateschedule.NewCmd(p)) cmd.AddCommand(schedule.NewCmd(p)) cmd.AddCommand(restore.NewCmd(p)) From f4ac2450b4bc865196d72f2cfa9e94f70b01d19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 14 May 2024 17:12:36 +0100 Subject: [PATCH 7/8] address PR comments --- .../cmd/mongodbflex/backup/restore/restore.go | 22 +++++++++---------- .../mongodbflex/backup/schedule/schedule.go | 10 ++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go index 43e15c647..afcfdbef3 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore.go +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -40,20 +40,20 @@ func NewCmd(p *print.Printer) *cobra.Command { Short: "Restores a MongoDB Flex instance from a backup", Long: fmt.Sprintf("%s\n%s\n%s", "Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot.", - "The backup is specified by a backup id and the point-in-time snapshot is specified by a timestamp.", - "The instance to apply the backup to can be specified, otherwise it will be the same as the backup.", + "The backup is specified by a backup ID and the point-in-time snapshot is specified by a timestamp.", + "You can specify the instance to which the backup will be applied. If not specified, the backup will be applied to the same instance from which it was taken.", ), Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Restores a MongoDB Flex instance with id "yyy" using backup with id "zzz"`, + `Restores a MongoDB Flex instance with ID "yyy" using backup with ID "zzz"`, `$ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz`), examples.NewExample( - `Clone a MongoDB Flex instance with id "yyy" via point-in-time restore to timestamp "zzz"`, - `$ stackit mongodbflex backup restore --instance-id yyy --timestamp zzz`), + `Clone a MongoDB Flex instance with ID "yyy" via point-in-time restore to timestamp "2024-05-14T14:31:48Z"`, + `$ stackit mongodbflex backup restore --instance-id yyy --timestamp 2024-05-14T14:31:48Z`), examples.NewExample( - `Restores a MongoDB Flex instance with id "yyy" using backup from instance with id "zzz" with backup id "aaa"`, - `$ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id aaa`), + `Restores a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx"`, + `$ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id xxx`), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -138,10 +138,10 @@ func NewCmd(p *print.Printer) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance id") - cmd.Flags().Var(flags.UUIDFlag(), backupInstanceIdFlag, "Instance id of the target instance to restore the backup to") - cmd.Flags().String(backupIdFlag, "", "Backup id") - cmd.Flags().String(timestampFlag, "", "Timestamp of the snapshot to clone the instance from") + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().Var(flags.UUIDFlag(), backupInstanceIdFlag, "Instance ID of the target instance to restore the backup to") + cmd.Flags().String(backupIdFlag, "", "Backup ID") + cmd.Flags().String(timestampFlag, "", "Timestamp of the snapshot to use as a source for cloning the instance in a date-time with the RFC3339 layout format, e.g. 2024-01-01T00:00:00Z") err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule.go b/internal/cmd/mongodbflex/backup/schedule/schedule.go index 6a68c75d2..3be9badf2 100644 --- a/internal/cmd/mongodbflex/backup/schedule/schedule.go +++ b/internal/cmd/mongodbflex/backup/schedule/schedule.go @@ -131,15 +131,15 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I table := tables.NewTable() table.AddRow("BACKUP SCHEDULE", *instance.BackupSchedule) table.AddSeparator() - table.AddRow("DAILY SNAPSHOT RETENTION DAYS", (*instance.Options)["dailySnapshotRetentionDays"]) + table.AddRow("DAILY SNAPSHOT RETENTION (DAYS)", (*instance.Options)["dailySnapshotRetentionDays"]) table.AddSeparator() - table.AddRow("MONTHLY SNAPSHOT RETENTION MONTHS", (*instance.Options)["monthlySnapshotRetentionMonths"]) + table.AddRow("MONTHLY SNAPSHOT RETENTION (MONTHS)", (*instance.Options)["monthlySnapshotRetentionMonths"]) table.AddSeparator() - table.AddRow("POINT IN TIME WINDOW HOURS", (*instance.Options)["pointInTimeWindowHours"]) + table.AddRow("POINT IN TIME WINDOW (HOURS)", (*instance.Options)["pointInTimeWindowHours"]) table.AddSeparator() - table.AddRow("SNAPSHOT RETENTION DAYS", (*instance.Options)["snapshotRetentionDays"]) + table.AddRow("SNAPSHOT RETENTION (DAYS)", (*instance.Options)["snapshotRetentionDays"]) table.AddSeparator() - table.AddRow("WEEKLY SNAPSHOT RETENTION WEEKS", (*instance.Options)["weeklySnapshotRetentionWeeks"]) + table.AddRow("WEEKLY SNAPSHOT RETENTION (WEEKS)", (*instance.Options)["weeklySnapshotRetentionWeeks"]) table.AddSeparator() err := table.Display(p) if err != nil { From 4c39b0fccf733d4ae87b201a2c21e9adb384ab43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Wed, 15 May 2024 09:38:02 +0100 Subject: [PATCH 8/8] add custom error, fix restore examples --- docs/stackit_mongodbflex_backup.md | 3 ++ ...stackit_mongodbflex_backup_restore-jobs.md | 6 +-- docs/stackit_mongodbflex_backup_restore.md | 51 +++++++++++++++++++ docs/stackit_mongodbflex_backup_schedule.md | 43 ++++++++++++++++ ...ckit_mongodbflex_backup_update-schedule.md | 51 +++++++++++++++++++ .../cmd/mongodbflex/backup/restore/restore.go | 16 ++++-- internal/pkg/errors/errors.go | 10 ++++ 7 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 docs/stackit_mongodbflex_backup_restore.md create mode 100644 docs/stackit_mongodbflex_backup_schedule.md create mode 100644 docs/stackit_mongodbflex_backup_update-schedule.md diff --git a/docs/stackit_mongodbflex_backup.md b/docs/stackit_mongodbflex_backup.md index 765096f4e..19e304c76 100644 --- a/docs/stackit_mongodbflex_backup.md +++ b/docs/stackit_mongodbflex_backup.md @@ -31,5 +31,8 @@ stackit mongodbflex backup [flags] * [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex * [stackit mongodbflex backup describe](./stackit_mongodbflex_backup_describe.md) - Shows details of a backup for a MongoDB Flex instance * [stackit mongodbflex backup list](./stackit_mongodbflex_backup_list.md) - Lists all backups which are available for a MongoDB Flex instance +* [stackit mongodbflex backup restore](./stackit_mongodbflex_backup_restore.md) - Restores a MongoDB Flex instance from a backup * [stackit mongodbflex backup restore-jobs](./stackit_mongodbflex_backup_restore-jobs.md) - Lists all restore jobs which have been run for a MongoDB Flex instance +* [stackit mongodbflex backup schedule](./stackit_mongodbflex_backup_schedule.md) - Shows details of the backup schedule and retention policy of a MongoDB Flex instance +* [stackit mongodbflex backup update-schedule](./stackit_mongodbflex_backup_update-schedule.md) - Updates the backup schedule and retention policy for a MongoDB Flex instance diff --git a/docs/stackit_mongodbflex_backup_restore-jobs.md b/docs/stackit_mongodbflex_backup_restore-jobs.md index de1d6686f..af5626306 100644 --- a/docs/stackit_mongodbflex_backup_restore-jobs.md +++ b/docs/stackit_mongodbflex_backup_restore-jobs.md @@ -14,13 +14,13 @@ stackit mongodbflex backup restore-jobs [flags] ``` List all restore jobs of instance with ID "xxx" - $ stackit mongodbflex backup list --instance-id xxx + $ stackit mongodbflex backup restore-jobs --instance-id xxx List all restore jobs of instance with ID "xxx" in JSON format - $ stackit mongodbflex backup list --instance-id xxx --output-format json + $ stackit mongodbflex backup restore-jobs --instance-id xxx --output-format json List up to 10 restore jobs of instance with ID "xxx" - $ stackit mongodbflex backup list --instance-id xxx --limit 10 + $ stackit mongodbflex backup restore-jobs --instance-id xxx --limit 10 ``` ### Options diff --git a/docs/stackit_mongodbflex_backup_restore.md b/docs/stackit_mongodbflex_backup_restore.md new file mode 100644 index 000000000..f0f8e00e8 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_restore.md @@ -0,0 +1,51 @@ +## stackit mongodbflex backup restore + +Restores a MongoDB Flex instance from a backup + +### Synopsis + +Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot. +The backup is specified by a backup ID and the point-in-time snapshot is specified by a timestamp. +You can specify the instance to which the backup will be applied. If not specified, the backup will be applied to the same instance from which it was taken. + +``` +stackit mongodbflex backup restore [flags] +``` + +### Examples + +``` + Restore a MongoDB Flex instance with ID "yyy" using backup with ID "zzz" + $ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz + + Clone a MongoDB Flex instance with ID "yyy" via point-in-time restore to timestamp "2024-05-14T14:31:48Z" + $ stackit mongodbflex backup restore --instance-id yyy --timestamp 2024-05-14T14:31:48Z + + Restore a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx" + $ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id xxx +``` + +### Options + +``` + --backup-id string Backup ID + --backup-instance-id string Instance ID of the target instance to restore the backup to + -h, --help Help for "stackit mongodbflex backup restore" + --instance-id string Instance ID + --timestamp string Timestamp of the snapshot to use as a source for cloning the instance in a date-time with the RFC3339 layout format, e.g. 2024-01-01T00:00:00Z +``` + +### 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_schedule.md b/docs/stackit_mongodbflex_backup_schedule.md new file mode 100644 index 000000000..8993c63c4 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_schedule.md @@ -0,0 +1,43 @@ +## stackit mongodbflex backup schedule + +Shows details of the backup schedule and retention policy of a MongoDB Flex instance + +### Synopsis + +Shows details of the backup schedule and retention policy of a MongoDB Flex instance. + +``` +stackit mongodbflex backup schedule [flags] +``` + +### Examples + +``` + Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" + $ stackit mongodbflex backup schedule --instance-id xxx + + Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format + $ stackit mongodbflex backup schedule --instance-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup schedule" + --instance-id string Instance ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_update-schedule.md b/docs/stackit_mongodbflex_backup_update-schedule.md new file mode 100644 index 000000000..38e1e69dc --- /dev/null +++ b/docs/stackit_mongodbflex_backup_update-schedule.md @@ -0,0 +1,51 @@ +## stackit mongodbflex backup update-schedule + +Updates the backup schedule and retention policy for a MongoDB Flex instance + +### Synopsis + +Updates the backup schedule and retention policy for a MongoDB Flex instance. +The current backup schedule and retention policy can be seen in the output of the "stackit mongodbflex backup schedule" command. +The backup schedule is defined in the cron scheduling system format e.g. '0 0 * * *'. +See below for more detail on the retention policy options. + +``` +stackit mongodbflex backup update-schedule [flags] +``` + +### Examples + +``` + Update the backup schedule of a MongoDB Flex instance with ID "xxx" + $ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *' + + Update the retention days for snapshots of a MongoDB Flex instance with ID "xxx" to 5 days + $ stackit mongodbflex backup update-schedule --instance-id xxx --save-snapshot-days 5 +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup update-schedule" + --instance-id string Instance ID + --save-daily-snapshot-days int Number of days to retain daily snapshots. Should be less than or equal to the number of days of the selected weekly or monthly value. + --save-monthly-snapshot-months int Number of months to retain monthly snapshots + --save-snapshot-days int Number of days to retain snapshots. Should be less than or equal to the value of the daily backup. + --save-weekly-snapshot-weeks int Number of weeks to retain weekly snapshots. Should be less than or equal to the number of weeks of the selected monthly value. + --schedule string Backup schedule, in the cron scheduling system format e.g. '0 0 * * *' +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go index afcfdbef3..c043e4292 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore.go +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -46,13 +46,13 @@ func NewCmd(p *print.Printer) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Restores a MongoDB Flex instance with ID "yyy" using backup with ID "zzz"`, + `Restore a MongoDB Flex instance with ID "yyy" using backup with ID "zzz"`, `$ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz`), examples.NewExample( `Clone a MongoDB Flex instance with ID "yyy" via point-in-time restore to timestamp "2024-05-14T14:31:48Z"`, `$ stackit mongodbflex backup restore --instance-id yyy --timestamp 2024-05-14T14:31:48Z`), examples.NewExample( - `Restores a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx"`, + `Restore a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx"`, `$ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id xxx`), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -145,9 +145,6 @@ func configureFlags(cmd *cobra.Command) { err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) - - cmd.MarkFlagsOneRequired(backupIdFlag, timestampFlag) - cmd.MarkFlagsMutuallyExclusive(backupIdFlag, timestampFlag) } func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { @@ -156,6 +153,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return nil, &cliErr.ProjectIdError{} } + backupId := flags.FlagToStringValue(p, cmd, backupIdFlag) + timestamp := flags.FlagToStringValue(p, cmd, timestampFlag) + + if backupId != "" && timestamp != "" || backupId == "" && timestamp == "" { + return nil, &cliErr.RequiredMutuallyExclusiveFlagsError{ + Flags: []string{backupIdFlag, timestampFlag}, + } + } + model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index d421aa1b7..6b127bf67 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -22,6 +22,8 @@ or you can also set it through the environment variable [STACKIT_PROJECT_ID]` Get details on the available flags by re-running your command with the --help flag.` + REQUIRED_MUTUALLY_EXCLUSIVE_FLAGS = `the following flags are mutually exclusive and at least one of them is required: %s` + FAILED_AUTH = `you are not authenticated. You can authenticate as a user by running: @@ -237,6 +239,14 @@ func (e *FlagValidationError) Error() string { return fmt.Sprintf(FLAG_VALIDATION, e.Flag, e.Details) } +type RequiredMutuallyExclusiveFlagsError struct { + Flags []string +} + +func (e *RequiredMutuallyExclusiveFlagsError) Error() string { + return fmt.Sprintf(REQUIRED_MUTUALLY_EXCLUSIVE_FLAGS, strings.Join(e.Flags, ", ")) +} + type ArgValidationError struct { Arg string Details string