From adbc7fe03e8c58bf8bc45a92da70f500d3c69e76 Mon Sep 17 00:00:00 2001 From: neha_gupta Date: Tue, 26 Sep 2023 14:23:14 +0530 Subject: [PATCH] allow delete of multiple backup IDs (#4335) allow deletion of multiple IDs in single command #### Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No #### Type of change - [ ] :sunflower: Feature #### Issue(s) * https://github.com/alcionai/corso/issues/4119 #### Test Plan - [ ] :muscle: Manual - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/cli/backup/backup.go | 10 ++-- src/cli/backup/exchange.go | 22 +++++-- src/cli/backup/exchange_e2e_test.go | 78 ++++++++++++++++++++++-- src/cli/backup/groups.go | 22 +++++-- src/cli/backup/groups_e2e_test.go | 69 ++++++++++++++++++++-- src/cli/backup/onedrive.go | 22 +++++-- src/cli/backup/onedrive_e2e_test.go | 85 +++++++++++++++++++++++++-- src/cli/backup/sharepoint.go | 22 +++++-- src/cli/backup/sharepoint_e2e_test.go | 43 ++++++++++++-- src/cli/flags/repo.go | 14 +++++ src/cli/restore/groups.go | 2 +- 12 files changed, 346 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 862a58f08..7d5ad8ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enables local or network-attached storage for Corso repositories. - Reduce backup runtime for OneDrive and SharePoint incremental backups that have no file changes. - Increase Exchange backup performance by lazily fetching data only for items whose content changed. +- Added `--backups` flag to delete multiple backups in `corso backup delete` command. ## [v0.13.0] (beta) - 2023-09-18 diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index a79a8afb2..51f712e53 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -252,8 +252,8 @@ func runBackups( func genericDeleteCommand( cmd *cobra.Command, pst path.ServiceType, - bID, designation string, - args []string, + designation string, + bID, args []string, ) error { if utils.HasNoFlagsAndShownHelp(cmd) { return nil @@ -275,11 +275,11 @@ func genericDeleteCommand( defer utils.CloseRepo(ctx, r) - if err := r.DeleteBackups(ctx, true, bID); err != nil { - return Only(ctx, clues.Wrap(err, "Deleting backup "+bID)) + if err := r.DeleteBackups(ctx, true, bID...); err != nil { + return Only(ctx, clues.Wrap(err, fmt.Sprintf("Deleting backup %v", bID))) } - Infof(ctx, "Deleted %s backup %s", designation, bID) + Infof(ctx, "Deleted %s backup %v", designation, bID) return nil } diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 4a37c032b..85b474bbc 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -32,7 +32,7 @@ const ( const ( exchangeServiceCommand = "exchange" exchangeServiceCommandCreateUseSuffix = "--mailbox | '" + flags.Wildcard + "'" - exchangeServiceCommandDeleteUseSuffix = "--backup " + exchangeServiceCommandDeleteUseSuffix = "--backups " exchangeServiceCommandDetailsUseSuffix = "--backup " ) @@ -46,8 +46,9 @@ corso backup create exchange --mailbox alice@example.com,bob@example.com --data # Backup all Exchange data for all M365 users corso backup create exchange --mailbox '*'` - exchangeServiceCommandDeleteExamples = `# Delete Exchange backup with ID 1234abcd-12ab-cd34-56de-1234abcd -corso backup delete exchange --backup 1234abcd-12ab-cd34-56de-1234abcd` + exchangeServiceCommandDeleteExamples = `# Delete Exchange backup with IDs 1234abcd-12ab-cd34-56de-1234abcd \ +and 1234abcd-12ab-cd34-56de-1234abce +corso backup delete exchange --backups 1234abcd-12ab-cd34-56de-1234abcd,1234abcd-12ab-cd34-56de-1234abce` exchangeServiceCommandDetailsExamples = `# Explore items in Alice's latest backup (1234abcd...) corso backup details exchange --backup 1234abcd-12ab-cd34-56de-1234abcd @@ -121,7 +122,8 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command { c.Use = c.Use + " " + exchangeServiceCommandDeleteUseSuffix c.Example = exchangeServiceCommandDeleteExamples - flags.AddBackupIDFlag(c, true) + flags.AddMultipleBackupIDsFlag(c, false) + flags.AddBackupIDFlag(c, false) } return c @@ -352,5 +354,15 @@ func exchangeDeleteCmd() *cobra.Command { // deletes an exchange service backup. func deleteExchangeCmd(cmd *cobra.Command, args []string) error { - return genericDeleteCommand(cmd, path.ExchangeService, flags.BackupIDFV, "Exchange", args) + var backupIDValue []string + + if len(flags.BackupIDsFV) > 0 { + backupIDValue = flags.BackupIDsFV + } else if len(flags.BackupIDFV) > 0 { + backupIDValue = append(backupIDValue, flags.BackupIDFV) + } else { + return clues.New("either --backup or --backups flag is required") + } + + return genericDeleteCommand(cmd, path.ExchangeService, "Exchange", backupIDValue, args) } diff --git a/src/cli/backup/exchange_e2e_test.go b/src/cli/backup/exchange_e2e_test.go index 992787997..9965652d6 100644 --- a/src/cli/backup/exchange_e2e_test.go +++ b/src/cli/backup/exchange_e2e_test.go @@ -561,8 +561,9 @@ func runExchangeDetailsCmdTest(suite *PreparedBackupExchangeE2ESuite, category p type BackupDeleteExchangeE2ESuite struct { tester.Suite - dpnd dependencies - backupOp operations.BackupOperation + dpnd dependencies + backupOp operations.BackupOperation + secondaryBackupOp operations.BackupOperation } func TestBackupDeleteExchangeE2ESuite(t *testing.T) { @@ -595,6 +596,14 @@ func (suite *BackupDeleteExchangeE2ESuite) SetupSuite() { err = suite.backupOp.Run(ctx) require.NoError(t, err, clues.ToCore(err)) + + backupOp2, err := suite.dpnd.repo.NewBackup(ctx, sel.Selector) + require.NoError(t, err, clues.ToCore(err)) + + suite.secondaryBackupOp = backupOp2 + + err = suite.secondaryBackupOp.Run(ctx) + require.NoError(t, err, clues.ToCore(err)) } func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd() { @@ -608,7 +617,10 @@ func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd() { cmd := cliTD.StubRootCmd( "backup", "delete", "exchange", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, string(suite.backupOp.Results.BackupID)) + "--"+flags.BackupIDsFN, + fmt.Sprintf("%s,%s", + string(suite.backupOp.Results.BackupID), + string(suite.secondaryBackupOp.Results.BackupID))) cli.BuildCommandTree(cmd) // run the command @@ -624,6 +636,46 @@ func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd() { err = cmd.ExecuteContext(ctx) require.Error(t, err, clues.ToCore(err)) + + // a follow-up details call should fail, due to the backup ID being deleted + cmd = cliTD.StubRootCmd( + "backup", "details", "exchange", + "--config-file", suite.dpnd.configFilePath, + "--backup", string(suite.secondaryBackupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + err = cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} + +func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd_SingleID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "exchange", + "--config-file", suite.dpnd.configFilePath, + "--"+flags.BackupFN, + string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + // run the command + err := cmd.ExecuteContext(ctx) + require.NoError(t, err, clues.ToCore(err)) + + // a follow-up details call should fail, due to the backup ID being deleted + cmd = cliTD.StubRootCmd( + "backup", "details", "exchange", + "--config-file", suite.dpnd.configFilePath, + "--backup", string(suite.secondaryBackupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + err = cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) } func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd_UnknownID() { @@ -637,10 +689,28 @@ func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd_UnknownID cmd := cliTD.StubRootCmd( "backup", "delete", "exchange", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, uuid.NewString()) + "--"+flags.BackupIDsFN, uuid.NewString()) cli.BuildCommandTree(cmd) // unknown backupIDs should error since the modelStore can't find the backup err := cmd.ExecuteContext(ctx) require.Error(t, err, clues.ToCore(err)) } + +func (suite *BackupDeleteExchangeE2ESuite) TestExchangeBackupDeleteCmd_NoBackupID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "exchange", + "--config-file", suite.dpnd.configFilePath) + cli.BuildCommandTree(cmd) + + // empty backupIDs should error since no data provided + err := cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} diff --git a/src/cli/backup/groups.go b/src/cli/backup/groups.go index 7e3e041db..84db6ebde 100644 --- a/src/cli/backup/groups.go +++ b/src/cli/backup/groups.go @@ -32,7 +32,7 @@ const ( groupsServiceCommand = "groups" teamsServiceCommand = "teams" groupsServiceCommandCreateUseSuffix = "--group | '" + flags.Wildcard + "'" - groupsServiceCommandDeleteUseSuffix = "--backup " + groupsServiceCommandDeleteUseSuffix = "--backups " groupsServiceCommandDetailsUseSuffix = "--backup " ) @@ -46,8 +46,9 @@ corso backup create groups --group Marketing --data messages # Backup all Groups and Teams data for all groups corso backup create groups --group '*'` - groupsServiceCommandDeleteExamples = `# Delete Groups backup with ID 1234abcd-12ab-cd34-56de-1234abcd -corso backup delete groups --backup 1234abcd-12ab-cd34-56de-1234abcd` + groupsServiceCommandDeleteExamples = `# Delete Groups backup with ID 1234abcd-12ab-cd34-56de-1234abcd \ +and 1234abcd-12ab-cd34-56de-1234abce +corso backup delete groups --backups 1234abcd-12ab-cd34-56de-1234abcd,1234abcd-12ab-cd34-56de-1234abce` groupsServiceCommandDetailsExamples = `# Explore items in Marketing's latest backup (1234abcd...) corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd @@ -110,7 +111,8 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command { c.Use = c.Use + " " + groupsServiceCommandDeleteUseSuffix c.Example = groupsServiceCommandDeleteExamples - flags.AddBackupIDFlag(c, true) + flags.AddMultipleBackupIDsFlag(c, false) + flags.AddBackupIDFlag(c, false) } return c @@ -305,7 +307,17 @@ func groupsDeleteCmd() *cobra.Command { // deletes an groups service backup. func deleteGroupsCmd(cmd *cobra.Command, args []string) error { - return genericDeleteCommand(cmd, path.GroupsService, flags.BackupIDFV, "Groups", args) + backupIDValue := []string{} + + if len(flags.BackupIDsFV) > 0 { + backupIDValue = flags.BackupIDsFV + } else if len(flags.BackupIDFV) > 0 { + backupIDValue = append(backupIDValue, flags.BackupIDFV) + } else { + return clues.New("either --backup or --backups flag is required") + } + + return genericDeleteCommand(cmd, path.GroupsService, "Groups", backupIDValue, args) } // --------------------------------------------------------------------------- diff --git a/src/cli/backup/groups_e2e_test.go b/src/cli/backup/groups_e2e_test.go index 036b2cbb6..66de86eb4 100644 --- a/src/cli/backup/groups_e2e_test.go +++ b/src/cli/backup/groups_e2e_test.go @@ -497,8 +497,9 @@ func runGroupsDetailsCmdTest(suite *PreparedBackupGroupsE2ESuite, category path. type BackupDeleteGroupsE2ESuite struct { tester.Suite - dpnd dependencies - backupOp operations.BackupOperation + dpnd dependencies + backupOp operations.BackupOperation + secondaryBackupOp operations.BackupOperation } func TestBackupDeleteGroupsE2ESuite(t *testing.T) { @@ -531,6 +532,15 @@ func (suite *BackupDeleteGroupsE2ESuite) SetupSuite() { err = suite.backupOp.Run(ctx) require.NoError(t, err, clues.ToCore(err)) + + // secondary backup + secondaryBackupOp, err := suite.dpnd.repo.NewBackup(ctx, sel.Selector) + require.NoError(t, err, clues.ToCore(err)) + + suite.secondaryBackupOp = secondaryBackupOp + + err = suite.secondaryBackupOp.Run(ctx) + require.NoError(t, err, clues.ToCore(err)) } func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd() { @@ -544,7 +554,40 @@ func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd() { cmd := cliTD.StubRootCmd( "backup", "delete", "groups", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, string(suite.backupOp.Results.BackupID)) + "--"+flags.BackupIDsFN, + fmt.Sprintf("%s,%s", + string(suite.backupOp.Results.BackupID), + string(suite.secondaryBackupOp.Results.BackupID))) + cli.BuildCommandTree(cmd) + + // run the command + err := cmd.ExecuteContext(ctx) + require.NoError(t, err, clues.ToCore(err)) + + // a follow-up details call should fail, due to the backup ID being deleted + cmd = cliTD.StubRootCmd( + "backup", "details", "groups", + "--config-file", suite.dpnd.configFilePath, + "--backups", string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + err = cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} + +func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd_SingleID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "groups", + "--config-file", suite.dpnd.configFilePath, + "--"+flags.BackupFN, + string(suite.backupOp.Results.BackupID)) cli.BuildCommandTree(cmd) // run the command @@ -573,7 +616,7 @@ func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd_UnknownID() { cmd := cliTD.StubRootCmd( "backup", "delete", "groups", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, uuid.NewString()) + "--"+flags.BackupIDsFN, uuid.NewString()) cli.BuildCommandTree(cmd) // unknown backupIDs should error since the modelStore can't find the backup @@ -581,6 +624,24 @@ func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd_UnknownID() { require.Error(t, err, clues.ToCore(err)) } +func (suite *BackupDeleteGroupsE2ESuite) TestGroupsBackupDeleteCmd_NoBackupID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "groups", + "--config-file", suite.dpnd.configFilePath) + cli.BuildCommandTree(cmd) + + // empty backupIDs should error since no data provided + err := cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} + // --------------------------------------------------------------------------- // helpers // --------------------------------------------------------------------------- diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index c870f84ee..0eeb13308 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -26,7 +26,7 @@ import ( const ( oneDriveServiceCommand = "onedrive" oneDriveServiceCommandCreateUseSuffix = "--user | '" + flags.Wildcard + "'" - oneDriveServiceCommandDeleteUseSuffix = "--backup " + oneDriveServiceCommandDeleteUseSuffix = "--backups " oneDriveServiceCommandDetailsUseSuffix = "--backup " ) @@ -40,8 +40,9 @@ corso backup create onedrive --user alice@example.com,bob@example.com # Backup all OneDrive data for all M365 users corso backup create onedrive --user '*'` - oneDriveServiceCommandDeleteExamples = `# Delete OneDrive backup with ID 1234abcd-12ab-cd34-56de-1234abcd -corso backup delete onedrive --backup 1234abcd-12ab-cd34-56de-1234abcd` + oneDriveServiceCommandDeleteExamples = `# Delete OneDrive backup with ID 1234abcd-12ab-cd34-56de-1234abcd \ +and 1234abcd-12ab-cd34-56de-1234abce +corso backup delete onedrive --backups 1234abcd-12ab-cd34-56de-1234abcd,1234abcd-12ab-cd34-56de-1234abce` oneDriveServiceCommandDetailsExamples = `# Explore items in Bob's latest backup (1234abcd...) corso backup details onedrive --backup 1234abcd-12ab-cd34-56de-1234abcd @@ -100,7 +101,8 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { c.Use = c.Use + " " + oneDriveServiceCommandDeleteUseSuffix c.Example = oneDriveServiceCommandDeleteExamples - flags.AddBackupIDFlag(c, true) + flags.AddMultipleBackupIDsFlag(c, false) + flags.AddBackupIDFlag(c, false) } return c @@ -306,5 +308,15 @@ func oneDriveDeleteCmd() *cobra.Command { // deletes a oneDrive service backup. func deleteOneDriveCmd(cmd *cobra.Command, args []string) error { - return genericDeleteCommand(cmd, path.OneDriveService, flags.BackupIDFV, "OneDrive", args) + backupIDValue := []string{} + + if len(flags.BackupIDsFV) > 0 { + backupIDValue = flags.BackupIDsFV + } else if len(flags.BackupIDFV) > 0 { + backupIDValue = append(backupIDValue, flags.BackupIDFV) + } else { + return clues.New("either --backup or --backups flag is required") + } + + return genericDeleteCommand(cmd, path.OneDriveService, "OneDrive", backupIDValue, args) } diff --git a/src/cli/backup/onedrive_e2e_test.go b/src/cli/backup/onedrive_e2e_test.go index ea5e808b4..77d599862 100644 --- a/src/cli/backup/onedrive_e2e_test.go +++ b/src/cli/backup/onedrive_e2e_test.go @@ -121,8 +121,9 @@ func (suite *NoBackupOneDriveE2ESuite) TestOneDriveBackupCmd_userNotInTenant() { type BackupDeleteOneDriveE2ESuite struct { tester.Suite - dpnd dependencies - backupOp operations.BackupOperation + dpnd dependencies + backupOp operations.BackupOperation + secondaryBackupOp operations.BackupOperation } func TestBackupDeleteOneDriveE2ESuite(t *testing.T) { @@ -158,6 +159,15 @@ func (suite *BackupDeleteOneDriveE2ESuite) SetupSuite() { err = suite.backupOp.Run(ctx) require.NoError(t, err, clues.ToCore(err)) + + // secondary backup + secondaryBackupOp, err := suite.dpnd.repo.NewBackupWithLookup(ctx, sel.Selector, ins) + require.NoError(t, err, clues.ToCore(err)) + + suite.secondaryBackupOp = secondaryBackupOp + + err = suite.secondaryBackupOp.Run(ctx) + require.NoError(t, err, clues.ToCore(err)) } func (suite *BackupDeleteOneDriveE2ESuite) TestOneDriveBackupDeleteCmd() { @@ -173,7 +183,10 @@ func (suite *BackupDeleteOneDriveE2ESuite) TestOneDriveBackupDeleteCmd() { cmd := cliTD.StubRootCmd( "backup", "delete", "onedrive", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, string(suite.backupOp.Results.BackupID)) + "--"+flags.BackupIDsFN, + fmt.Sprintf("%s,%s", + string(suite.backupOp.Results.BackupID), + string(suite.secondaryBackupOp.Results.BackupID))) cli.BuildCommandTree(cmd) cmd.SetErr(&suite.dpnd.recorder) @@ -187,7 +200,51 @@ func (suite *BackupDeleteOneDriveE2ESuite) TestOneDriveBackupDeleteCmd() { assert.True(t, strings.HasSuffix( result, - fmt.Sprintf("Deleted OneDrive backup %s\n", string(suite.backupOp.Results.BackupID)))) + fmt.Sprintf("Deleted OneDrive backup [%s %s]\n", + string(suite.backupOp.Results.BackupID), + string(suite.secondaryBackupOp.Results.BackupID)))) + + // a follow-up details call should fail, due to the backup ID being deleted + cmd = cliTD.StubRootCmd( + "backup", "details", "onedrive", + "--config-file", suite.dpnd.configFilePath, + "--backups", string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + err = cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} + +func (suite *BackupDeleteOneDriveE2ESuite) TestOneDriveBackupDeleteCmd_SingleID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + suite.dpnd.recorder.Reset() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "onedrive", + "--config-file", suite.dpnd.configFilePath, + "--"+flags.BackupFN, + string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + cmd.SetErr(&suite.dpnd.recorder) + + ctx = print.SetRootCmd(ctx, cmd) + + // run the command + err := cmd.ExecuteContext(ctx) + require.NoError(t, err, clues.ToCore(err)) + + result := suite.dpnd.recorder.String() + assert.True(t, + strings.HasSuffix( + result, + fmt.Sprintf("Deleted OneDrive backup [%s]\n", + string(suite.backupOp.Results.BackupID)))) // a follow-up details call should fail, due to the backup ID being deleted cmd = cliTD.StubRootCmd( @@ -211,10 +268,28 @@ func (suite *BackupDeleteOneDriveE2ESuite) TestOneDriveBackupDeleteCmd_unknownID cmd := cliTD.StubRootCmd( "backup", "delete", "onedrive", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, uuid.NewString()) + "--"+flags.BackupIDsFN, uuid.NewString()) cli.BuildCommandTree(cmd) // unknown backupIDs should error since the modelStore can't find the backup err := cmd.ExecuteContext(ctx) require.Error(t, err, clues.ToCore(err)) } + +func (suite *BackupDeleteOneDriveE2ESuite) TestOneDriveBackupDeleteCmd_NoBackupID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "onedrive", + "--config-file", suite.dpnd.configFilePath) + cli.BuildCommandTree(cmd) + + // empty backupIDs should error since no data provided + err := cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} diff --git a/src/cli/backup/sharepoint.go b/src/cli/backup/sharepoint.go index 2b154573e..71a5ca524 100644 --- a/src/cli/backup/sharepoint.go +++ b/src/cli/backup/sharepoint.go @@ -30,7 +30,7 @@ import ( const ( sharePointServiceCommand = "sharepoint" sharePointServiceCommandCreateUseSuffix = "--site | '" + flags.Wildcard + "'" - sharePointServiceCommandDeleteUseSuffix = "--backup " + sharePointServiceCommandDeleteUseSuffix = "--backups " sharePointServiceCommandDetailsUseSuffix = "--backup " ) @@ -44,8 +44,9 @@ corso backup create sharepoint --site https://example.com/hr,https://example.com # Backup all SharePoint data for all Sites corso backup create sharepoint --site '*'` - sharePointServiceCommandDeleteExamples = `# Delete SharePoint backup with ID 1234abcd-12ab-cd34-56de-1234abcd -corso backup delete sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd` + sharePointServiceCommandDeleteExamples = `# Delete SharePoint backup with ID 1234abcd-12ab-cd34-56de-1234abcd \ +and 1234abcd-12ab-cd34-56de-1234abce +corso backup delete sharepoint --backups 1234abcd-12ab-cd34-56de-1234abcd,1234abcd-12ab-cd34-56de-1234abce` sharePointServiceCommandDetailsExamples = `# Explore items in the HR site's latest backup (1234abcd...) corso backup details sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd @@ -111,7 +112,8 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { c.Use = c.Use + " " + sharePointServiceCommandDeleteUseSuffix c.Example = sharePointServiceCommandDeleteExamples - flags.AddBackupIDFlag(c, true) + flags.AddMultipleBackupIDsFlag(c, false) + flags.AddBackupIDFlag(c, false) } return c @@ -284,7 +286,17 @@ func sharePointDeleteCmd() *cobra.Command { // deletes a sharePoint service backup. func deleteSharePointCmd(cmd *cobra.Command, args []string) error { - return genericDeleteCommand(cmd, path.SharePointService, flags.BackupIDFV, "SharePoint", args) + backupIDValue := []string{} + + if len(flags.BackupIDsFV) > 0 { + backupIDValue = flags.BackupIDsFV + } else if len(flags.BackupIDFV) > 0 { + backupIDValue = append(backupIDValue, flags.BackupIDFV) + } else { + return clues.New("either --backup or --backups flag is required") + } + + return genericDeleteCommand(cmd, path.SharePointService, "SharePoint", backupIDValue, args) } // ------------------------------------------------------------------------------------------------ diff --git a/src/cli/backup/sharepoint_e2e_test.go b/src/cli/backup/sharepoint_e2e_test.go index 138247850..bfb67f85a 100644 --- a/src/cli/backup/sharepoint_e2e_test.go +++ b/src/cli/backup/sharepoint_e2e_test.go @@ -84,8 +84,9 @@ func (suite *NoBackupSharePointE2ESuite) TestSharePointBackupListCmd_empty() { type BackupDeleteSharePointE2ESuite struct { tester.Suite - dpnd dependencies - backupOp operations.BackupOperation + dpnd dependencies + backupOp operations.BackupOperation + secondaryBackupOp operations.BackupOperation } func TestBackupDeleteSharePointE2ESuite(t *testing.T) { @@ -121,6 +122,15 @@ func (suite *BackupDeleteSharePointE2ESuite) SetupSuite() { err = suite.backupOp.Run(ctx) require.NoError(t, err, clues.ToCore(err)) + + // secondary backup + secondaryBackupOp, err := suite.dpnd.repo.NewBackupWithLookup(ctx, sel.Selector, ins) + require.NoError(t, err, clues.ToCore(err)) + + suite.secondaryBackupOp = secondaryBackupOp + + err = suite.secondaryBackupOp.Run(ctx) + require.NoError(t, err, clues.ToCore(err)) } func (suite *BackupDeleteSharePointE2ESuite) TestSharePointBackupDeleteCmd() { @@ -136,7 +146,10 @@ func (suite *BackupDeleteSharePointE2ESuite) TestSharePointBackupDeleteCmd() { cmd := cliTD.StubRootCmd( "backup", "delete", "sharepoint", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, string(suite.backupOp.Results.BackupID)) + "--"+flags.BackupIDsFN, + fmt.Sprintf("%s,%s", + string(suite.backupOp.Results.BackupID), + string(suite.secondaryBackupOp.Results.BackupID))) cli.BuildCommandTree(cmd) cmd.SetErr(&suite.dpnd.recorder) @@ -150,7 +163,9 @@ func (suite *BackupDeleteSharePointE2ESuite) TestSharePointBackupDeleteCmd() { assert.True(t, strings.HasSuffix( result, - fmt.Sprintf("Deleted SharePoint backup %s\n", string(suite.backupOp.Results.BackupID)))) + fmt.Sprintf("Deleted SharePoint backup [%s %s]\n", + string(suite.backupOp.Results.BackupID), + string(suite.secondaryBackupOp.Results.BackupID)))) } // moved out of the func above to make the linter happy @@ -175,10 +190,28 @@ func (suite *BackupDeleteSharePointE2ESuite) TestSharePointBackupDeleteCmd_unkno cmd := cliTD.StubRootCmd( "backup", "delete", "sharepoint", "--config-file", suite.dpnd.configFilePath, - "--"+flags.BackupFN, uuid.NewString()) + "--"+flags.BackupIDsFN, uuid.NewString()) cli.BuildCommandTree(cmd) // unknown backupIDs should error since the modelStore can't find the backup err := cmd.ExecuteContext(ctx) require.Error(t, err, clues.ToCore(err)) } + +func (suite *BackupDeleteSharePointE2ESuite) TestSharePointBackupDeleteCmd_NoBackupID() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + ctx = config.SetViper(ctx, suite.dpnd.vpr) + + defer flush() + + cmd := cliTD.StubRootCmd( + "backup", "delete", "groups", + "--config-file", suite.dpnd.configFilePath) + cli.BuildCommandTree(cmd) + + // empty backupIDs should error since no data provided + err := cmd.ExecuteContext(ctx) + require.Error(t, err, clues.ToCore(err)) +} diff --git a/src/cli/flags/repo.go b/src/cli/flags/repo.go index 44bc3a2a3..77495c08d 100644 --- a/src/cli/flags/repo.go +++ b/src/cli/flags/repo.go @@ -6,6 +6,7 @@ import ( const ( BackupFN = "backup" + BackupIDsFN = "backups" AWSAccessKeyFN = "aws-access-key" AWSSecretAccessKeyFN = "aws-secret-access-key" AWSSessionTokenFN = "aws-session-token" @@ -17,6 +18,7 @@ const ( var ( BackupIDFV string + BackupIDsFV []string AWSAccessKeyFV string AWSSecretAccessKeyFV string AWSSessionTokenFV string @@ -24,6 +26,18 @@ var ( SucceedIfExistsFV bool ) +// AddMultipleBackupIDsFlag adds the --backups flag. +func AddMultipleBackupIDsFlag(cmd *cobra.Command, require bool) { + cmd.Flags().StringSliceVar( + &BackupIDsFV, + BackupIDsFN, nil, + "',' separated IDs of the backup to retrieve") + + if require { + cobra.CheckErr(cmd.MarkFlagRequired(BackupIDsFN)) + } +} + // AddBackupIDFlag adds the --backup flag. func AddBackupIDFlag(cmd *cobra.Command, require bool) { cmd.Flags().StringVar(&BackupIDFV, BackupFN, "", "ID of the backup to retrieve.") diff --git a/src/cli/restore/groups.go b/src/cli/restore/groups.go index 755d797e0..9e1f9cf5d 100644 --- a/src/cli/restore/groups.go +++ b/src/cli/restore/groups.go @@ -46,7 +46,7 @@ const ( corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef # Restore the file with ID 98765abcdef without its associated permissions -corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef --skip-permissions +corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef --no-permissions # Restore all files named "FY2021 Planning.xlsx" corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd --file "FY2021 Planning.xlsx"