From efaa2da1bb53642a529c6ce759ff11b6932c5750 Mon Sep 17 00:00:00 2001 From: Keepers <104464746+ryanfkeepers@users.noreply.github.com> Date: Fri, 15 Jul 2022 10:27:20 -0600 Subject: [PATCH] add --all and --data to backup create (#342) * add --all and --data to backup create Adds flags for backing up all exchange data, and for isolating the data in the backup by data type. Introduces validation and selector creation in backup create. Switches the --user flag variable type from a string to a []string. --- src/cli/backup/exchange.go | 87 +++++++++++++++-- src/cli/backup/exchange_test.go | 163 +++++++++++++++++++++++++++++++ src/cli/restore/exchange.go | 4 +- src/cli/restore/exchange_test.go | 5 +- src/cli/utils/utils.go | 4 + 5 files changed, 253 insertions(+), 10 deletions(-) diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index d944ea23f..ed9e4b20d 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -15,8 +15,16 @@ import ( // exchange bucket info from flags var ( - user string backupDetailsID string + exchangeAll bool + exchangeData []string + user []string +) + +const ( + dataContacts = "contacts" + dataEmail = "email" + dataEvents = "events" ) // called by backup.go to map parent subcommands to provider-specific handling. @@ -28,12 +36,18 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { switch parent.Use { case createCommand: c, fs = utils.AddCommand(parent, exchangeCreateCmd) - fs.StringVar(&user, "user", "", "ID of the user whose Exchange data is to be backed up.") + fs.StringArrayVar(&user, "user", nil, "Back up Exchange data by user ID; accepts "+utils.Wildcard+" to select all users") + fs.BoolVar(&exchangeAll, "all", false, "Back up all Exchange data for all users") + fs.StringArrayVar( + &exchangeData, + "data", + nil, + "Select one or more types of data to backup: "+dataEmail+", "+dataContacts+", or "+dataEvents) case listCommand: c, _ = utils.AddCommand(parent, exchangeListCmd) case detailsCommand: c, fs = utils.AddCommand(parent, exchangeDetailsCmd) - fs.StringVar(&backupDetailsID, "backup-details", "", "ID of the backup details to be shown.") + fs.StringVar(&backupDetailsID, "backup-details", "", "ID of the backup details to be shown") cobra.CheckErr(c.MarkFlagRequired("backup-details")) } return c @@ -56,6 +70,9 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { if utils.HasNoFlagsAndShownHelp(cmd) { return nil } + if err := validateBackupCreateFlags(exchangeAll, user, exchangeData); err != nil { + return err + } s, acct, err := config.GetStorageAndAccount(true, nil) if err != nil { @@ -79,10 +96,9 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { } defer utils.CloseRepo(ctx, r) - sel := selectors.NewExchangeBackup() - sel.Include(sel.Users(user)) + sel := exchangeBackupCreateSelectors(exchangeAll, user, exchangeData) - bo, err := r.NewBackup(ctx, sel.Selector) + bo, err := r.NewBackup(ctx, sel) if err != nil { return errors.Wrap(err, "Failed to initialize Exchange backup") } @@ -97,6 +113,65 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { return nil } +func exchangeBackupCreateSelectors(all bool, users, data []string) selectors.Selector { + sel := selectors.NewExchangeBackup() + if all { + sel.Include(sel.Users(selectors.All)) + return sel.Selector + } + if len(data) == 0 { + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.ContactFolders(user, selectors.All)) + sel.Include(sel.MailFolders(user, selectors.All)) + sel.Include(sel.Events(user, selectors.All)) + } + } + for _, d := range data { + switch d { + case dataContacts: + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.ContactFolders(user, selectors.All)) + } + case dataEmail: + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.MailFolders(user, selectors.All)) + } + case dataEvents: + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.Events(user, selectors.All)) + } + } + } + return sel.Selector +} + +func validateBackupCreateFlags(all bool, users, data []string) error { + if len(users) == 0 && !all { + return errors.New("requries one or more --user ids, the wildcard --user *, or the --all flag.") + } + if len(data) > 0 && all { + return errors.New("--all backs up all data, and cannot be reduced with --data") + } + for _, d := range data { + if d != dataContacts && d != dataEmail && d != dataEvents { + return errors.New(d + " is an unrecognized data type; must be one of " + dataContacts + ", " + dataEmail + ", or " + dataEvents) + } + } + return nil +} + // `corso backup list exchange [...]` var exchangeListCmd = &cobra.Command{ Use: exchangeServiceCommand, diff --git a/src/cli/backup/exchange_test.go b/src/cli/backup/exchange_test.go index 8cacad8ee..cbbc9b5e6 100644 --- a/src/cli/backup/exchange_test.go +++ b/src/cli/backup/exchange_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/cli/utils" ctesting "github.com/alcionai/corso/internal/testing" ) @@ -49,3 +50,165 @@ func (suite *ExchangeSuite) TestAddExchangeCommands() { }) } } + +func (suite *ExchangeSuite) TestValidateBackupCreateFlags() { + table := []struct { + name string + all bool + user, data []string + expect assert.ErrorAssertionFunc + }{ + { + name: "no users, not all", + expect: assert.Error, + }, + { + name: "all and data", + all: true, + data: []string{dataEmail}, + expect: assert.Error, + }, + { + name: "unrecognized data", + user: []string{"fnord"}, + data: []string{"smurfs"}, + expect: assert.Error, + }, + { + name: "users, not all", + user: []string{"fnord"}, + expect: assert.NoError, + }, + { + name: "no users, all", + all: true, + expect: assert.NoError, + }, + { + name: "users, all", + all: true, + user: []string{"fnord"}, + expect: assert.NoError, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, validateBackupCreateFlags(test.all, test.user, test.data)) + }) + } +} + +func (suite *ExchangeSuite) TestExchangeBackupCreateSelectors() { + table := []struct { + name string + all bool + user, data []string + expectIncludeLen int + }{ + { + name: "all", + all: true, + expectIncludeLen: 1, + }, + { + name: "all users, no data", + user: []string{utils.Wildcard}, + expectIncludeLen: 3, + }, + { + name: "single user, no data", + user: []string{"u1"}, + expectIncludeLen: 3, + }, + { + name: "all users, contacts", + user: []string{utils.Wildcard}, + data: []string{dataContacts}, + expectIncludeLen: 1, + }, + { + name: "single user, contacts", + user: []string{"u1"}, + data: []string{dataContacts}, + expectIncludeLen: 1, + }, + { + name: "all users, email", + user: []string{utils.Wildcard}, + data: []string{dataEmail}, + expectIncludeLen: 1, + }, + { + name: "single user, email", + user: []string{"u1"}, + data: []string{dataEmail}, + expectIncludeLen: 1, + }, + { + name: "all users, events", + user: []string{utils.Wildcard}, + data: []string{dataEvents}, + expectIncludeLen: 1, + }, + { + name: "single user, events", + user: []string{"u1"}, + data: []string{dataEvents}, + expectIncludeLen: 1, + }, + { + name: "all users, contacts + email", + user: []string{utils.Wildcard}, + data: []string{dataContacts, dataEmail}, + expectIncludeLen: 2, + }, + { + name: "single user, contacts + email", + user: []string{"u1"}, + data: []string{dataContacts, dataEmail}, + expectIncludeLen: 2, + }, + { + name: "all users, email + events", + user: []string{utils.Wildcard}, + data: []string{dataEmail, dataEvents}, + expectIncludeLen: 2, + }, + { + name: "single user, email + events", + user: []string{"u1"}, + data: []string{dataEmail, dataEvents}, + expectIncludeLen: 2, + }, + { + name: "all users, events + contacts", + user: []string{utils.Wildcard}, + data: []string{dataEvents, dataContacts}, + expectIncludeLen: 2, + }, + { + name: "single user, events + contacts", + user: []string{"u1"}, + data: []string{dataEvents, dataContacts}, + expectIncludeLen: 2, + }, + { + name: "many users, events", + user: []string{"fnord", "smarf"}, + data: []string{dataEvents}, + expectIncludeLen: 2, + }, + { + name: "many users, events + contacts", + user: []string{"fnord", "smarf"}, + data: []string{dataEvents, dataContacts}, + expectIncludeLen: 4, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sel := exchangeBackupCreateSelectors(test.all, test.user, test.data) + assert.Equal(t, test.expectIncludeLen, len(sel.Includes)) + }) + } +} diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index e3658e455..2948edf4c 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -127,10 +127,10 @@ func validateRestoreFlags(u, f, m, rpid string) error { return errors.New("a restore point ID is requried") } lu, lf, lm := len(u), len(f), len(m) - if (lu == 0 || u == "*") && (lf+lm > 0) { + if (lu == 0 || u == utils.Wildcard) && (lf+lm > 0) { return errors.New("a specific --user must be provided if --folder or --mail is specified") } - if (lf == 0 || f == "*") && lm > 0 { + if (lf == 0 || f == utils.Wildcard) && lm > 0 { return errors.New("a specific --folder must be provided if a --mail is specified") } return nil diff --git a/src/cli/restore/exchange_test.go b/src/cli/restore/exchange_test.go index fdbc5aa4a..31bb29b70 100644 --- a/src/cli/restore/exchange_test.go +++ b/src/cli/restore/exchange_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/cli/utils" ctesting "github.com/alcionai/corso/internal/testing" ) @@ -27,10 +28,10 @@ func (suite *ExchangeSuite) TestValidateRestoreFlags() { }{ {"all populated", "u", "f", "m", "rpid", assert.NoError}, {"folder missing user", "", "f", "m", "rpid", assert.Error}, - {"folder with wildcard user", "*", "f", "m", "rpid", assert.Error}, + {"folder with wildcard user", utils.Wildcard, "f", "m", "rpid", assert.Error}, {"mail missing user", "", "", "m", "rpid", assert.Error}, {"mail missing folder", "u", "", "m", "rpid", assert.Error}, - {"mail with wildcard folder", "u", "*", "m", "rpid", assert.Error}, + {"mail with wildcard folder", "u", utils.Wildcard, "m", "rpid", assert.Error}, {"missing backup id", "u", "f", "m", "", assert.Error}, {"all missing", "", "", "", "rpid", assert.NoError}, } diff --git a/src/cli/utils/utils.go b/src/cli/utils/utils.go index 9a07f891d..185ef42f2 100644 --- a/src/cli/utils/utils.go +++ b/src/cli/utils/utils.go @@ -10,6 +10,10 @@ import ( "github.com/spf13/pflag" ) +const ( + Wildcard = "*" +) + // RequireProps validates the existence of the properties // in the map. Expects the format map[propName]propVal. func RequireProps(props map[string]string) error {