diff --git a/src/internal/kopia/base_finder.go b/src/internal/kopia/base_finder.go index 625ded643..9ac651512 100644 --- a/src/internal/kopia/base_finder.go +++ b/src/internal/kopia/base_finder.go @@ -249,6 +249,8 @@ func (b *baseFinder) findBasesInSet( // If we've made it to this point then we're considering the backup // complete as it has both an item data snapshot and a backup details // snapshot. + logger.Ctx(ictx).Infow("found complete backup", "base_backup_id", bup.ID) + me := ManifestEntry{ Manifest: man, Reasons: []Reason{reason}, @@ -293,11 +295,11 @@ func (b *baseFinder) getBase( return b.findBasesInSet(ctx, reason, metas) } -func (b *baseFinder) findBases( +func (b *baseFinder) FindBases( ctx context.Context, reasons []Reason, tags map[string]string, -) (backupBases, error) { +) BackupBases { var ( // All maps go from ID -> entry. We need to track by ID so we can coalesce // the reason for selecting something. Kopia assisted snapshots also use @@ -361,24 +363,13 @@ func (b *baseFinder) findBases( } } - return backupBases{ + res := &backupBases{ backups: maps.Values(baseBups), mergeBases: maps.Values(baseSnaps), assistBases: maps.Values(kopiaAssistSnaps), - }, nil -} - -func (b *baseFinder) FindBases( - ctx context.Context, - reasons []Reason, - tags map[string]string, -) ([]ManifestEntry, error) { - bb, err := b.findBases(ctx, reasons, tags) - if err != nil { - return nil, clues.Stack(err) } - // assistBases contains all snapshots so we can return it while maintaining - // almost all compatibility. - return bb.assistBases, nil + res.fixupAndVerify(ctx) + + return res } diff --git a/src/internal/kopia/base_finder_test.go b/src/internal/kopia/base_finder_test.go index c950b4f9d..f76b3c81a 100644 --- a/src/internal/kopia/base_finder_test.go +++ b/src/internal/kopia/base_finder_test.go @@ -5,11 +5,9 @@ import ( "testing" "time" - "github.com/alcionai/clues" "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/data" @@ -332,8 +330,7 @@ func (suite *BaseFinderUnitSuite) TestNoResult_NoBackupsOrSnapshots() { }, } - bb, err := bf.findBases(ctx, reasons, nil) - assert.NoError(t, err, "getting bases: %v", clues.ToCore(err)) + bb := bf.FindBases(ctx, reasons, nil) assert.Empty(t, bb.MergeBases()) assert.Empty(t, bb.AssistBases()) } @@ -356,8 +353,7 @@ func (suite *BaseFinderUnitSuite) TestNoResult_ErrorListingSnapshots() { }, } - bb, err := bf.findBases(ctx, reasons, nil) - assert.NoError(t, err, "getting bases: %v", clues.ToCore(err)) + bb := bf.FindBases(ctx, reasons, nil) assert.Empty(t, bb.MergeBases()) assert.Empty(t, bb.AssistBases()) } @@ -817,11 +813,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() { bg: &mockModelGetter{data: test.backupData}, } - bb, err := bf.findBases( + bb := bf.FindBases( ctx, test.input, nil) - require.NoError(t, err, "getting bases: %v", clues.ToCore(err)) checkBackupEntriesMatch( t, @@ -912,11 +907,10 @@ func (suite *BaseFinderUnitSuite) TestFindBases_CustomTags() { bg: &mockModelGetter{data: backupData}, } - bb, err := bf.findBases( + bb := bf.FindBases( ctx, testAllUsersAllCats, test.tags) - require.NoError(t, err, "getting bases: %v", clues.ToCore(err)) checkManifestEntriesMatch( t, diff --git a/src/internal/kopia/inject/inject.go b/src/internal/kopia/inject/inject.go index d97e06d31..6921c353d 100644 --- a/src/internal/kopia/inject/inject.go +++ b/src/internal/kopia/inject/inject.go @@ -39,6 +39,6 @@ type ( ctx context.Context, reasons []kopia.Reason, tags map[string]string, - ) ([]kopia.ManifestEntry, error) + ) kopia.BackupBases } ) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 4772f8b20..f3a3cbd55 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -6,6 +6,7 @@ import ( "github.com/alcionai/clues" "github.com/google/uuid" + "github.com/kopia/kopia/repo/manifest" "github.com/alcionai/corso/src/internal/common/crash" "github.com/alcionai/corso/src/internal/common/dttm" @@ -296,20 +297,10 @@ func (op *BackupOperation) do( return nil, clues.Stack(err) } - type baseFinder struct { - kinject.BaseFinder - kinject.RestoreProducer - } - - bf := baseFinder{ - BaseFinder: kbf, - RestoreProducer: op.kopia, - } - mans, mdColls, canUseMetaData, err := produceManifestsAndMetadata( ctx, - bf, - op.store, + kbf, + op.kopia, reasons, fallbackReasons, op.account.ID(), op.incremental) @@ -318,10 +309,7 @@ func (op *BackupOperation) do( } if canUseMetaData { - _, lastBackupVersion, err = lastCompleteBackups(ctx, op.store, mans) - if err != nil { - return nil, clues.Wrap(err, "retrieving prior backups") - } + lastBackupVersion = mans.MinBackupVersion() } cs, ssmb, canUsePreviousBackup, err := produceBackupDataCollections( @@ -358,9 +346,8 @@ func (op *BackupOperation) do( err = mergeDetails( ctx, - op.store, detailsStore, - mans, + mans.Backups(), toMerge, deets, writeStats, @@ -482,7 +469,7 @@ func consumeBackupCollections( bc kinject.BackupConsumer, tenantID string, reasons []kopia.Reason, - mans []kopia.ManifestEntry, + bbs kopia.BackupBases, cs []data.BackupCollection, pmr prefixmatcher.StringSetReader, backupID model.StableID, @@ -506,9 +493,24 @@ func consumeBackupCollections( } } - bases := make([]kopia.IncrementalBase, 0, len(mans)) + // AssistBases should be the upper bound for how many snapshots we pass in. + bases := make([]kopia.IncrementalBase, 0, len(bbs.AssistBases())) + // Track IDs we've seen already so we don't accidentally duplicate some + // manifests. This can be removed when we move the code below into the kopia + // package. + ids := map[manifest.ID]struct{}{} - for _, m := range mans { + var mb []kopia.ManifestEntry + + if bbs != nil { + mb = bbs.MergeBases() + } + + // TODO(ashmrtn): Make a wrapper for Reson that allows adding a tenant and + // make a function that will spit out a prefix that includes the tenant. With + // that done this code can be moved to kopia wrapper since it's really more + // specific to that. + for _, m := range mb { paths := make([]*path.Builder, 0, len(m.Reasons)) services := map[string]struct{}{} categories := map[string]struct{}{} @@ -524,6 +526,8 @@ func consumeBackupCollections( categories[reason.Category.String()] = struct{}{} } + ids[m.ID] = struct{}{} + bases = append(bases, kopia.IncrementalBase{ Manifest: m.Manifest, SubtreePaths: paths, @@ -552,6 +556,18 @@ func consumeBackupCollections( "base_backup_id", mbID) } + // At the moment kopia assisted snapshots are in the same set as merge bases. + // When we fixup generating subtree paths we can remove this. + if bbs != nil { + for _, ab := range bbs.AssistBases() { + if _, ok := ids[ab.ID]; ok { + continue + } + + bases = append(bases, kopia.IncrementalBase{Manifest: ab.Manifest}) + } + } + kopiaStats, deets, itemsSourcedFromBase, err := bc.ConsumeBackupCollections( ctx, bases, @@ -663,61 +679,10 @@ func getNewPathRefs( return newPath, newLoc, updated, nil } -func lastCompleteBackups( - ctx context.Context, - ms *store.Wrapper, - mans []kopia.ManifestEntry, -) (map[string]*backup.Backup, int, error) { - var ( - oldestVersion = version.NoBackup - result = map[string]*backup.Backup{} - ) - - if len(mans) == 0 { - return result, -1, nil - } - - for _, man := range mans { - // For now skip snapshots that aren't complete. We will need to revisit this - // when we tackle restartability. - if len(man.IncompleteReason) > 0 { - continue - } - - var ( - mctx = clues.Add(ctx, "base_manifest_id", man.ID) - reasons = man.Reasons - ) - - bID, ok := man.GetTag(kopia.TagBackupID) - if !ok { - return result, oldestVersion, clues.New("no backup ID in snapshot manifest").WithClues(mctx) - } - - mctx = clues.Add(mctx, "base_manifest_backup_id", bID) - - bup, err := getBackupFromID(mctx, model.StableID(bID), ms) - if err != nil { - return result, oldestVersion, err - } - - for _, r := range reasons { - result[r.Key()] = bup - } - - if oldestVersion == -1 || bup.Version < oldestVersion { - oldestVersion = bup.Version - } - } - - return result, oldestVersion, nil -} - func mergeDetails( ctx context.Context, - ms *store.Wrapper, detailsStore streamstore.Streamer, - mans []kopia.ManifestEntry, + backups []kopia.BackupEntry, dataFromBackup kopia.DetailsMergeInfoer, deets *details.Builder, writeStats *kopia.BackupStats, @@ -738,29 +703,15 @@ func mergeDetails( var addedEntries int - for _, man := range mans { + for _, baseBackup := range backups { var ( - mctx = clues.Add(ctx, "base_manifest_id", man.ID) + mctx = clues.Add(ctx, "base_backup_id", baseBackup.ID) manifestAddedEntries int ) - // For now skip snapshots that aren't complete. We will need to revisit this - // when we tackle restartability. - if len(man.IncompleteReason) > 0 { - continue - } - - bID, ok := man.GetTag(kopia.TagBackupID) - if !ok { - return clues.New("no backup ID in snapshot manifest").WithClues(mctx) - } - - mctx = clues.Add(mctx, "base_manifest_backup_id", bID) - - baseBackup, baseDeets, err := getBackupAndDetailsFromID( + baseDeets, err := getDetailsFromBackup( mctx, - model.StableID(bID), - ms, + baseBackup.Backup, detailsStore, errs) if err != nil { @@ -781,7 +732,7 @@ func mergeDetails( // // TODO(ashmrtn): This logic will need expanded to cover entries from // checkpoints if we start doing kopia-assisted incrementals for those. - if !matchesReason(man.Reasons, rr) { + if !matchesReason(baseBackup.Reasons, rr) { continue } diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index df57dea03..771c77122 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -232,10 +232,8 @@ func checkBackupIsInManifests( bf, err := kw.NewBaseFinder(bo.store) require.NoError(t, err, clues.ToCore(err)) - mans, err := bf.FindBases(ctx, reasons, tags) - require.NoError(t, err, clues.ToCore(err)) - - for _, man := range mans { + mans := bf.FindBases(ctx, reasons, tags) + for _, man := range mans.MergeBases() { bID, ok := man.GetTag(kopia.TagBackupID) if !assert.Truef(t, ok, "snapshot manifest %s missing backup ID tag", man.ID) { continue diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index 248d40087..c8ff42f9d 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -2,13 +2,11 @@ package operations import ( "context" - "fmt" stdpath "path" "testing" "time" "github.com/alcionai/clues" - "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -128,77 +126,6 @@ func (mbu mockBackupConsumer) ConsumeBackupCollections( // ----- model store for backups -type mockBackupStorer struct { - // Only using this to store backup models right now. - entries map[model.StableID]backup.Backup -} - -func (mbs mockBackupStorer) Get( - ctx context.Context, - s model.Schema, - id model.StableID, - toPopulate model.Model, -) error { - ctx = clues.Add( - ctx, - "model_schema", s, - "model_id", id, - "model_type", fmt.Sprintf("%T", toPopulate)) - - if s != model.BackupSchema { - return clues.New("unexpected schema").WithClues(ctx) - } - - r, ok := mbs.entries[id] - if !ok { - return clues.New("model not found").WithClues(ctx) - } - - bu, ok := toPopulate.(*backup.Backup) - if !ok { - return clues.New("bad population type").WithClues(ctx) - } - - *bu = r - - return nil -} - -func (mbs mockBackupStorer) Delete(context.Context, model.Schema, model.StableID) error { - return clues.New("not implemented") -} - -func (mbs mockBackupStorer) DeleteWithModelStoreID(context.Context, manifest.ID) error { - return clues.New("not implemented") -} - -func (mbs mockBackupStorer) GetIDsForType( - context.Context, - model.Schema, - map[string]string, -) ([]*model.BaseModel, error) { - return nil, clues.New("not implemented") -} - -func (mbs mockBackupStorer) GetWithModelStoreID( - context.Context, - model.Schema, - manifest.ID, - model.Model, -) error { - return clues.New("not implemented") -} - -func (mbs mockBackupStorer) Put(context.Context, model.Schema, model.Model) error { - return clues.New("not implemented") -} - -func (mbs mockBackupStorer) Update(context.Context, model.Schema, model.Model) error { - return clues.New("not implemented") -} - -// ----- model store for backups - type mockDetailsMergeInfoer struct { repoRefs map[string]path.Path locs map[string]*path.Builder @@ -260,27 +187,6 @@ func makeMetadataBasePath( return p } -func makeMetadataPath( - t *testing.T, - tenant string, - service path.ServiceType, - resourceOwner string, - category path.CategoryType, - fileName string, -) path.Path { - t.Helper() - - p, err := path.Builder{}.Append(fileName).ToServiceCategoryMetadataPath( - tenant, - resourceOwner, - service, - category, - true) - require.NoError(t, err, clues.ToCore(err)) - - return p -} - func makeFolderEntry( t *testing.T, pb, loc *path.Builder, @@ -379,25 +285,6 @@ func makeDetailsEntry( return res } -// TODO(ashmrtn): This should belong to some code that lives in the kopia -// package that is only compiled when running tests. -func makeKopiaTagKey(k string) string { - return "tag:" + k -} - -func makeManifest(t *testing.T, backupID model.StableID, incompleteReason string) *snapshot.Manifest { - t.Helper() - - tagKey := makeKopiaTagKey(kopia.TagBackupID) - - return &snapshot.Manifest{ - Tags: map[string]string{ - tagKey: string(backupID), - }, - IncompleteReason: incompleteReason, - } -} - // --------------------------------------------------------------------------- // unit tests // --------------------------------------------------------------------------- @@ -532,20 +419,20 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections ) table := []struct { - name string - inputMan []kopia.ManifestEntry + name string + // Backup model is untouched in this test so there's no need to populate it. + input kopia.BackupBases expected []kopia.IncrementalBase }{ { name: "SingleManifestSingleReason", - inputMan: []kopia.ManifestEntry{ - { + input: kopia.NewMockBackupBases().WithMergeBases( + kopia.ManifestEntry{ Manifest: manifest1, Reasons: []kopia.Reason{ emailReason, }, - }, - }, + }).ClearMockAssistBases(), expected: []kopia.IncrementalBase{ { Manifest: manifest1, @@ -557,15 +444,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections }, { name: "SingleManifestMultipleReasons", - inputMan: []kopia.ManifestEntry{ - { + input: kopia.NewMockBackupBases().WithMergeBases( + kopia.ManifestEntry{ Manifest: manifest1, Reasons: []kopia.Reason{ emailReason, contactsReason, }, - }, - }, + }).ClearMockAssistBases(), expected: []kopia.IncrementalBase{ { Manifest: manifest1, @@ -578,22 +464,21 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections }, { name: "MultipleManifestsMultipleReasons", - inputMan: []kopia.ManifestEntry{ - { + input: kopia.NewMockBackupBases().WithMergeBases( + kopia.ManifestEntry{ Manifest: manifest1, Reasons: []kopia.Reason{ emailReason, contactsReason, }, }, - { + kopia.ManifestEntry{ Manifest: manifest2, Reasons: []kopia.Reason{ emailReason, contactsReason, }, - }, - }, + }).ClearMockAssistBases(), expected: []kopia.IncrementalBase{ { Manifest: manifest1, @@ -611,6 +496,33 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections }, }, }, + { + name: "Single Manifest Single Reason With Assist Base", + input: kopia.NewMockBackupBases().WithMergeBases( + kopia.ManifestEntry{ + Manifest: manifest1, + Reasons: []kopia.Reason{ + emailReason, + }, + }).WithAssistBases( + kopia.ManifestEntry{ + Manifest: manifest2, + Reasons: []kopia.Reason{ + contactsReason, + }, + }), + expected: []kopia.IncrementalBase{ + { + Manifest: manifest1, + SubtreePaths: []*path.Builder{ + emailBuilder, + }, + }, + { + Manifest: manifest2, + }, + }, + }, } for _, test := range table { @@ -637,7 +549,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections mbu, tenant, nil, - test.inputMan, + test.input, nil, nil, model.StableID(""), @@ -731,9 +643,8 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems table := []struct { name string - populatedModels map[model.StableID]backup.Backup populatedDetails map[string]*details.Details - inputMans []kopia.ManifestEntry + inputBackups []kopia.BackupEntry mdm *mockDetailsMergeInfoer errCheck assert.ErrorAssertionFunc @@ -752,24 +663,6 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems // Use empty slice so we don't error out on nil != empty. expectedEntries: []*details.Entry{}, }, - { - name: "BackupIDNotFound", - mdm: func() *mockDetailsMergeInfoer { - res := newMockDetailsMergeInfoer() - res.add(itemPath1, itemPath1, locationPath1) - - return res - }(), - inputMans: []kopia.ManifestEntry{ - { - Manifest: makeManifest(suite.T(), "foo", ""), - Reasons: []kopia.Reason{ - pathReason1, - }, - }, - }, - errCheck: assert.Error, - }, { name: "DetailsIDNotFound", mdm: func() *mockDetailsMergeInfoer { @@ -778,22 +671,19 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup.Backup{ + BaseModel: model.BaseModel{ + ID: backup1.ID, + }, + DetailsID: "foo", + }, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: { - BaseModel: model.BaseModel{ - ID: backup1.ID, - }, - DetailsID: "foo", - }, - }, errCheck: assert.Error, }, { @@ -805,17 +695,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -835,23 +722,20 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -871,17 +755,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -933,17 +814,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -963,17 +841,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -996,17 +871,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -1029,17 +901,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -1063,17 +932,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -1097,24 +963,20 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems return res }(), - inputMans: []kopia.ManifestEntry{ + inputBackups: []kopia.BackupEntry{ { - Manifest: makeManifest(suite.T(), backup1.ID, ""), + Backup: &backup1, Reasons: []kopia.Reason{ pathReason1, }, }, { - Manifest: makeManifest(suite.T(), backup2.ID, ""), + Backup: &backup2, Reasons: []kopia.Reason{ pathReason3, }, }, }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - backup2.ID: backup2, - }, populatedDetails: map[string]*details.Details{ backup1.DetailsID: { DetailsModel: details.DetailsModel{ @@ -1140,54 +1002,6 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems makeDetailsEntry(suite.T(), itemPath3, locationPath3, 37, false), }, }, - { - name: "SomeBasesIncomplete", - mdm: func() *mockDetailsMergeInfoer { - res := newMockDetailsMergeInfoer() - res.add(itemPath1, itemPath1, locationPath1) - - return res - }(), - inputMans: []kopia.ManifestEntry{ - { - Manifest: makeManifest(suite.T(), backup1.ID, ""), - Reasons: []kopia.Reason{ - pathReason1, - }, - }, - { - Manifest: makeManifest(suite.T(), backup2.ID, "checkpoint"), - Reasons: []kopia.Reason{ - pathReason1, - }, - }, - }, - populatedModels: map[model.StableID]backup.Backup{ - backup1.ID: backup1, - backup2.ID: backup2, - }, - populatedDetails: map[string]*details.Details{ - backup1.DetailsID: { - DetailsModel: details.DetailsModel{ - Entries: []details.Entry{ - *makeDetailsEntry(suite.T(), itemPath1, locationPath1, 42, false), - }, - }, - }, - backup2.DetailsID: { - DetailsModel: details.DetailsModel{ - Entries: []details.Entry{ - // This entry should not be picked due to being incomplete. - *makeDetailsEntry(suite.T(), itemPath1, locationPath1, 84, false), - }, - }, - }, - }, - errCheck: assert.NoError, - expectedEntries: []*details.Entry{ - makeDetailsEntry(suite.T(), itemPath1, locationPath1, 42, false), - }, - }, } for _, test := range table { @@ -1198,15 +1012,13 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems defer flush() mds := ssmock.Streamer{Deets: test.populatedDetails} - w := &store.Wrapper{Storer: mockBackupStorer{entries: test.populatedModels}} deets := details.Builder{} writeStats := kopia.BackupStats{} err := mergeDetails( ctx, - w, mds, - test.inputMans, + test.inputBackups, test.mdm, &deets, &writeStats, @@ -1247,30 +1059,22 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsFolde locPath1 = path.Builder{}.Append(itemPath1.Folders()...) - backup1 = backup.Backup{ - BaseModel: model.BaseModel{ - ID: "bid1", - }, - DetailsID: "did1", - } - pathReason1 = kopia.Reason{ ResourceOwner: itemPath1.ResourceOwner(), Service: itemPath1.Service(), Category: itemPath1.Category(), } - inputMans = []kopia.ManifestEntry{ - { - Manifest: makeManifest(t, backup1.ID, ""), - Reasons: []kopia.Reason{ - pathReason1, + backup1 = kopia.BackupEntry{ + Backup: &backup.Backup{ + BaseModel: model.BaseModel{ + ID: "bid1", }, + DetailsID: "did1", + }, + Reasons: []kopia.Reason{ + pathReason1, }, - } - - populatedModels = map[model.StableID]backup.Backup{ - backup1.ID: backup1, } itemSize = 42 @@ -1313,16 +1117,14 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsFolde var ( mds = ssmock.Streamer{Deets: populatedDetails} - w = &store.Wrapper{Storer: mockBackupStorer{entries: populatedModels}} deets = details.Builder{} writeStats = kopia.BackupStats{} ) err := mergeDetails( ctx, - w, mds, - inputMans, + []kopia.BackupEntry{backup1}, mdm, &deets, &writeStats, diff --git a/src/internal/operations/common.go b/src/internal/operations/common.go index 70c53d2cb..57a40d2de 100644 --- a/src/internal/operations/common.go +++ b/src/internal/operations/common.go @@ -13,19 +13,6 @@ import ( "github.com/alcionai/corso/src/pkg/store" ) -func getBackupFromID( - ctx context.Context, - backupID model.StableID, - ms *store.Wrapper, -) (*backup.Backup, error) { - bup, err := ms.GetBackup(ctx, backupID) - if err != nil { - return nil, clues.Wrap(err, "getting backup") - } - - return bup, nil -} - func getBackupAndDetailsFromID( ctx context.Context, backupID model.StableID, @@ -38,6 +25,20 @@ func getBackupAndDetailsFromID( return nil, nil, clues.Wrap(err, "getting backup") } + deets, err := getDetailsFromBackup(ctx, bup, detailsStore, errs) + if err != nil { + return nil, nil, clues.Stack(err) + } + + return bup, deets, nil +} + +func getDetailsFromBackup( + ctx context.Context, + bup *backup.Backup, + detailsStore streamstore.Reader, + errs *fault.Bus, +) (*details.Details, error) { var ( deets details.Details umt = streamstore.DetailsReader(details.UnmarshalTo(&deets)) @@ -49,12 +50,12 @@ func getBackupAndDetailsFromID( } if len(ssid) == 0 { - return bup, nil, clues.New("no details or errors in backup").WithClues(ctx) + return nil, clues.New("no details or errors in backup").WithClues(ctx) } if err := detailsStore.Read(ctx, ssid, umt, errs); err != nil { - return nil, nil, clues.Wrap(err, "reading backup data from streamstore") + return nil, clues.Wrap(err, "reading backup data from streamstore") } - return bup, &deets, nil + return &deets, nil } diff --git a/src/internal/operations/manifests.go b/src/internal/operations/manifests.go index 121481066..5e1c79e4f 100644 --- a/src/internal/operations/manifests.go +++ b/src/internal/operations/manifests.go @@ -4,74 +4,39 @@ import ( "context" "github.com/alcionai/clues" - "github.com/kopia/kopia/repo/manifest" "github.com/pkg/errors" - "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/kopia" "github.com/alcionai/corso/src/internal/kopia/inject" "github.com/alcionai/corso/src/internal/m365/graph" - "github.com/alcionai/corso/src/internal/model" - "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" ) -type manifestRestorer interface { - inject.BaseFinder - inject.RestoreProducer -} - -type getBackuper interface { - GetBackup( - ctx context.Context, - backupID model.StableID, - ) (*backup.Backup, error) -} - // calls kopia to retrieve prior backup manifests, metadata collections to supply backup heuristics. +// TODO(ashmrtn): Make this a helper function that always returns as much as +// possible and call in another function that drops metadata and/or +// kopia-assisted incremental bases based on flag values. func produceManifestsAndMetadata( ctx context.Context, - mr manifestRestorer, - gb getBackuper, + bf inject.BaseFinder, + rp inject.RestoreProducer, reasons, fallbackReasons []kopia.Reason, tenantID string, getMetadata bool, -) ([]kopia.ManifestEntry, []data.RestoreCollection, bool, error) { +) (kopia.BackupBases, []data.RestoreCollection, bool, error) { var ( tags = map[string]string{kopia.TagBackupCategory: ""} metadataFiles = graph.AllMetadataFileNames() collections []data.RestoreCollection ) - ms, err := mr.FindBases(ctx, reasons, tags) - if err != nil { - return nil, nil, false, clues.Wrap(err, "looking up prior snapshots") - } - - // We only need to check that we have 1:1 reason:base if we're doing an - // incremental with associated metadata. This ensures that we're only sourcing - // data from a single Point-In-Time (base) for each incremental backup. - // - // TODO(ashmrtn): This may need updating if we start sourcing item backup - // details from previous snapshots when using kopia-assisted incrementals. - if err := verifyDistinctBases(ctx, ms); err != nil { - logger.CtxErr(ctx, err).Info("base snapshot collision, falling back to full backup") - return ms, nil, false, nil - } - - fbms, err := mr.FindBases(ctx, fallbackReasons, tags) - if err != nil { - return nil, nil, false, clues.Wrap(err, "looking up prior snapshots under alternate id") - } - - // Also check distinct bases for the fallback set. - if err := verifyDistinctBases(ctx, fbms); err != nil { - logger.CtxErr(ctx, err).Info("fallback snapshot collision, falling back to full backup") - return ms, nil, false, nil - } + bb := bf.FindBases(ctx, reasons, tags) + // TODO(ashmrtn): Only fetch these if we haven't already covered all the + // reasons for this backup. + fbb := bf.FindBases(ctx, fallbackReasons, tags) // one of three cases can occur when retrieving backups across reason migrations: // 1. the current reasons don't match any manifests, and we use the fallback to @@ -79,56 +44,26 @@ func produceManifestsAndMetadata( // 2. the current reasons only contain an incomplete manifest, and the fallback // can find a complete manifest. // 3. the current reasons contain all the necessary manifests. - ms = unionManifests(reasons, ms, fbms) + bb = bb.MergeBackupBases( + ctx, + fbb, + func(r kopia.Reason) string { + return r.Service.String() + r.Category.String() + }) if !getMetadata { - return ms, nil, false, nil + logger.Ctx(ctx).Debug("full backup requested, dropping merge bases") + + // TODO(ashmrtn): If this function is moved to be a helper function then + // move this change to the bases to the caller of this function. + bb.ClearMergeBases() + + return bb, nil, false, nil } - for _, man := range ms { - if len(man.IncompleteReason) > 0 { - continue - } - + for _, man := range bb.MergeBases() { mctx := clues.Add(ctx, "manifest_id", man.ID) - bID, ok := man.GetTag(kopia.TagBackupID) - if !ok { - err = clues.New("snapshot manifest missing backup ID").WithClues(ctx) - return nil, nil, false, err - } - - mctx = clues.Add(mctx, "manifest_backup_id", bID) - - bup, err := gb.GetBackup(mctx, model.StableID(bID)) - // if no backup exists for any of the complete manifests, we want - // to fall back to a complete backup. - if errors.Is(err, data.ErrNotFound) { - logger.Ctx(mctx).Infow("backup missing, falling back to full backup", clues.In(mctx).Slice()...) - return ms, nil, false, nil - } - - if err != nil { - return nil, nil, false, clues.Wrap(err, "retrieving prior backup data") - } - - ssid := bup.StreamStoreID - if len(ssid) == 0 { - ssid = bup.DetailsID - } - - mctx = clues.Add(mctx, "manifest_streamstore_id", ssid) - - // if no detailsID exists for any of the complete manifests, we want - // to fall back to a complete backup. This is a temporary prevention - // mechanism to keep backups from falling into a perpetually bad state. - // This makes an assumption that the ID points to a populated set of - // details; we aren't doing the work to look them up. - if len(ssid) == 0 { - logger.Ctx(ctx).Infow("backup missing streamstore ID, falling back to full backup", clues.In(mctx).Slice()...) - return ms, nil, false, nil - } - // a local fault.Bus intance is used to collect metadata files here. // we avoid the global fault.Bus because all failures here are ignorable, // and cascading errors up to the operation can cause a conflict that forces @@ -137,9 +72,19 @@ func produceManifestsAndMetadata( // spread around. Need to find more idiomatic handling. fb := fault.New(true) - colls, err := collectMetadata(mctx, mr, man, metadataFiles, tenantID, fb) + colls, err := collectMetadata(mctx, rp, man, metadataFiles, tenantID, fb) LogFaultErrors(ctx, fb.Errors(), "collecting metadata") + // TODO(ashmrtn): It should be alright to relax this condition a little. We + // should be able to just remove the offending manifest and backup from the + // set of bases. Since we're looking at manifests in this loop, it should be + // possible to find the backup by either checking the reasons or extracting + // the backup ID from the manifests tags. + // + // Assuming that only the corso metadata is corrupted for the manifest, it + // should be safe to leave this manifest in the AssistBases set, though we + // could remove it there too if we want to be conservative. That can be done + // by finding the manifest ID. if err != nil && !errors.Is(err, data.ErrNotFound) { // prior metadata isn't guaranteed to exist. // if it doesn't, we'll just have to do a @@ -150,148 +95,7 @@ func produceManifestsAndMetadata( collections = append(collections, colls...) } - if err != nil { - return nil, nil, false, err - } - - return ms, collections, true, nil -} - -// unionManifests reduces the two manifest slices into a single slice. -// Assumes fallback represents a prior manifest version (across some migration -// that disrupts manifest lookup), and that mans contains the current version. -// Also assumes the mans slice will have, at most, one complete and one incomplete -// manifest per service+category tuple. -// -// Selection priority, for each reason, follows these rules: -// 1. If the mans manifest is complete, ignore fallback manifests for that reason. -// 2. If the mans manifest is only incomplete, look for a matching complete manifest in fallbacks. -// 3. If mans has no entry for a reason, look for both complete and incomplete fallbacks. -func unionManifests( - reasons []kopia.Reason, - mans []kopia.ManifestEntry, - fallback []kopia.ManifestEntry, -) []kopia.ManifestEntry { - if len(fallback) == 0 { - return mans - } - - if len(mans) == 0 { - return fallback - } - - type manTup struct { - complete *kopia.ManifestEntry - incomplete *kopia.ManifestEntry - } - - tups := map[string]manTup{} - - for _, r := range reasons { - // no resource owner in the key. Assume it's the same owner across all - // manifests, but that the identifier is different due to migration. - k := r.Service.String() + r.Category.String() - tups[k] = manTup{} - } - - // track the manifests that were collected with the current lookup - for i := range mans { - m := &mans[i] - - for _, r := range m.Reasons { - k := r.Service.String() + r.Category.String() - t := tups[k] - // assume mans will have, at most, one complete and one incomplete per key - if len(m.IncompleteReason) > 0 { - t.incomplete = m - } else { - t.complete = m - } - - tups[k] = t - } - } - - // backfill from the fallback where necessary - for i := range fallback { - m := &fallback[i] - useReasons := []kopia.Reason{} - - for _, r := range m.Reasons { - k := r.Service.String() + r.Category.String() - t := tups[k] - - if t.complete != nil { - // assume fallbacks contains prior manifest versions. - // we don't want to stack a prior version incomplete onto - // a current version's complete snapshot. - continue - } - - useReasons = append(useReasons, r) - - if len(m.IncompleteReason) > 0 && t.incomplete == nil { - t.incomplete = m - } else if len(m.IncompleteReason) == 0 { - t.complete = m - } - - tups[k] = t - } - - if len(m.IncompleteReason) == 0 && len(useReasons) > 0 { - m.Reasons = useReasons - } - } - - // collect the results into a single slice of manifests - ms := map[string]kopia.ManifestEntry{} - - for _, m := range tups { - if m.complete != nil { - ms[string(m.complete.ID)] = *m.complete - } - - if m.incomplete != nil { - ms[string(m.incomplete.ID)] = *m.incomplete - } - } - - return maps.Values(ms) -} - -// verifyDistinctBases is a validation checker that ensures, for a given slice -// of manifests, that each manifest's Reason (owner, service, category) is only -// included once. If a reason is duplicated by any two manifests, an error is -// returned. -func verifyDistinctBases(ctx context.Context, mans []kopia.ManifestEntry) error { - reasons := map[string]manifest.ID{} - - for _, man := range mans { - // Incomplete snapshots are used only for kopia-assisted incrementals. The - // fact that we need this check here makes it seem like this should live in - // the kopia code. However, keeping it here allows for better debugging as - // the kopia code only has access to a path builder which means it cannot - // remove the resource owner from the error/log output. That is also below - // the point where we decide if we should do a full backup or an incremental. - if len(man.IncompleteReason) > 0 { - continue - } - - for _, reason := range man.Reasons { - reasonKey := reason.ResourceOwner + reason.Service.String() + reason.Category.String() - - if b, ok := reasons[reasonKey]; ok { - return clues.New("manifests have overlapping reasons"). - WithClues(ctx). - With("other_manifest_id", b) - } - - reasons[reasonKey] = man.ID - } - } - - return nil + return bb, collections, true, nil } // collectMetadata retrieves all metadata files associated with the manifest. diff --git a/src/internal/operations/manifests_test.go b/src/internal/operations/manifests_test.go index dd477ee50..e4ae9b6d3 100644 --- a/src/internal/operations/manifests_test.go +++ b/src/internal/operations/manifests_test.go @@ -8,9 +8,7 @@ import ( "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/kopia" @@ -25,49 +23,6 @@ import ( // interfaces // --------------------------------------------------------------------------- -type mockManifestRestorer struct { - mockRestoreProducer - mans []kopia.ManifestEntry - mrErr error // err varname already claimed by mockRestoreProducer -} - -func (mmr mockManifestRestorer) FindBases( - ctx context.Context, - reasons []kopia.Reason, - tags map[string]string, -) ([]kopia.ManifestEntry, error) { - mans := map[string]kopia.ManifestEntry{} - - for _, r := range reasons { - for _, m := range mmr.mans { - for _, mr := range m.Reasons { - if mr.ResourceOwner == r.ResourceOwner { - mans[string(m.ID)] = m - break - } - } - } - } - - return maps.Values(mans), mmr.mrErr -} - -type mockGetBackuper struct { - detailsID string - streamstoreID string - err error -} - -func (mg mockGetBackuper) GetBackup( - ctx context.Context, - backupID model.StableID, -) (*backup.Backup, error) { - return &backup.Backup{ - DetailsID: mg.detailsID, - StreamStoreID: mg.streamstoreID, - }, mg.err -} - type mockColl struct { id string // for comparisons p path.Path @@ -81,6 +36,36 @@ func (mc mockColl) FullPath() path.Path { return mc.p } +type mockBackupFinder struct { + // ResourceOwner -> returned set of data for call to FindBases. We can just + // switch on the ResourceOwner as the passed in Reasons should be the same + // beyond that and results are returned for the union of the reasons anyway. + // This does assume that the return data is properly constructed to return a + // union of the reasons etc. + data map[string]kopia.BackupBases +} + +func (bf *mockBackupFinder) FindBases( + _ context.Context, + reasons []kopia.Reason, + _ map[string]string, +) kopia.BackupBases { + if len(reasons) == 0 { + return kopia.NewMockBackupBases() + } + + if bf == nil { + return kopia.NewMockBackupBases() + } + + b := bf.data[reasons[0].ResourceOwner] + if b == nil { + return kopia.NewMockBackupBases() + } + + return b +} + // --------------------------------------------------------------------------- // tests // --------------------------------------------------------------------------- @@ -254,169 +239,24 @@ func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() { } } -func (suite *OperationsManifestsUnitSuite) TestVerifyDistinctBases() { - ro := "resource_owner" +func buildReasons( + ro string, + service path.ServiceType, + cats ...path.CategoryType, +) []kopia.Reason { + var reasons []kopia.Reason - table := []struct { - name string - mans []kopia.ManifestEntry - expect assert.ErrorAssertionFunc - }{ - { - name: "one manifest, one reason", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - expect: assert.NoError, - }, - { - name: "one incomplete manifest", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{IncompleteReason: "ir"}, - }, - }, - expect: assert.NoError, - }, - { - name: "one manifest, multiple reasons", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.ContactsCategory, - }, - }, - }, - }, - expect: assert.NoError, - }, - { - name: "one manifest, duplicate reasons", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - expect: assert.Error, - }, - { - name: "two manifests, non-overlapping reasons", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.ContactsCategory, - }, - }, - }, - }, - expect: assert.NoError, - }, - { - name: "two manifests, overlapping reasons", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - expect: assert.Error, - }, - { - name: "two manifests, overlapping reasons, one snapshot incomplete", - mans: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{IncompleteReason: "ir"}, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - expect: assert.NoError, - }, + for _, cat := range cats { + reasons = append( + reasons, + kopia.Reason{ + ResourceOwner: ro, + Service: service, + Category: cat, + }) } - for _, test := range table { - suite.Run(test.name, func() { - ctx, flush := tester.NewContext(suite.T()) - defer flush() - err := verifyDistinctBases(ctx, test.mans) - test.expect(suite.T(), err, clues.ToCore(err)) - }) - } + return reasons } func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { @@ -426,228 +266,235 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { did = "detailsid" ) - makeMan := func(pct path.CategoryType, id, incmpl, bid string) kopia.ManifestEntry { - tags := map[string]string{} - if len(bid) > 0 { - tags = map[string]string{"tag:" + kopia.TagBackupID: bid} - } - + makeMan := func(id, incmpl string, cats ...path.CategoryType) kopia.ManifestEntry { return kopia.ManifestEntry{ Manifest: &snapshot.Manifest{ ID: manifest.ID(id), IncompleteReason: incmpl, - Tags: tags, - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: ro, - Service: path.ExchangeService, - Category: pct, - }, }, + Reasons: buildReasons(ro, path.ExchangeService, cats...), } } table := []struct { - name string - mr mockManifestRestorer - gb mockGetBackuper - getMeta bool - assertErr assert.ErrorAssertionFunc - assertB assert.BoolAssertionFunc - expectDCS []mockColl - expectNilMans bool + name string + bf *mockBackupFinder + rp mockRestoreProducer + reasons []kopia.Reason + getMeta bool + assertErr assert.ErrorAssertionFunc + assertB assert.BoolAssertionFunc + expectDCS []mockColl + expectPaths func(t *testing.T, gotPaths []path.Path) + expectMans kopia.BackupBases }{ { - name: "don't get metadata, no mans", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{}, - }, - gb: mockGetBackuper{detailsID: did}, - getMeta: false, - assertErr: assert.NoError, - assertB: assert.False, - expectDCS: nil, + name: "don't get metadata, no mans", + rp: mockRestoreProducer{}, + reasons: []kopia.Reason{}, + getMeta: false, + assertErr: assert.NoError, + assertB: assert.False, + expectDCS: nil, + expectMans: kopia.NewMockBackupBases(), }, { name: "don't get metadata", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{makeMan(path.EmailCategory, "id1", "", "")}, + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{}, + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, }, - gb: mockGetBackuper{detailsID: did}, getMeta: false, assertErr: assert.NoError, assertB: assert.False, expectDCS: nil, + expectMans: kopia.NewMockBackupBases().WithAssistBases( + makeMan("id1", "", path.EmailCategory), + ), }, { name: "don't get metadata, incomplete manifest", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{makeMan(path.EmailCategory, "id1", "ir", "")}, - }, - gb: mockGetBackuper{detailsID: did}, - getMeta: false, - assertErr: assert.NoError, - assertB: assert.False, - expectDCS: nil, - }, - { - name: "fetch manifests errors", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mrErr: assert.AnError, - }, - gb: mockGetBackuper{detailsID: did}, - getMeta: true, - assertErr: assert.Error, - assertB: assert.False, - expectDCS: nil, - }, - { - name: "verify distinct bases fails", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{ - makeMan(path.EmailCategory, "id1", "", ""), - makeMan(path.EmailCategory, "id2", "", ""), + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithAssistBases( + makeMan("id1", "checkpoint", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{}, + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, }, }, - gb: mockGetBackuper{detailsID: did}, getMeta: true, - assertErr: assert.NoError, // No error, even though verify failed. - assertB: assert.False, + assertErr: assert.NoError, + // Doesn't matter if it's true or false as merge/assist bases are + // distinct. A future PR can go and remove the requirement to pass the + // flag to kopia and just pass it the bases instead. + assertB: assert.True, expectDCS: nil, + expectMans: kopia.NewMockBackupBases().WithAssistBases( + makeMan("id1", "checkpoint", path.EmailCategory), + ), }, { - name: "no manifests", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{}, + name: "one valid man, multiple reasons", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory, path.ContactsCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + }, + }, + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, }, - gb: mockGetBackuper{detailsID: did}, getMeta: true, assertErr: assert.NoError, assertB: assert.True, - expectDCS: nil, + expectDCS: []mockColl{{id: "id1"}}, + expectPaths: func(t *testing.T, gotPaths []path.Path) { + for _, p := range gotPaths { + assert.Equal( + t, + path.ExchangeMetadataService, + p.Service(), + "read data service") + + assert.Contains( + t, + []path.CategoryType{ + path.EmailCategory, + path.ContactsCategory, + }, + p.Category(), + "read data category doesn't match a given reason", + ) + } + }, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory, path.ContactsCategory), + ), }, { - name: "only incomplete manifests", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{ - makeMan(path.EmailCategory, "id1", "ir", ""), - makeMan(path.ContactsCategory, "id2", "ir", ""), + name: "one valid man, extra incomplete man", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory), + ).WithAssistBases( + makeMan("id2", "checkpoint", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, + }, + }, + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, }, }, - gb: mockGetBackuper{detailsID: did}, getMeta: true, assertErr: assert.NoError, assertB: assert.True, - expectDCS: nil, - }, - { - name: "man missing backup id", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{ - collsByID: map[string][]data.RestoreCollection{ - "id": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id_coll"}}}, - }, - }, - mans: []kopia.ManifestEntry{makeMan(path.EmailCategory, "id", "", "")}, - }, - gb: mockGetBackuper{detailsID: did}, - getMeta: true, - assertErr: assert.Error, - assertB: assert.False, - expectNilMans: true, - }, - { - name: "backup missing details id", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{}, - mans: []kopia.ManifestEntry{makeMan(path.EmailCategory, "id1", "", "bid")}, - }, - gb: mockGetBackuper{}, - getMeta: true, - assertErr: assert.NoError, - assertB: assert.False, - }, - { - name: "one complete, one incomplete", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{ - collsByID: map[string][]data.RestoreCollection{ - "id": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id_coll"}}}, - "incmpl_id": {data.NoFetchRestoreCollection{Collection: mockColl{id: "incmpl_id_coll"}}}, - }, - }, - mans: []kopia.ManifestEntry{ - makeMan(path.EmailCategory, "id", "", "bid"), - makeMan(path.EmailCategory, "incmpl_id", "ir", ""), - }, - }, - gb: mockGetBackuper{detailsID: did}, - getMeta: true, - assertErr: assert.NoError, - assertB: assert.True, - expectDCS: []mockColl{{id: "id_coll"}}, - }, - { - name: "single valid man", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{ - collsByID: map[string][]data.RestoreCollection{ - "id": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id_coll"}}}, - }, - }, - mans: []kopia.ManifestEntry{makeMan(path.EmailCategory, "id", "", "bid")}, - }, - gb: mockGetBackuper{detailsID: did}, - getMeta: true, - assertErr: assert.NoError, - assertB: assert.True, - expectDCS: []mockColl{{id: "id_coll"}}, + expectDCS: []mockColl{{id: "id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory), + ).WithAssistBases( + makeMan("id2", "checkpoint", path.EmailCategory), + ), }, { name: "multiple valid mans", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{ - collsByID: map[string][]data.RestoreCollection{ - "mail": {data.NoFetchRestoreCollection{Collection: mockColl{id: "mail_coll"}}}, - "contact": {data.NoFetchRestoreCollection{Collection: mockColl{id: "contact_coll"}}}, - }, - }, - mans: []kopia.ManifestEntry{ - makeMan(path.EmailCategory, "mail", "", "bid"), - makeMan(path.ContactsCategory, "contact", "", "bid"), + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory), + makeMan("id2", "", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, + }, + }, + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, }, }, - gb: mockGetBackuper{detailsID: did}, getMeta: true, assertErr: assert.NoError, assertB: assert.True, - expectDCS: []mockColl{ - {id: "mail_coll"}, - {id: "contact_coll"}, - }, + expectDCS: []mockColl{{id: "id1"}, {id: "id2"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory), + makeMan("id2", "", path.EmailCategory), + ), }, { name: "error collecting metadata", - mr: mockManifestRestorer{ - mockRestoreProducer: mockRestoreProducer{err: assert.AnError}, - mans: []kopia.ManifestEntry{makeMan(path.EmailCategory, "id1", "", "bid")}, + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan("id1", "", path.EmailCategory), + ), + }, }, - gb: mockGetBackuper{detailsID: did}, - getMeta: true, - assertErr: assert.Error, - assertB: assert.False, - expectDCS: nil, - expectNilMans: true, + rp: mockRestoreProducer{err: assert.AnError}, + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + getMeta: true, + assertErr: assert.Error, + assertB: assert.False, + expectDCS: nil, + expectMans: nil, }, } + for _, test := range table { suite.Run(test.name, func() { t := suite.T() @@ -657,20 +504,470 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { mans, dcs, b, err := produceManifestsAndMetadata( ctx, - &test.mr, - &test.gb, - []kopia.Reason{{ResourceOwner: ro}}, nil, + test.bf, + &test.rp, + test.reasons, nil, tid, test.getMeta) test.assertErr(t, err, clues.ToCore(err)) test.assertB(t, b) - expectMans := test.mr.mans - if test.expectNilMans { - expectMans = nil + kopia.AssertBackupBasesEqual(t, test.expectMans, mans) + + expect, got := []string{}, []string{} + + for _, dc := range test.expectDCS { + expect = append(expect, dc.id) } - assert.ElementsMatch(t, expectMans, mans) + for _, dc := range dcs { + if !assert.IsTypef( + t, + data.NoFetchRestoreCollection{}, + dc, + "unexpected type returned [%T]", + dc, + ) { + continue + } + + tmp := dc.(data.NoFetchRestoreCollection) + + if !assert.IsTypef( + t, + mockColl{}, + tmp.Collection, + "unexpected type returned [%T]", + tmp.Collection, + ) { + continue + } + + mc := tmp.Collection.(mockColl) + got = append(got, mc.id) + } + + assert.ElementsMatch(t, expect, got, "expected collections are present") + + if test.expectPaths != nil { + test.expectPaths(t, test.rp.gotPaths) + } + }) + } +} + +func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_FallbackReasons() { + const ( + ro = "resourceowner" + fbro = "fb_resourceowner" + tid = "tenantid" + did = "detailsid" + ) + + makeMan := func(ro, id, incmpl string, cats ...path.CategoryType) kopia.ManifestEntry { + return kopia.ManifestEntry{ + Manifest: &snapshot.Manifest{ + ID: manifest.ID(id), + IncompleteReason: incmpl, + Tags: map[string]string{"tag:" + kopia.TagBackupID: id + "bup"}, + }, + Reasons: buildReasons(ro, path.ExchangeService, cats...), + } + } + + makeBackup := func(ro, snapID string, cats ...path.CategoryType) kopia.BackupEntry { + return kopia.BackupEntry{ + Backup: &backup.Backup{ + BaseModel: model.BaseModel{ + ID: model.StableID(snapID + "bup"), + }, + SnapshotID: snapID, + StreamStoreID: snapID + "store", + }, + Reasons: buildReasons(ro, path.ExchangeService, cats...), + } + } + + emailReason := kopia.Reason{ + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + } + + fbEmailReason := kopia.Reason{ + ResourceOwner: fbro, + Service: path.ExchangeService, + Category: path.EmailCategory, + } + + table := []struct { + name string + bf *mockBackupFinder + rp mockRestoreProducer + reasons []kopia.Reason + fallbackReasons []kopia.Reason + getMeta bool + assertErr assert.ErrorAssertionFunc + assertB assert.BoolAssertionFunc + expectDCS []mockColl + expectMans kopia.BackupBases + }{ + { + name: "don't get metadata, only fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{}, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: false, + assertErr: assert.NoError, + assertB: assert.False, + expectDCS: nil, + expectMans: kopia.NewMockBackupBases().WithAssistBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ), + }, + { + name: "only fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + }, + }, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "fb_id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ), + }, + { + name: "complete mans and fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + }, + }, + reasons: []kopia.Reason{emailReason}, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ), + }, + { + name: "incomplete mans and fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithAssistBases( + makeMan(ro, "id2", "checkpoint", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithAssistBases( + makeMan(fbro, "fb_id2", "checkpoint", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, + "fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}}, + }, + }, + reasons: []kopia.Reason{emailReason}, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: nil, + expectMans: kopia.NewMockBackupBases().WithAssistBases( + makeMan(ro, "id2", "checkpoint", path.EmailCategory), + ), + }, + { + name: "complete and incomplete mans and fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ).WithAssistBases( + makeMan(ro, "id2", "checkpoint", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ).WithAssistBases( + makeMan(fbro, "fb_id2", "checkpoint", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + "fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}}, + }, + }, + reasons: []kopia.Reason{emailReason}, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ).WithAssistBases( + makeMan(ro, "id2", "checkpoint", path.EmailCategory), + ), + }, + { + name: "incomplete mans and complete fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithAssistBases( + makeMan(ro, "id2", "checkpoint", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + }, + }, + reasons: []kopia.Reason{emailReason}, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "fb_id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory), + ).WithAssistBases( + makeMan(ro, "id2", "checkpoint", path.EmailCategory), + ), + }, + { + name: "complete mans and incomplete fallbacks", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithAssistBases( + makeMan(fbro, "fb_id2", "checkpoint", path.EmailCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}}, + }, + }, + reasons: []kopia.Reason{emailReason}, + fallbackReasons: []kopia.Reason{fbEmailReason}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ), + }, + { + name: "complete mans and complete fallbacks, multiple reasons", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory, path.ContactsCategory), + ), + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory, path.ContactsCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory, path.ContactsCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + }, + }, + reasons: []kopia.Reason{ + emailReason, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + fallbackReasons: []kopia.Reason{ + fbEmailReason, + { + ResourceOwner: fbro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory, path.ContactsCategory), + ), + }, + { + name: "complete mans and complete fallbacks, distinct reasons", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.ContactsCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.ContactsCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + }, + }, + reasons: []kopia.Reason{emailReason}, + fallbackReasons: []kopia.Reason{ + { + ResourceOwner: fbro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "id1"}, {id: "fb_id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + makeMan(fbro, "fb_id1", "", path.ContactsCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.ContactsCategory), + ), + }, + { + name: "complete mans and complete fallbacks, fallback has superset of reasons", + bf: &mockBackupFinder{ + data: map[string]kopia.BackupBases{ + ro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + ), + fbro: kopia.NewMockBackupBases().WithMergeBases( + makeMan(fbro, "fb_id1", "", path.EmailCategory, path.ContactsCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.EmailCategory, path.ContactsCategory), + ), + }, + }, + rp: mockRestoreProducer{ + collsByID: map[string][]data.RestoreCollection{ + "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, + "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, + }, + }, + reasons: []kopia.Reason{ + emailReason, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + fallbackReasons: []kopia.Reason{ + fbEmailReason, + { + ResourceOwner: fbro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []mockColl{{id: "id1"}, {id: "fb_id1"}}, + expectMans: kopia.NewMockBackupBases().WithMergeBases( + makeMan(ro, "id1", "", path.EmailCategory), + makeMan(fbro, "fb_id1", "", path.ContactsCategory), + ).WithBackups( + makeBackup(fbro, "fb_id1", path.ContactsCategory), + ), + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + mans, dcs, b, err := produceManifestsAndMetadata( + ctx, + test.bf, + &test.rp, + test.reasons, test.fallbackReasons, + tid, + test.getMeta) + test.assertErr(t, err, clues.ToCore(err)) + test.assertB(t, b) + + kopia.AssertBackupBasesEqual(t, test.expectMans, mans) expect, got := []string{}, []string{} @@ -709,603 +1006,3 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { }) } } - -func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_fallbackReasons() { - const ( - ro = "resourceowner" - manComplete = "complete" - manIncomplete = "incmpl" - - fbro = "fb_resourceowner" - fbComplete = "fb_complete" - fbIncomplete = "fb_incmpl" - ) - - makeMan := func(id, incmpl string, reasons []kopia.Reason) kopia.ManifestEntry { - return kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{ - ID: manifest.ID(id), - IncompleteReason: incmpl, - Tags: map[string]string{}, - }, - Reasons: reasons, - } - } - - type testInput struct { - id string - incomplete bool - } - - table := []struct { - name string - man []testInput - fallback []testInput - reasons []kopia.Reason - fallbackReasons []kopia.Reason - manCategories []path.CategoryType - fbCategories []path.CategoryType - assertErr assert.ErrorAssertionFunc - expectManIDs []string - expectNilMans bool - expectReasons map[string][]path.CategoryType - }{ - { - name: "only mans, no fallbacks", - man: []testInput{ - { - id: manComplete, - }, - { - id: manIncomplete, - incomplete: true, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{manComplete, manIncomplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.EmailCategory}, - manIncomplete: {path.EmailCategory}, - }, - }, - { - name: "no mans, only fallbacks", - fallback: []testInput{ - { - id: fbComplete, - }, - { - id: fbIncomplete, - incomplete: true, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{fbComplete, fbIncomplete}, - expectReasons: map[string][]path.CategoryType{ - fbComplete: {path.EmailCategory}, - fbIncomplete: {path.EmailCategory}, - }, - }, - { - name: "complete mans and fallbacks", - man: []testInput{ - { - id: manComplete, - }, - }, - fallback: []testInput{ - { - id: fbComplete, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{manComplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.EmailCategory}, - }, - }, - { - name: "incomplete mans and fallbacks", - man: []testInput{ - { - id: manIncomplete, - incomplete: true, - }, - }, - fallback: []testInput{ - { - id: fbIncomplete, - incomplete: true, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{manIncomplete}, - expectReasons: map[string][]path.CategoryType{ - manIncomplete: {path.EmailCategory}, - }, - }, - { - name: "complete and incomplete mans and fallbacks", - man: []testInput{ - { - id: manComplete, - }, - { - id: manIncomplete, - incomplete: true, - }, - }, - fallback: []testInput{ - { - id: fbComplete, - }, - { - id: fbIncomplete, - incomplete: true, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{manComplete, manIncomplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.EmailCategory}, - manIncomplete: {path.EmailCategory}, - }, - }, - { - name: "incomplete mans, complete fallbacks", - man: []testInput{ - { - id: manIncomplete, - incomplete: true, - }, - }, - fallback: []testInput{ - { - id: fbComplete, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{fbComplete, manIncomplete}, - expectReasons: map[string][]path.CategoryType{ - fbComplete: {path.EmailCategory}, - manIncomplete: {path.EmailCategory}, - }, - }, - { - name: "complete mans, incomplete fallbacks", - man: []testInput{ - { - id: manComplete, - }, - }, - fallback: []testInput{ - { - id: fbIncomplete, - incomplete: true, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{manComplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.EmailCategory}, - }, - }, - { - name: "complete mans, complete fallbacks, multiple reasons", - man: []testInput{ - { - id: manComplete, - }, - }, - fallback: []testInput{ - { - id: fbComplete, - }, - }, - manCategories: []path.CategoryType{path.EmailCategory, path.ContactsCategory}, - fbCategories: []path.CategoryType{path.EmailCategory, path.ContactsCategory}, - expectManIDs: []string{manComplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.EmailCategory, path.ContactsCategory}, - }, - }, - { - name: "complete mans, complete fallbacks, distinct reasons", - man: []testInput{ - { - id: manComplete, - }, - }, - fallback: []testInput{ - { - id: fbComplete, - }, - }, - manCategories: []path.CategoryType{path.ContactsCategory}, - fbCategories: []path.CategoryType{path.EmailCategory}, - expectManIDs: []string{manComplete, fbComplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.ContactsCategory}, - fbComplete: {path.EmailCategory}, - }, - }, - { - name: "fb has superset of mans reasons", - man: []testInput{ - { - id: manComplete, - }, - }, - fallback: []testInput{ - { - id: fbComplete, - }, - }, - manCategories: []path.CategoryType{path.ContactsCategory}, - fbCategories: []path.CategoryType{path.EmailCategory, path.ContactsCategory, path.EventsCategory}, - expectManIDs: []string{manComplete, fbComplete}, - expectReasons: map[string][]path.CategoryType{ - manComplete: {path.ContactsCategory}, - fbComplete: {path.EmailCategory, path.EventsCategory}, - }, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - mainReasons := []kopia.Reason{} - fbReasons := []kopia.Reason{} - - for _, cat := range test.manCategories { - mainReasons = append( - mainReasons, - kopia.Reason{ - ResourceOwner: ro, - Service: path.ExchangeService, - Category: cat, - }) - } - - for _, cat := range test.fbCategories { - fbReasons = append( - fbReasons, - kopia.Reason{ - ResourceOwner: fbro, - Service: path.ExchangeService, - Category: cat, - }) - } - - mans := []kopia.ManifestEntry{} - - for _, m := range test.man { - incomplete := "" - if m.incomplete { - incomplete = "ir" - } - - mans = append(mans, makeMan(m.id, incomplete, mainReasons)) - } - - for _, m := range test.fallback { - incomplete := "" - if m.incomplete { - incomplete = "ir" - } - - mans = append(mans, makeMan(m.id, incomplete, fbReasons)) - } - - mr := mockManifestRestorer{mans: mans} - - gotMans, _, b, err := produceManifestsAndMetadata( - ctx, - &mr, - nil, - mainReasons, - fbReasons, - "tid", - false) - require.NoError(t, err, clues.ToCore(err)) - assert.False(t, b, "no-metadata is forced for this test") - - manIDs := []string{} - - for _, m := range gotMans { - manIDs = append(manIDs, string(m.ID)) - - reasons := test.expectReasons[string(m.ID)] - - mrs := []path.CategoryType{} - for _, r := range m.Reasons { - mrs = append(mrs, r.Category) - } - - assert.ElementsMatch(t, reasons, mrs) - } - - assert.ElementsMatch(t, test.expectManIDs, manIDs) - }) - } -} - -// --------------------------------------------------------------------------- -// older tests -// --------------------------------------------------------------------------- - -type BackupManifestUnitSuite struct { - tester.Suite -} - -func TestBackupManifestUnitSuite(t *testing.T) { - suite.Run(t, &BackupManifestUnitSuite{Suite: tester.NewUnitSuite(t)}) -} - -func (suite *BackupManifestUnitSuite) TestBackupOperation_VerifyDistinctBases() { - const user = "a-user" - - table := []struct { - name string - input []kopia.ManifestEntry - errCheck assert.ErrorAssertionFunc - }{ - { - name: "SingleManifestMultipleReasons", - input: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EventsCategory, - }, - }, - }, - }, - errCheck: assert.NoError, - }, - { - name: "MultipleManifestsDistinctReason", - input: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{ - ID: "id2", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EventsCategory, - }, - }, - }, - }, - errCheck: assert.NoError, - }, - { - name: "MultipleManifestsSameReason", - input: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{ - ID: "id2", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - errCheck: assert.Error, - }, - { - name: "MultipleManifestsSameReasonOneIncomplete", - input: []kopia.ManifestEntry{ - { - Manifest: &snapshot.Manifest{ - ID: "id1", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - { - Manifest: &snapshot.Manifest{ - ID: "id2", - IncompleteReason: "checkpoint", - }, - Reasons: []kopia.Reason{ - { - ResourceOwner: user, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - }, - errCheck: assert.NoError, - }, - } - - for _, test := range table { - suite.Run(test.name, func() { - ctx, flush := tester.NewContext(suite.T()) - defer flush() - - err := verifyDistinctBases(ctx, test.input) - test.errCheck(suite.T(), err, clues.ToCore(err)) - }) - } -} - -func (suite *BackupManifestUnitSuite) TestBackupOperation_CollectMetadata() { - var ( - tenant = "a-tenant" - resourceOwner = "a-user" - fileNames = []string{ - "delta", - "paths", - } - - emailDeltaPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.EmailCategory, - fileNames[0], - ) - emailPathsPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.EmailCategory, - fileNames[1], - ) - contactsDeltaPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.ContactsCategory, - fileNames[0], - ) - contactsPathsPath = makeMetadataPath( - suite.T(), - tenant, - path.ExchangeService, - resourceOwner, - path.ContactsCategory, - fileNames[1], - ) - ) - - table := []struct { - name string - inputMan kopia.ManifestEntry - inputFiles []string - expected []path.Path - }{ - { - name: "SingleReasonSingleFile", - inputMan: kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - inputFiles: []string{fileNames[0]}, - expected: []path.Path{emailDeltaPath}, - }, - { - name: "SingleReasonMultipleFiles", - inputMan: kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - }, - }, - inputFiles: fileNames, - expected: []path.Path{emailDeltaPath, emailPathsPath}, - }, - { - name: "MultipleReasonsMultipleFiles", - inputMan: kopia.ManifestEntry{ - Manifest: &snapshot.Manifest{}, - Reasons: []kopia.Reason{ - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.EmailCategory, - }, - { - ResourceOwner: resourceOwner, - Service: path.ExchangeService, - Category: path.ContactsCategory, - }, - }, - }, - inputFiles: fileNames, - expected: []path.Path{ - emailDeltaPath, - emailPathsPath, - contactsDeltaPath, - contactsPathsPath, - }, - }, - } - - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - mr := &mockRestoreProducer{} - - _, err := collectMetadata(ctx, mr, test.inputMan, test.inputFiles, tenant, fault.New(true)) - assert.NoError(t, err, clues.ToCore(err)) - - checkPaths(t, test.expected, mr.gotPaths) - }) - } -}