From 3a02e3269b38da6fba3cea40b0395f72edbabb93 Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Wed, 19 Jul 2023 13:03:39 -0600 Subject: [PATCH] look up restore resource if specified If the restore configuration specifies a protected resource as a restore target, use that as the destination for the restore. First step is to ensure the provided target can be retrieved and identified. --- src/cmd/factory/impl/common.go | 2 +- src/internal/common/idname/idname.go | 4 ++ src/internal/m365/backup_test.go | 4 +- src/internal/m365/controller.go | 4 +- src/internal/m365/controller_test.go | 4 +- src/internal/m365/mock/connector.go | 14 ++++ src/internal/operations/help_test.go | 2 +- src/internal/operations/inject/inject.go | 20 ++++++ src/internal/operations/restore.go | 35 ++++++++- src/internal/operations/restore_test.go | 79 +++++++++++++++++++-- src/internal/operations/test/helper_test.go | 2 +- src/pkg/repository/repository.go | 2 +- 12 files changed, 155 insertions(+), 17 deletions(-) diff --git a/src/cmd/factory/impl/common.go b/src/cmd/factory/impl/common.go index 04967dc2a..0b0cdd4a8 100644 --- a/src/cmd/factory/impl/common.go +++ b/src/cmd/factory/impl/common.go @@ -144,7 +144,7 @@ func getControllerAndVerifyResourceOwner( return nil, account.Account{}, nil, clues.Wrap(err, "connecting to graph api") } - id, _, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, resourceOwner, nil) + id, _, err := ctrl.PopulateProtectedResourceIDAndName(ctx, resourceOwner, nil) if err != nil { return nil, account.Account{}, nil, clues.Wrap(err, "verifying user") } diff --git a/src/internal/common/idname/idname.go b/src/internal/common/idname/idname.go index 56460dd6e..ebc40842c 100644 --- a/src/internal/common/idname/idname.go +++ b/src/internal/common/idname/idname.go @@ -28,6 +28,10 @@ type is struct { name string } +func NewProvider(id, name string) *is { + return &is{id, name} +} + func (is is) ID() string { return is.id } func (is is) Name() string { return is.name } diff --git a/src/internal/m365/backup_test.go b/src/internal/m365/backup_test.go index bb59741f8..a671d1b67 100644 --- a/src/internal/m365/backup_test.go +++ b/src/internal/m365/backup_test.go @@ -367,7 +367,7 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() { siteIDs = []string{siteID} ) - id, name, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, siteID, nil) + id, name, err := ctrl.PopulateProtectedResourceIDAndName(ctx, siteID, nil) require.NoError(t, err, clues.ToCore(err)) sel := selectors.NewSharePointBackup(siteIDs) @@ -414,7 +414,7 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() { siteIDs = []string{siteID} ) - id, name, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, siteID, nil) + id, name, err := ctrl.PopulateProtectedResourceIDAndName(ctx, siteID, nil) require.NoError(t, err, clues.ToCore(err)) sel := selectors.NewSharePointBackup(siteIDs) diff --git a/src/internal/m365/controller.go b/src/internal/m365/controller.go index 9b037350b..e3d3d6a80 100644 --- a/src/internal/m365/controller.go +++ b/src/internal/m365/controller.go @@ -248,7 +248,7 @@ func (r resourceClient) getOwnerIDAndNameFrom( return id, name, nil } -// PopulateOwnerIDAndNamesFrom takes the provided owner identifier and produces +// PopulateProtectedResourceIDAndName takes the provided owner identifier and produces // the owner's name and ID from that value. Returns an error if the owner is // not recognized by the current tenant. // @@ -256,7 +256,7 @@ func (r resourceClient) getOwnerIDAndNameFrom( // the tenant before reaching this step. In that case, the data gets handed // down for this func to consume instead of performing further queries. The // data gets stored inside the controller instance for later re-use. -func (ctrl *Controller) PopulateOwnerIDAndNamesFrom( +func (ctrl *Controller) PopulateProtectedResourceIDAndName( ctx context.Context, owner string, // input value, can be either id or name ins idname.Cacher, diff --git a/src/internal/m365/controller_test.go b/src/internal/m365/controller_test.go index aca8280c7..7fb047f65 100644 --- a/src/internal/m365/controller_test.go +++ b/src/internal/m365/controller_test.go @@ -222,7 +222,7 @@ func (suite *ControllerUnitSuite) TestPopulateOwnerIDAndNamesFrom() { ctrl := &Controller{ownerLookup: test.rc} - rID, rName, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, test.owner, test.ins) + rID, rName, err := ctrl.PopulateProtectedResourceIDAndName(ctx, test.owner, test.ins) test.expectErr(t, err, clues.ToCore(err)) assert.Equal(t, test.expectID, rID, "id") assert.Equal(t, test.expectName, rName, "name") @@ -1295,7 +1295,7 @@ func (suite *ControllerIntegrationSuite) TestBackup_CreatesPrefixCollections() { start = time.Now() ) - id, name, err := backupCtrl.PopulateOwnerIDAndNamesFrom(ctx, backupSel.DiscreteOwner, nil) + id, name, err := backupCtrl.PopulateProtectedResourceIDAndName(ctx, backupSel.DiscreteOwner, nil) require.NoError(t, err, clues.ToCore(err)) backupSel.SetDiscreteOwnerIDName(id, name) diff --git a/src/internal/m365/mock/connector.go b/src/internal/m365/mock/connector.go index 977306883..6c6fde6d8 100644 --- a/src/internal/m365/mock/connector.go +++ b/src/internal/m365/mock/connector.go @@ -26,6 +26,10 @@ type Controller struct { Err error Stats data.CollectionStats + + ProtectedResourceID string + ProtectedResourceName string + ProtectedResourceErr error } func (ctrl Controller) ProduceBackupCollections( @@ -71,3 +75,13 @@ func (ctrl Controller) ConsumeRestoreCollections( } func (ctrl Controller) CacheItemInfo(dii details.ItemInfo) {} + +func (ctrl Controller) PopulateProtectedResourceIDAndName( + ctx context.Context, + protectedResource string, // input value, can be either id or name + ins idname.Cacher, +) (string, string, error) { + return ctrl.ProtectedResourceID, + ctrl.ProtectedResourceName, + ctrl.ProtectedResourceErr +} diff --git a/src/internal/operations/help_test.go b/src/internal/operations/help_test.go index 0951572ba..944e35de8 100644 --- a/src/internal/operations/help_test.go +++ b/src/internal/operations/help_test.go @@ -36,7 +36,7 @@ func ControllerWithSelector( t.FailNow() } - id, name, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, sel.DiscreteOwner, ins) + id, name, err := ctrl.PopulateProtectedResourceIDAndName(ctx, sel.DiscreteOwner, ins) if !assert.NoError(t, err, clues.ToCore(err)) { if onFail != nil { onFail() diff --git a/src/internal/operations/inject/inject.go b/src/internal/operations/inject/inject.go index 912b46743..71d3de9fd 100644 --- a/src/internal/operations/inject/inject.go +++ b/src/internal/operations/inject/inject.go @@ -48,6 +48,7 @@ type ( Wait() *data.CollectionStats CacheItemInfoer + PopulateProtectedResourceIDAndNamer } CacheItemInfoer interface { @@ -59,6 +60,25 @@ type ( CacheItemInfo(v details.ItemInfo) } + PopulateProtectedResourceIDAndNamer interface { + // PopulateProtectedResourceIDAndName takes the provided owner identifier and produces + // the owner's name and ID from that value. Returns an error if the owner is + // not recognized by the current tenant. + // + // The id-name swapper should be optional. Some processes will look up all owners in + // the tenant before reaching this step. In that case, the data gets handed + // down for this func to consume instead of performing further queries. The + // data gets stored inside the controller instance for later re-use. + PopulateProtectedResourceIDAndName( + ctx context.Context, + owner string, // input value, can be either id or name + ins idname.Cacher, + ) ( + id, name string, + err error, + ) + } + RepoMaintenancer interface { RepoMaintenance(ctx context.Context, opts repository.Maintenance) error } diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 0f853a853..e7343e607 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -11,6 +11,7 @@ import ( "github.com/alcionai/corso/src/internal/common/crash" "github.com/alcionai/corso/src/internal/common/dttm" + "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/events" @@ -217,7 +218,19 @@ func (op *RestoreOperation) do( return nil, clues.Wrap(err, "getting backup and details") } - observe.Message(ctx, "Restoring", observe.Bullet, clues.Hide(bup.Selector.DiscreteOwner)) + restoreProtectedResource, err := chooseRestoreResource(ctx, op.rc, op.RestoreCfg, bup.Selector) + if err != nil { + return nil, clues.Wrap(err, "getting destination protected resource") + } + + ctx = clues.Add( + ctx, + "backup_protected_resource_id", bup.Selector.ID(), + "backup_protected_resource_name", clues.Hide(bup.Selector.Name()), + "restore_protected_resource_id", restoreProtectedResource.ID(), + "restore_protected_resource_name", clues.Hide(restoreProtectedResource.Name())) + + observe.Message(ctx, "Restoring", observe.Bullet, clues.Hide(restoreProtectedResource.Name())) paths, err := formatDetailsForRestoration( ctx, @@ -232,8 +245,6 @@ func (op *RestoreOperation) do( ctx = clues.Add( ctx, - "resource_owner_id", bup.Selector.ID(), - "resource_owner_name", clues.Hide(bup.Selector.Name()), "details_entries", len(deets.Entries), "details_paths", len(paths), "backup_snapshot_id", bup.SnapshotID, @@ -321,6 +332,24 @@ func (op *RestoreOperation) persistResults( return op.Errors.Failure() } +func chooseRestoreResource( + ctx context.Context, + pprian inject.PopulateProtectedResourceIDAndNamer, + restoreCfg control.RestoreConfig, + orig idname.Provider, +) (idname.Provider, error) { + if len(restoreCfg.ProtectedResource) == 0 { + return orig, nil + } + + id, name, err := pprian.PopulateProtectedResourceIDAndName( + ctx, + restoreCfg.ProtectedResource, + nil) + + return idname.NewProvider(id, name), clues.Stack(err).OrNil() +} + // --------------------------------------------------------------------------- // Restorer funcs // --------------------------------------------------------------------------- diff --git a/src/internal/operations/restore_test.go b/src/internal/operations/restore_test.go index 01e2303d2..3ad265aa1 100644 --- a/src/internal/operations/restore_test.go +++ b/src/internal/operations/restore_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/internal/common/dttm" + "github.com/alcionai/corso/src/internal/common/idname" inMock "github.com/alcionai/corso/src/internal/common/idname/mock" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/events" @@ -40,15 +42,15 @@ import ( // unit // --------------------------------------------------------------------------- -type RestoreOpSuite struct { +type RestoreOpUnitSuite struct { tester.Suite } -func TestRestoreOpSuite(t *testing.T) { - suite.Run(t, &RestoreOpSuite{Suite: tester.NewUnitSuite(t)}) +func TestRestoreOpUnitSuite(t *testing.T) { + suite.Run(t, &RestoreOpUnitSuite{Suite: tester.NewUnitSuite(t)}) } -func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { +func (suite *RestoreOpUnitSuite) TestRestoreOperation_PersistResults() { var ( kw = &kopia.Wrapper{} sw = &store.Wrapper{} @@ -138,6 +140,75 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { } } +func (suite *RestoreOpUnitSuite) TestChooseRestoreResource() { + var ( + id = "id" + name = "name" + cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable) + ) + + cfgWithPR.ProtectedResource = "cfgid" + + table := []struct { + name string + cfg control.RestoreConfig + ctrl *mock.Controller + orig idname.Provider + expectErr assert.ErrorAssertionFunc + expectProvider assert.ValueAssertionFunc + expectID string + expectName string + }{ + { + name: "use original", + cfg: control.DefaultRestoreConfig(dttm.HumanReadable), + ctrl: &mock.Controller{ + ProtectedResourceID: id, + ProtectedResourceName: name, + }, + orig: idname.NewProvider("oid", "oname"), + expectErr: assert.NoError, + expectID: "oid", + expectName: "oname", + }, + { + name: "look up resource with iface", + cfg: cfgWithPR, + ctrl: &mock.Controller{ + ProtectedResourceID: id, + ProtectedResourceName: name, + }, + orig: idname.NewProvider("oid", "oname"), + expectErr: assert.NoError, + expectID: id, + expectName: name, + }, + { + name: "error looking up protected resource", + cfg: cfgWithPR, + ctrl: &mock.Controller{ + ProtectedResourceErr: assert.AnError, + }, + orig: idname.NewProvider("oid", "oname"), + expectErr: assert.Error, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + result, err := chooseRestoreResource(ctx, test.ctrl, test.cfg, test.orig) + test.expectErr(t, err, clues.ToCore(err)) + require.NotNil(t, result) + assert.Equal(t, test.expectID, result.ID()) + assert.Equal(t, test.expectName, result.Name()) + }) + } +} + // --------------------------------------------------------------------------- // integration // --------------------------------------------------------------------------- diff --git a/src/internal/operations/test/helper_test.go b/src/internal/operations/test/helper_test.go index 93a609365..52d860a2e 100644 --- a/src/internal/operations/test/helper_test.go +++ b/src/internal/operations/test/helper_test.go @@ -550,7 +550,7 @@ func ControllerWithSelector( t.FailNow() } - id, name, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, sel.DiscreteOwner, ins) + id, name, err := ctrl.PopulateProtectedResourceIDAndName(ctx, sel.DiscreteOwner, ins) if !assert.NoError(t, err, clues.ToCore(err)) { if onFail != nil { onFail(t, ctx) diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index 8a8abb33e..9874ed79b 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -329,7 +329,7 @@ func (r repository) NewBackupWithLookup( return operations.BackupOperation{}, clues.Wrap(err, "connecting to m365") } - ownerID, ownerName, err := ctrl.PopulateOwnerIDAndNamesFrom(ctx, sel.DiscreteOwner, ins) + ownerID, ownerName, err := ctrl.PopulateProtectedResourceIDAndName(ctx, sel.DiscreteOwner, ins) if err != nil { return operations.BackupOperation{}, clues.Wrap(err, "resolving resource owner details") }