adds cli flags for restore config (#3704)

implements cli integration for configuring restore operation collision policy and restore destination.

flags are hidden and will be made visible at a later time.

---

#### Does this PR need a docs update or release note?

- [x] 🕐 Yes, but in a later PR

#### Type of change

- [x] 🌻 Feature

#### Issue(s)

* #3562

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-07-06 11:15:04 -06:00 committed by GitHub
parent a48b5f415c
commit 24c590040b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 316 additions and 34 deletions

View File

@ -0,0 +1,30 @@
package flags
import (
"github.com/spf13/cobra"
"github.com/alcionai/corso/src/pkg/control"
)
const (
CollisionsFN = "collisions"
DestinationFN = "destination"
)
var (
CollisionsFV string
DestinationFV string
)
// AddRestoreConfigFlags adds the restore config flag set.
func AddRestoreConfigFlags(cmd *cobra.Command) {
fs := cmd.Flags()
fs.StringVar(
&CollisionsFV, CollisionsFN, string(control.Skip),
"How to handle item collisions: "+string(control.Skip)+", "+string(control.Copy)+", or "+string(control.Replace))
cobra.CheckErr(fs.MarkHidden(CollisionsFN))
fs.StringVar(
&DestinationFV, DestinationFN, "",
"Overrides the destination where items get restored. '/' places items back in their original location.")
cobra.CheckErr(fs.MarkHidden(DestinationFN))
}

View File

@ -12,7 +12,6 @@ import (
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
)
@ -36,6 +35,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
flags.AddBackupIDFlag(c, true)
flags.AddExchangeDetailsAndRestoreFlags(c)
flags.AddRestoreConfigFlags(c)
flags.AddFailFastFlag(c)
flags.AddCorsoPassphaseFlags(c)
flags.AddAWSCredsFlags(c)
@ -101,8 +101,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
defer utils.CloseRepo(ctx, r)
restoreCfg := control.DefaultRestoreConfig(dttm.HumanReadable)
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
restoreCfg := utils.MakeRestoreConfig(ctx, opts.RestoreCfg, dttm.HumanReadable)
sel := utils.IncludeExchangeRestoreDataSelectors(opts)
utils.FilterExchangeRestoreInfoSelectors(sel, opts)

View File

@ -62,15 +62,18 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() {
"exchange",
"--" + flags.RunModeFN, flags.RunModeFlagTest,
"--" + flags.BackupFN, testdata.BackupInput,
"--" + flags.ContactFN, testdata.FlgInputs(testdata.ContactInput),
"--" + flags.ContactFolderFN, testdata.FlgInputs(testdata.ContactFldInput),
"--" + flags.ContactNameFN, testdata.ContactNameInput,
"--" + flags.EmailFN, testdata.FlgInputs(testdata.EmailInput),
"--" + flags.EmailFolderFN, testdata.FlgInputs(testdata.EmailFldInput),
"--" + flags.EmailReceivedAfterFN, testdata.EmailReceivedAfterInput,
"--" + flags.EmailReceivedBeforeFN, testdata.EmailReceivedBeforeInput,
"--" + flags.EmailSenderFN, testdata.EmailSenderInput,
"--" + flags.EmailSubjectFN, testdata.EmailSubjectInput,
"--" + flags.EventFN, testdata.FlgInputs(testdata.EventInput),
"--" + flags.EventCalendarFN, testdata.FlgInputs(testdata.EventCalInput),
"--" + flags.EventOrganizerFN, testdata.EventOrganizerInput,
@ -78,6 +81,10 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() {
"--" + flags.EventStartsAfterFN, testdata.EventStartsAfterInput,
"--" + flags.EventStartsBeforeFN, testdata.EventStartsBeforeInput,
"--" + flags.EventSubjectFN, testdata.EventSubjectInput,
"--" + flags.CollisionsFN, testdata.Collisions,
"--" + flags.DestinationFN, testdata.Destination,
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
"--" + flags.AWSSessionTokenFN, testdata.AWSSessionToken,
@ -116,6 +123,9 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() {
assert.Equal(t, testdata.EventStartsBeforeInput, opts.EventStartsBefore)
assert.Equal(t, testdata.EventSubjectInput, opts.EventSubject)
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
assert.Equal(t, testdata.AWSSessionToken, flags.AWSSessionTokenFV)

View File

@ -12,7 +12,6 @@ import (
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
)
@ -36,6 +35,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
flags.AddBackupIDFlag(c, true)
flags.AddOneDriveDetailsAndRestoreFlags(c)
flags.AddRestorePermissionsFlag(c)
flags.AddRestoreConfigFlags(c)
flags.AddFailFastFlag(c)
flags.AddCorsoPassphaseFlags(c)
flags.AddAWSCredsFlags(c)
@ -100,12 +100,11 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error {
defer utils.CloseRepo(ctx, r)
restoreCfg := control.DefaultRestoreConfig(dttm.HumanReadableDriveItem)
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
sel := utils.IncludeOneDriveRestoreDataSelectors(opts)
utils.FilterOneDriveRestoreInfoSelectors(sel, opts)
restoreCfg := utils.MakeRestoreConfig(ctx, opts.RestoreCfg, dttm.HumanReadableDriveItem)
ro, err := r.NewRestore(ctx, flags.BackupIDFV, sel.Selector, restoreCfg)
if err != nil {
return Only(ctx, clues.Wrap(err, "Failed to initialize OneDrive restore"))

View File

@ -67,6 +67,10 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() {
"--" + flags.FileCreatedBeforeFN, testdata.FileCreatedBeforeInput,
"--" + flags.FileModifiedAfterFN, testdata.FileModifiedAfterInput,
"--" + flags.FileModifiedBeforeFN, testdata.FileModifiedBeforeInput,
"--" + flags.CollisionsFN, testdata.Collisions,
"--" + flags.DestinationFN, testdata.Destination,
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
"--" + flags.AWSSessionTokenFN, testdata.AWSSessionToken,
@ -93,6 +97,9 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() {
assert.Equal(t, testdata.FileModifiedAfterInput, opts.FileModifiedAfter)
assert.Equal(t, testdata.FileModifiedBeforeInput, opts.FileModifiedBefore)
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
assert.Equal(t, testdata.AWSSessionToken, flags.AWSSessionTokenFV)

View File

@ -12,7 +12,6 @@ import (
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
)
@ -36,8 +35,8 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
flags.AddBackupIDFlag(c, true)
flags.AddSharePointDetailsAndRestoreFlags(c)
flags.AddRestorePermissionsFlag(c)
flags.AddRestoreConfigFlags(c)
flags.AddFailFastFlag(c)
flags.AddCorsoPassphaseFlags(c)
flags.AddAWSCredsFlags(c)
flags.AddAzureCredsFlags(c)
@ -107,12 +106,11 @@ func restoreSharePointCmd(cmd *cobra.Command, args []string) error {
defer utils.CloseRepo(ctx, r)
restoreCfg := control.DefaultRestoreConfig(dttm.HumanReadableDriveItem)
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts)
utils.FilterSharePointRestoreInfoSelectors(sel, opts)
restoreCfg := utils.MakeRestoreConfig(ctx, opts.RestoreCfg, dttm.HumanReadableDriveItem)
ro, err := r.NewRestore(ctx, flags.BackupIDFV, sel.Selector, restoreCfg)
if err != nil {
return Only(ctx, clues.Wrap(err, "Failed to initialize SharePoint restore"))

View File

@ -72,6 +72,10 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() {
"--" + flags.ListFolderFN, testdata.FlgInputs(testdata.ListFolderInput),
"--" + flags.PageFN, testdata.FlgInputs(testdata.PageInput),
"--" + flags.PageFolderFN, testdata.FlgInputs(testdata.PageFolderInput),
"--" + flags.CollisionsFN, testdata.Collisions,
"--" + flags.DestinationFN, testdata.Destination,
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
"--" + flags.AWSSessionTokenFN, testdata.AWSSessionToken,
@ -105,6 +109,9 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() {
assert.ElementsMatch(t, testdata.PageInput, opts.Page)
assert.ElementsMatch(t, testdata.PageFolderInput, opts.PageFolder)
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
assert.Equal(t, testdata.AWSSessionToken, flags.AWSSessionTokenFV)

View File

@ -6,15 +6,13 @@ import (
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/alcionai/corso/src/internal/common/dttm"
)
// StubRootCmd builds a stub cobra command to be used as
// the root command for integration testing on the CLI
func StubRootCmd(args ...string) *cobra.Command {
id := uuid.NewString()
now := dttm.Format(time.Now())
now := time.Now().UTC().Format(time.RFC3339Nano)
cmdArg := "testing-corso"
c := &cobra.Command{
Use: cmdArg,

View File

@ -30,6 +30,8 @@ type ExchangeOpts struct {
EventStartsBefore string
EventSubject string
RestoreCfg RestoreCfgOpts
Populated flags.PopulatedFlags
}
@ -57,6 +59,11 @@ func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts {
EventStartsBefore: flags.EventStartsBeforeFV,
EventSubject: flags.EventSubjectFV,
RestoreCfg: makeRestoreCfgOpts(cmd),
// populated contains the list of flags that appear in the
// command, according to pflags. Use this to differentiate
// between an "empty" and a "missing" value.
Populated: flags.GetPopulatedFlags(cmd),
}
}
@ -132,7 +139,7 @@ func ValidateExchangeRestoreFlags(backupID string, opts ExchangeOpts) error {
return clues.New("invalid format for event-recurs")
}
return nil
return validateRestoreConfigFlags(flags.CollisionsFV, opts.RestoreCfg)
}
// IncludeExchangeRestoreDataSelectors builds the common data-selector

View File

@ -18,6 +18,8 @@ type OneDriveOpts struct {
FileModifiedAfter string
FileModifiedBefore string
RestoreCfg RestoreCfgOpts
Populated flags.PopulatedFlags
}
@ -32,6 +34,11 @@ func MakeOneDriveOpts(cmd *cobra.Command) OneDriveOpts {
FileModifiedAfter: flags.FileModifiedAfterFV,
FileModifiedBefore: flags.FileModifiedBeforeFV,
RestoreCfg: makeRestoreCfgOpts(cmd),
// populated contains the list of flags that appear in the
// command, according to pflags. Use this to differentiate
// between an "empty" and a "missing" value.
Populated: flags.GetPopulatedFlags(cmd),
}
}
@ -58,7 +65,7 @@ func ValidateOneDriveRestoreFlags(backupID string, opts OneDriveOpts) error {
return clues.New("invalid time format for modified-before")
}
return nil
return validateRestoreConfigFlags(flags.CollisionsFV, opts.RestoreCfg)
}
// AddOneDriveFilter adds the scope of the provided values to the selector's

View File

@ -0,0 +1,65 @@
package utils
import (
"context"
"github.com/alcionai/clues"
"github.com/spf13/cobra"
"github.com/alcionai/corso/src/cli/flags"
. "github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/control"
)
type RestoreCfgOpts struct {
Collisions string
Destination string
Populated flags.PopulatedFlags
}
func makeRestoreCfgOpts(cmd *cobra.Command) RestoreCfgOpts {
return RestoreCfgOpts{
Collisions: flags.CollisionsFV,
Destination: flags.DestinationFV,
// populated contains the list of flags that appear in the
// command, according to pflags. Use this to differentiate
// between an "empty" and a "missing" value.
Populated: flags.GetPopulatedFlags(cmd),
}
}
// validateRestoreConfigFlags checks common restore flags for
// correctness and interdependencies.
func validateRestoreConfigFlags(fv string, opts RestoreCfgOpts) error {
_, populated := opts.Populated[flags.CollisionsFN]
_, foundInValidSet := control.ValidCollisionPolicies()[control.CollisionPolicy(fv)]
if populated && !foundInValidSet {
return clues.New("invalid entry for " + flags.CollisionsFN)
}
return nil
}
func MakeRestoreConfig(
ctx context.Context,
opts RestoreCfgOpts,
locationTimeFormat dttm.TimeFormat,
) control.RestoreConfig {
restoreCfg := control.DefaultRestoreConfig(locationTimeFormat)
if _, ok := opts.Populated[flags.CollisionsFN]; ok {
restoreCfg.OnCollision = control.CollisionPolicy(opts.Collisions)
}
if _, ok := opts.Populated[flags.DestinationFN]; ok {
restoreCfg.Location = opts.Destination
}
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
return restoreCfg
}

View File

@ -0,0 +1,137 @@
package utils
import (
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control"
)
type RestoreCfgUnitSuite struct {
tester.Suite
}
func TestRestoreCfgUnitSuite(t *testing.T) {
suite.Run(t, &RestoreCfgUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *RestoreCfgUnitSuite) TestValidateRestoreConfigFlags() {
table := []struct {
name string
fv string
opts RestoreCfgOpts
expect assert.ErrorAssertionFunc
}{
{
name: "no error",
fv: string(control.Skip),
opts: RestoreCfgOpts{
Collisions: string(control.Skip),
Populated: flags.PopulatedFlags{
flags.CollisionsFN: {},
},
},
expect: assert.NoError,
},
{
name: "bad but not populated",
fv: "foo",
opts: RestoreCfgOpts{
Collisions: "foo",
Populated: flags.PopulatedFlags{},
},
expect: assert.NoError,
},
{
name: "error",
fv: "foo",
opts: RestoreCfgOpts{
Collisions: "foo",
Populated: flags.PopulatedFlags{
flags.CollisionsFN: {},
},
},
expect: assert.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
err := validateRestoreConfigFlags(test.fv, test.opts)
test.expect(suite.T(), err, clues.ToCore(err))
})
}
}
func (suite *RestoreCfgUnitSuite) TestMakeRestoreConfig() {
rco := &RestoreCfgOpts{
Collisions: "collisions",
Destination: "destination",
}
table := []struct {
name string
populated flags.PopulatedFlags
expect control.RestoreConfig
}{
{
name: "not populated",
populated: flags.PopulatedFlags{},
expect: control.RestoreConfig{
OnCollision: control.Skip,
Location: "Corso_Restore_",
},
},
{
name: "collision populated",
populated: flags.PopulatedFlags{
flags.CollisionsFN: {},
},
expect: control.RestoreConfig{
OnCollision: control.CollisionPolicy("collisions"),
Location: "Corso_Restore_",
},
},
{
name: "destination populated",
populated: flags.PopulatedFlags{
flags.DestinationFN: {},
},
expect: control.RestoreConfig{
OnCollision: control.Skip,
Location: "destination",
},
},
{
name: "both populated",
populated: flags.PopulatedFlags{
flags.CollisionsFN: {},
flags.DestinationFN: {},
},
expect: control.RestoreConfig{
OnCollision: control.CollisionPolicy("collisions"),
Location: "destination",
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
opts := *rco
opts.Populated = test.populated
result := MakeRestoreConfig(ctx, opts, dttm.HumanReadable)
assert.Equal(t, test.expect.OnCollision, result.OnCollision)
assert.Contains(t, result.Location, test.expect.Location)
})
}
}

View File

@ -31,6 +31,8 @@ type SharePointOpts struct {
PageFolder []string
Page []string
RestoreCfg RestoreCfgOpts
Populated flags.PopulatedFlags
}
@ -53,6 +55,11 @@ func MakeSharePointOpts(cmd *cobra.Command) SharePointOpts {
Page: flags.PageFV,
PageFolder: flags.PageFolderFV,
RestoreCfg: makeRestoreCfgOpts(cmd),
// populated contains the list of flags that appear in the
// command, according to pflags. Use this to differentiate
// between an "empty" and a "missing" value.
Populated: flags.GetPopulatedFlags(cmd),
}
}
@ -88,7 +95,7 @@ func ValidateSharePointRestoreFlags(backupID string, opts SharePointOpts) error
return clues.New("invalid time format for " + flags.FileModifiedBeforeFN)
}
return nil
return validateRestoreConfigFlags(flags.CollisionsFV, opts.RestoreCfg)
}
// AddSharePointInfo adds the scope of the provided values to the selector's

View File

@ -44,6 +44,8 @@ var (
PageFolderInput = []string{"pageFolder1", "pageFolder2"}
PageInput = []string{"page1", "page2"}
Collisions = "collisions"
Destination = "destination"
RestorePermissions = true
AzureClientID = "testAzureClientId"

View File

@ -72,6 +72,14 @@ const (
Replace CollisionPolicy = "replace"
)
func ValidCollisionPolicies() map[CollisionPolicy]struct{} {
return map[CollisionPolicy]struct{}{
Skip: {},
Copy: {},
Replace: {},
}
}
const RootLocation = "/"
// RestoreConfig contains

View File

@ -1,4 +1,4 @@
package control
package test
import (
"testing"
@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control"
)
type OptionsUnitSuite struct {
@ -20,19 +21,19 @@ func TestOptionsUnitSuite(t *testing.T) {
func (suite *OptionsUnitSuite) TestEnsureRestoreConfigDefaults() {
table := []struct {
name string
input RestoreConfig
expect RestoreConfig
input control.RestoreConfig
expect control.RestoreConfig
}{
{
name: "populated",
input: RestoreConfig{
OnCollision: Copy,
input: control.RestoreConfig{
OnCollision: control.Copy,
ProtectedResource: "batman",
Location: "badman",
Drive: "hatman",
},
expect: RestoreConfig{
OnCollision: Copy,
expect: control.RestoreConfig{
OnCollision: control.Copy,
ProtectedResource: "batman",
Location: "badman",
Drive: "hatman",
@ -40,14 +41,14 @@ func (suite *OptionsUnitSuite) TestEnsureRestoreConfigDefaults() {
},
{
name: "unpopulated",
input: RestoreConfig{
OnCollision: Unknown,
input: control.RestoreConfig{
OnCollision: control.Unknown,
ProtectedResource: "",
Location: "",
Drive: "",
},
expect: RestoreConfig{
OnCollision: Skip,
expect: control.RestoreConfig{
OnCollision: control.Skip,
ProtectedResource: "",
Location: "",
Drive: "",
@ -55,14 +56,14 @@ func (suite *OptionsUnitSuite) TestEnsureRestoreConfigDefaults() {
},
{
name: "populated, but modified",
input: RestoreConfig{
OnCollision: CollisionPolicy("batman"),
input: control.RestoreConfig{
OnCollision: control.CollisionPolicy("batman"),
ProtectedResource: "",
Location: "/",
Drive: "",
},
expect: RestoreConfig{
OnCollision: Skip,
expect: control.RestoreConfig{
OnCollision: control.Skip,
ProtectedResource: "",
Location: "",
Drive: "",
@ -76,7 +77,7 @@ func (suite *OptionsUnitSuite) TestEnsureRestoreConfigDefaults() {
ctx, flush := tester.NewContext(t)
defer flush()
result := EnsureRestoreConfigDefaults(ctx, test.input)
result := control.EnsureRestoreConfigDefaults(ctx, test.input)
assert.Equal(t, test.expect, result)
})
}