diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index 7de12cc25..5ce2a8f64 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -4,21 +4,28 @@ import ( "github.com/spf13/cobra" ) -var backupCommands = []func(parent *cobra.Command) *cobra.Command{ +var subCommands = []*cobra.Command{ + createCmd, + listCmd, + detailsCmd, + deleteCmd, +} + +var serviceCommands = []func(parent *cobra.Command) *cobra.Command{ addExchangeCommands, } // AddCommands attaches all `corso backup * *` commands to the parent. func AddCommands(parent *cobra.Command) { parent.AddCommand(backupCmd) - backupCmd.AddCommand(createCmd) - backupCmd.AddCommand(listCmd) - backupCmd.AddCommand(detailsCmd) + for _, sc := range subCommands { + backupCmd.AddCommand(sc) + } - for _, addBackupTo := range backupCommands { - addBackupTo(createCmd) - addBackupTo(listCmd) - addBackupTo(detailsCmd) + for _, addBackupTo := range serviceCommands { + for _, sc := range subCommands { + addBackupTo(sc) + } } } @@ -75,7 +82,7 @@ func handleListCmd(cmd *cobra.Command, args []string) error { } // The backup details subcommand. -// `corso backup list [...]` +// `corso backup details [...]` var ( detailsCommand = "details" detailsCmd = &cobra.Command{ @@ -91,3 +98,21 @@ var ( func handleDetailsCmd(cmd *cobra.Command, args []string) error { return cmd.Help() } + +// The backup delete subcommand. +// `corso backup delete [...]` +var ( + deleteCommand = "delete" + deleteCmd = &cobra.Command{ + Use: deleteCommand, + Short: "Deletes a backup for a service", + RunE: handleDeleteCmd, + Args: cobra.NoArgs, + } +) + +// Handler for calls to `corso backup delete`. +// Produces the same output as `corso backup delete --help`. +func handleDeleteCmd(cmd *cobra.Command, args []string) error { + return cmd.Help() +} diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 7487662e8..905790439 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/corso/cli/options" . "github.com/alcionai/corso/cli/print" "github.com/alcionai/corso/cli/utils" + "github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/pkg/backup" "github.com/alcionai/corso/pkg/logger" "github.com/alcionai/corso/pkg/repository" @@ -134,6 +135,11 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { "", "Select backup details where the email subject lines contain this value", ) + + case deleteCommand: + c, fs = utils.AddCommand(parent, exchangeDeleteCmd()) + fs.StringVar(&backupID, "backup", "", "ID of the backup containing the details to be shown") + cobra.CheckErr(c.MarkFlagRequired("backup")) } return c @@ -506,3 +512,54 @@ func validateExchangeBackupDetailFlags( } return nil } + +// ------------------------------------------------------------------------------------------------ +// backup delete +// ------------------------------------------------------------------------------------------------ + +// `corso backup delete exchange [...]` +func exchangeDeleteCmd() *cobra.Command { + return &cobra.Command{ + Use: exchangeServiceCommand, + Short: "Delete backed-up M365 Exchange service data", + RunE: deleteExchangeCmd, + Args: cobra.NoArgs, + } +} + +// deletes an exchange service backup. +func deleteExchangeCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + s, acct, err := config.GetStorageAndAccount(ctx, true, nil) + if err != nil { + return Only(ctx, err) + } + + m365, err := acct.M365Config() + if err != nil { + return Only(ctx, errors.Wrap(err, "Failed to parse m365 account config")) + } + + logger.Ctx(ctx).Debugw( + "Called - "+cmd.CommandPath(), + "tenantID", m365.TenantID, + "clientID", m365.ClientID, + "hasClientSecret", len(m365.ClientSecret) > 0) + + r, err := repository.Connect(ctx, acct, s) + if err != nil { + return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) + } + defer utils.CloseRepo(ctx, r) + + if err := r.DeleteBackup(ctx, model.StableID(backupID)); err != nil { + return Only(ctx, errors.Wrapf(err, "Deleting backup %s", backupID)) + } + + return nil +} diff --git a/src/cli/backup/exchange_integration_test.go b/src/cli/backup/exchange_integration_test.go index d35522d25..d4e60958d 100644 --- a/src/cli/backup/exchange_integration_test.go +++ b/src/cli/backup/exchange_integration_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -217,3 +218,106 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { }) } } + +// --------------------------------------------------------------------------- +// tests for deleting backups +// --------------------------------------------------------------------------- + +type BackupDeleteExchangeIntegrationSuite struct { + suite.Suite + acct account.Account + st storage.Storage + vpr *viper.Viper + cfgFP string + repo *repository.Repository + backupOp operations.BackupOperation +} + +func TestBackupDeleteExchangeIntegrationSuite(t *testing.T) { + if err := tester.RunOnAny( + tester.CorsoCITests, + tester.CorsoCLITests, + tester.CorsoCLIBackupTests, + ); err != nil { + t.Skip(err) + } + suite.Run(t, new(BackupDeleteExchangeIntegrationSuite)) +} + +func (suite *BackupDeleteExchangeIntegrationSuite) SetupSuite() { + t := suite.T() + _, err := tester.GetRequiredEnvSls( + tester.AWSStorageCredEnvs, + tester.M365AcctCredEnvs) + require.NoError(t, err) + + // prepare common details + suite.acct = tester.NewM365Account(t) + suite.st = tester.NewPrefixedS3Storage(t) + + cfg, err := suite.st.S3Config() + require.NoError(t, err) + + force := map[string]string{ + tester.TestCfgAccountProvider: "M365", + tester.TestCfgStorageProvider: "S3", + tester.TestCfgPrefix: cfg.Prefix, + } + suite.vpr, suite.cfgFP, err = tester.MakeTempTestConfigClone(t, force) + require.NoError(t, err) + ctx := config.SetViper(tester.NewContext(), suite.vpr) + + // init the repo first + suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st) + require.NoError(t, err) + + m365UserID := tester.M365UserID(t) + + // some tests require an existing backup + sel := selectors.NewExchangeBackup() + sel.Include(sel.MailFolders([]string{m365UserID}, []string{"Inbox"})) + + suite.backupOp, err = suite.repo.NewBackup( + ctx, + sel.Selector, + control.NewOptions(false)) + require.NoError(t, suite.backupOp.Run(ctx)) + require.NoError(t, err) +} + +func (suite *BackupDeleteExchangeIntegrationSuite) TestExchangeBackupDeleteCmd() { + ctx := config.SetViper(tester.NewContext(), suite.vpr) + t := suite.T() + + cmd := tester.StubRootCmd( + "backup", "delete", "exchange", + "--config-file", suite.cfgFP, + "--backup", string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + // run the command + require.NoError(t, cmd.ExecuteContext(ctx)) + + // a follow-up details call should fail, due to the backup ID being deleted + cmd = tester.StubRootCmd( + "backup", "details", "exchange", + "--config-file", suite.cfgFP, + "--backup", string(suite.backupOp.Results.BackupID)) + cli.BuildCommandTree(cmd) + + require.Error(t, cmd.ExecuteContext(ctx)) +} + +func (suite *BackupDeleteExchangeIntegrationSuite) TestExchangeBackupDeleteCmd_UnknownID() { + ctx := config.SetViper(tester.NewContext(), suite.vpr) + t := suite.T() + + cmd := tester.StubRootCmd( + "backup", "delete", "exchange", + "--config-file", suite.cfgFP, + "--backup", uuid.NewString()) + cli.BuildCommandTree(cmd) + + // unknown backupIDs should error since the modelStore can't find the backup + require.Error(t, cmd.ExecuteContext(ctx)) +} diff --git a/src/cli/backup/exchange_test.go b/src/cli/backup/exchange_test.go index 2a058f284..e5bea46d3 100644 --- a/src/cli/backup/exchange_test.go +++ b/src/cli/backup/exchange_test.go @@ -33,6 +33,7 @@ func (suite *ExchangeSuite) TestAddExchangeCommands() { {"create exchange", createCommand, expectUse, exchangeCreateCmd().Short, createExchangeCmd}, {"list exchange", listCommand, expectUse, exchangeListCmd().Short, listExchangeCmd}, {"details exchange", detailsCommand, expectUse, exchangeDetailsCmd().Short, detailsExchangeCmd}, + {"delete exchange", deleteCommand, expectUse, exchangeDeleteCmd().Short, deleteExchangeCmd}, } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) {