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.
This commit is contained in:
ryanfkeepers 2023-07-19 13:03:39 -06:00
parent 1272066d50
commit 3a02e3269b
12 changed files with 155 additions and 17 deletions

View File

@ -144,7 +144,7 @@ func getControllerAndVerifyResourceOwner(
return nil, account.Account{}, nil, clues.Wrap(err, "connecting to graph api") 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 { if err != nil {
return nil, account.Account{}, nil, clues.Wrap(err, "verifying user") return nil, account.Account{}, nil, clues.Wrap(err, "verifying user")
} }

View File

@ -28,6 +28,10 @@ type is struct {
name string name string
} }
func NewProvider(id, name string) *is {
return &is{id, name}
}
func (is is) ID() string { return is.id } func (is is) ID() string { return is.id }
func (is is) Name() string { return is.name } func (is is) Name() string { return is.name }

View File

@ -367,7 +367,7 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() {
siteIDs = []string{siteID} 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)) require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewSharePointBackup(siteIDs) sel := selectors.NewSharePointBackup(siteIDs)
@ -414,7 +414,7 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
siteIDs = []string{siteID} 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)) require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewSharePointBackup(siteIDs) sel := selectors.NewSharePointBackup(siteIDs)

View File

@ -248,7 +248,7 @@ func (r resourceClient) getOwnerIDAndNameFrom(
return id, name, nil 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 // the owner's name and ID from that value. Returns an error if the owner is
// not recognized by the current tenant. // 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 // 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 // down for this func to consume instead of performing further queries. The
// data gets stored inside the controller instance for later re-use. // data gets stored inside the controller instance for later re-use.
func (ctrl *Controller) PopulateOwnerIDAndNamesFrom( func (ctrl *Controller) PopulateProtectedResourceIDAndName(
ctx context.Context, ctx context.Context,
owner string, // input value, can be either id or name owner string, // input value, can be either id or name
ins idname.Cacher, ins idname.Cacher,

View File

@ -222,7 +222,7 @@ func (suite *ControllerUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
ctrl := &Controller{ownerLookup: test.rc} 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)) test.expectErr(t, err, clues.ToCore(err))
assert.Equal(t, test.expectID, rID, "id") assert.Equal(t, test.expectID, rID, "id")
assert.Equal(t, test.expectName, rName, "name") assert.Equal(t, test.expectName, rName, "name")
@ -1295,7 +1295,7 @@ func (suite *ControllerIntegrationSuite) TestBackup_CreatesPrefixCollections() {
start = time.Now() 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)) require.NoError(t, err, clues.ToCore(err))
backupSel.SetDiscreteOwnerIDName(id, name) backupSel.SetDiscreteOwnerIDName(id, name)

View File

@ -26,6 +26,10 @@ type Controller struct {
Err error Err error
Stats data.CollectionStats Stats data.CollectionStats
ProtectedResourceID string
ProtectedResourceName string
ProtectedResourceErr error
} }
func (ctrl Controller) ProduceBackupCollections( func (ctrl Controller) ProduceBackupCollections(
@ -71,3 +75,13 @@ func (ctrl Controller) ConsumeRestoreCollections(
} }
func (ctrl Controller) CacheItemInfo(dii details.ItemInfo) {} 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
}

View File

@ -36,7 +36,7 @@ func ControllerWithSelector(
t.FailNow() 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 !assert.NoError(t, err, clues.ToCore(err)) {
if onFail != nil { if onFail != nil {
onFail() onFail()

View File

@ -48,6 +48,7 @@ type (
Wait() *data.CollectionStats Wait() *data.CollectionStats
CacheItemInfoer CacheItemInfoer
PopulateProtectedResourceIDAndNamer
} }
CacheItemInfoer interface { CacheItemInfoer interface {
@ -59,6 +60,25 @@ type (
CacheItemInfo(v details.ItemInfo) 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 { RepoMaintenancer interface {
RepoMaintenance(ctx context.Context, opts repository.Maintenance) error RepoMaintenance(ctx context.Context, opts repository.Maintenance) error
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/alcionai/corso/src/internal/common/crash" "github.com/alcionai/corso/src/internal/common/crash"
"github.com/alcionai/corso/src/internal/common/dttm" "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/data"
"github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/events" "github.com/alcionai/corso/src/internal/events"
@ -217,7 +218,19 @@ func (op *RestoreOperation) do(
return nil, clues.Wrap(err, "getting backup and details") 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( paths, err := formatDetailsForRestoration(
ctx, ctx,
@ -232,8 +245,6 @@ func (op *RestoreOperation) do(
ctx = clues.Add( ctx = clues.Add(
ctx, ctx,
"resource_owner_id", bup.Selector.ID(),
"resource_owner_name", clues.Hide(bup.Selector.Name()),
"details_entries", len(deets.Entries), "details_entries", len(deets.Entries),
"details_paths", len(paths), "details_paths", len(paths),
"backup_snapshot_id", bup.SnapshotID, "backup_snapshot_id", bup.SnapshotID,
@ -321,6 +332,24 @@ func (op *RestoreOperation) persistResults(
return op.Errors.Failure() 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 // Restorer funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -10,6 +10,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "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" inMock "github.com/alcionai/corso/src/internal/common/idname/mock"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/events" "github.com/alcionai/corso/src/internal/events"
@ -40,15 +42,15 @@ import (
// unit // unit
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type RestoreOpSuite struct { type RestoreOpUnitSuite struct {
tester.Suite tester.Suite
} }
func TestRestoreOpSuite(t *testing.T) { func TestRestoreOpUnitSuite(t *testing.T) {
suite.Run(t, &RestoreOpSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &RestoreOpUnitSuite{Suite: tester.NewUnitSuite(t)})
} }
func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { func (suite *RestoreOpUnitSuite) TestRestoreOperation_PersistResults() {
var ( var (
kw = &kopia.Wrapper{} kw = &kopia.Wrapper{}
sw = &store.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 // integration
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -550,7 +550,7 @@ func ControllerWithSelector(
t.FailNow() 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 !assert.NoError(t, err, clues.ToCore(err)) {
if onFail != nil { if onFail != nil {
onFail(t, ctx) onFail(t, ctx)

View File

@ -329,7 +329,7 @@ func (r repository) NewBackupWithLookup(
return operations.BackupOperation{}, clues.Wrap(err, "connecting to m365") 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 { if err != nil {
return operations.BackupOperation{}, clues.Wrap(err, "resolving resource owner details") return operations.BackupOperation{}, clues.Wrap(err, "resolving resource owner details")
} }