Move the Reasoner implementation from the kopia package to the identity package. This will help avoid import cycles if we want to start persisting Reason information in the backup model. --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [x] ⛔ No #### Type of change - [ ] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [x] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Test Plan - [ ] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
972 lines
28 KiB
Go
972 lines
28 KiB
Go
package kopia
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/kopia/kopia/repo/manifest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/alcionai/corso/src/internal/data"
|
|
"github.com/alcionai/corso/src/internal/model"
|
|
"github.com/alcionai/corso/src/internal/tester"
|
|
"github.com/alcionai/corso/src/pkg/backup"
|
|
"github.com/alcionai/corso/src/pkg/backup/identity"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
)
|
|
|
|
type BackupCleanupUnitSuite struct {
|
|
tester.Suite
|
|
}
|
|
|
|
func TestBackupCleanupUnitSuite(t *testing.T) {
|
|
suite.Run(t, &BackupCleanupUnitSuite{Suite: tester.NewUnitSuite(t)})
|
|
}
|
|
|
|
type mockManifestFinder struct {
|
|
t *testing.T
|
|
manifests []*manifest.EntryMetadata
|
|
err error
|
|
}
|
|
|
|
func (mmf mockManifestFinder) FindManifests(
|
|
ctx context.Context,
|
|
tags map[string]string,
|
|
) ([]*manifest.EntryMetadata, error) {
|
|
assert.Equal(
|
|
mmf.t,
|
|
map[string]string{"type": "snapshot"},
|
|
tags,
|
|
"snapshot search tags")
|
|
|
|
return mmf.manifests, clues.Stack(mmf.err).OrNil()
|
|
}
|
|
|
|
type mockStorer struct {
|
|
t *testing.T
|
|
|
|
details []*model.BaseModel
|
|
detailsErr error
|
|
|
|
backups []backupRes
|
|
backupListErr error
|
|
|
|
expectDeleteIDs []manifest.ID
|
|
deleteErr error
|
|
}
|
|
|
|
func (ms mockStorer) Delete(context.Context, model.Schema, model.StableID) error {
|
|
return clues.New("not implemented")
|
|
}
|
|
|
|
func (ms mockStorer) Get(context.Context, model.Schema, model.StableID, model.Model) error {
|
|
return clues.New("not implemented")
|
|
}
|
|
|
|
func (ms mockStorer) Put(context.Context, model.Schema, model.Model) error {
|
|
return clues.New("not implemented")
|
|
}
|
|
|
|
func (ms mockStorer) Update(context.Context, model.Schema, model.Model) error {
|
|
return clues.New("not implemented")
|
|
}
|
|
|
|
func (ms mockStorer) GetIDsForType(
|
|
_ context.Context,
|
|
s model.Schema,
|
|
tags map[string]string,
|
|
) ([]*model.BaseModel, error) {
|
|
assert.Empty(ms.t, tags, "model search tags")
|
|
|
|
switch s {
|
|
case model.BackupDetailsSchema:
|
|
return ms.details, clues.Stack(ms.detailsErr).OrNil()
|
|
|
|
case model.BackupSchema:
|
|
var bases []*model.BaseModel
|
|
|
|
for _, b := range ms.backups {
|
|
bases = append(bases, &b.bup.BaseModel)
|
|
}
|
|
|
|
return bases, clues.Stack(ms.backupListErr).OrNil()
|
|
}
|
|
|
|
return nil, clues.New(fmt.Sprintf("unknown type: %s", s.String()))
|
|
}
|
|
|
|
func (ms mockStorer) GetWithModelStoreID(
|
|
_ context.Context,
|
|
s model.Schema,
|
|
id manifest.ID,
|
|
m model.Model,
|
|
) error {
|
|
assert.Equal(ms.t, model.BackupSchema, s, "model get schema")
|
|
|
|
d := m.(*backup.Backup)
|
|
|
|
for _, b := range ms.backups {
|
|
if id == b.bup.ModelStoreID {
|
|
*d = *b.bup
|
|
return clues.Stack(b.err).OrNil()
|
|
}
|
|
}
|
|
|
|
return clues.Stack(data.ErrNotFound)
|
|
}
|
|
|
|
func (ms mockStorer) DeleteWithModelStoreIDs(
|
|
_ context.Context,
|
|
ids ...manifest.ID,
|
|
) error {
|
|
assert.ElementsMatch(ms.t, ms.expectDeleteIDs, ids, "model delete IDs")
|
|
return clues.Stack(ms.deleteErr).OrNil()
|
|
}
|
|
|
|
// backupRes represents an individual return value for an item in GetIDsForType
|
|
// or the result of GetWithModelStoreID. err is used for GetWithModelStoreID
|
|
// only.
|
|
type backupRes struct {
|
|
bup *backup.Backup
|
|
err error
|
|
}
|
|
|
|
func (suite *BackupCleanupUnitSuite) TestCleanupOrphanedData() {
|
|
backupTag, _ := makeTagKV(TagBackupCategory)
|
|
|
|
// Current backup and snapshots.
|
|
bupCurrent := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("current-bup-id"),
|
|
ModelStoreID: manifest.ID("current-bup-msid"),
|
|
},
|
|
SnapshotID: "current-snap-msid",
|
|
StreamStoreID: "current-deets-msid",
|
|
}
|
|
}
|
|
|
|
snapCurrent := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "current-snap-msid",
|
|
Labels: map[string]string{
|
|
backupTag: "0",
|
|
},
|
|
}
|
|
}
|
|
|
|
deetsCurrent := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "current-deets-msid",
|
|
}
|
|
}
|
|
|
|
bupCurrent2 := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("current-bup-id-2"),
|
|
ModelStoreID: manifest.ID("current-bup-msid-2"),
|
|
},
|
|
SnapshotID: "current-snap-msid-2",
|
|
StreamStoreID: "current-deets-msid-2",
|
|
}
|
|
}
|
|
|
|
snapCurrent2 := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "current-snap-msid-2",
|
|
Labels: map[string]string{
|
|
backupTag: "0",
|
|
},
|
|
}
|
|
}
|
|
|
|
deetsCurrent2 := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "current-deets-msid-2",
|
|
}
|
|
}
|
|
|
|
bupCurrent3 := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("current-bup-id-3"),
|
|
ModelStoreID: manifest.ID("current-bup-msid-3"),
|
|
},
|
|
SnapshotID: "current-snap-msid-3",
|
|
StreamStoreID: "current-deets-msid-3",
|
|
}
|
|
}
|
|
|
|
snapCurrent3 := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "current-snap-msid-3",
|
|
Labels: map[string]string{
|
|
backupTag: "0",
|
|
},
|
|
}
|
|
}
|
|
|
|
deetsCurrent3 := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "current-deets-msid-3",
|
|
}
|
|
}
|
|
|
|
// Legacy backup with details in separate model.
|
|
bupLegacy := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("legacy-bup-id"),
|
|
ModelStoreID: manifest.ID("legacy-bup-msid"),
|
|
},
|
|
SnapshotID: "legacy-snap-msid",
|
|
DetailsID: "legacy-deets-msid",
|
|
}
|
|
}
|
|
|
|
snapLegacy := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "legacy-snap-msid",
|
|
Labels: map[string]string{
|
|
backupTag: "0",
|
|
},
|
|
}
|
|
}
|
|
|
|
deetsLegacy := func() *model.BaseModel {
|
|
return &model.BaseModel{
|
|
ID: "legacy-deets-id",
|
|
ModelStoreID: "legacy-deets-msid",
|
|
}
|
|
}
|
|
|
|
// Incomplete backup missing data snapshot.
|
|
bupNoSnapshot := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("ns-bup-id"),
|
|
ModelStoreID: manifest.ID("ns-bup-id-msid"),
|
|
},
|
|
StreamStoreID: "ns-deets-msid",
|
|
}
|
|
}
|
|
|
|
deetsNoSnapshot := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "ns-deets-msid",
|
|
}
|
|
}
|
|
|
|
// Legacy incomplete backup missing data snapshot.
|
|
bupLegacyNoSnapshot := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("ns-legacy-bup-id"),
|
|
ModelStoreID: manifest.ID("ns-legacy-bup-id-msid"),
|
|
},
|
|
DetailsID: "ns-legacy-deets-msid",
|
|
}
|
|
}
|
|
|
|
deetsLegacyNoSnapshot := func() *model.BaseModel {
|
|
return &model.BaseModel{
|
|
ID: "ns-legacy-deets-id",
|
|
ModelStoreID: "ns-legacy-deets-msid",
|
|
}
|
|
}
|
|
|
|
// Incomplete backup missing details.
|
|
bupNoDetails := func() *backup.Backup {
|
|
return &backup.Backup{
|
|
BaseModel: model.BaseModel{
|
|
ID: model.StableID("nssid-bup-id"),
|
|
ModelStoreID: manifest.ID("nssid-bup-msid"),
|
|
},
|
|
SnapshotID: "nssid-snap-msid",
|
|
}
|
|
}
|
|
|
|
snapNoDetails := func() *manifest.EntryMetadata {
|
|
return &manifest.EntryMetadata{
|
|
ID: "nssid-snap-msid",
|
|
Labels: map[string]string{
|
|
backupTag: "0",
|
|
},
|
|
}
|
|
}
|
|
|
|
// Get some stable time so that we can do everything relative to this in the
|
|
// tests. Mostly just makes reasoning/viewing times easier because the only
|
|
// differences will be the changes we make.
|
|
baseTime := time.Now()
|
|
|
|
manifestWithTime := func(
|
|
mt time.Time,
|
|
m *manifest.EntryMetadata,
|
|
) *manifest.EntryMetadata {
|
|
res := *m
|
|
res.ModTime = mt
|
|
|
|
return &res
|
|
}
|
|
|
|
manifestWithReasons := func(
|
|
m *manifest.EntryMetadata,
|
|
tenantID string,
|
|
reasons ...identity.Reasoner,
|
|
) *manifest.EntryMetadata {
|
|
res := *m
|
|
|
|
if res.Labels == nil {
|
|
res.Labels = map[string]string{}
|
|
}
|
|
|
|
res.Labels[kopiaPathLabel] = encodeAsPath(tenantID)
|
|
|
|
// Add the given reasons.
|
|
for _, r := range reasons {
|
|
for _, k := range tagKeys(r) {
|
|
key, _ := makeTagKV(k)
|
|
res.Labels[key] = "0"
|
|
}
|
|
}
|
|
|
|
// Also add other common reasons on item data snapshots.
|
|
k, _ := makeTagKV(TagBackupCategory)
|
|
res.Labels[k] = "0"
|
|
|
|
return &res
|
|
}
|
|
|
|
backupWithTime := func(mt time.Time, b *backup.Backup) *backup.Backup {
|
|
res := *b
|
|
res.ModTime = mt
|
|
res.CreationTime = mt
|
|
|
|
return &res
|
|
}
|
|
|
|
backupWithResource := func(protectedResource string, isAssist bool, b *backup.Backup) *backup.Backup {
|
|
res := *b
|
|
res.ProtectedResourceID = protectedResource
|
|
|
|
t := model.MergeBackup
|
|
if isAssist {
|
|
t = model.AssistBackup
|
|
}
|
|
|
|
if res.Tags == nil {
|
|
res.Tags = map[string]string{}
|
|
}
|
|
|
|
res.Tags[model.BackupTypeTag] = t
|
|
|
|
return &res
|
|
}
|
|
|
|
backupWithLegacyResource := func(protectedResource string, b *backup.Backup) *backup.Backup {
|
|
res := *b
|
|
res.ResourceOwnerID = protectedResource
|
|
|
|
return &res
|
|
}
|
|
|
|
table := []struct {
|
|
name string
|
|
snapshots []*manifest.EntryMetadata
|
|
snapshotFetchErr error
|
|
// only need BaseModel here since we never look inside the details items.
|
|
detailsModels []*model.BaseModel
|
|
detailsModelListErr error
|
|
backups []backupRes
|
|
backupListErr error
|
|
deleteErr error
|
|
time time.Time
|
|
buffer time.Duration
|
|
|
|
expectDeleteIDs []manifest.ID
|
|
expectErr assert.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "EmptyRepo",
|
|
time: baseTime,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "OnlyCompleteBackups Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
deetsCurrent(),
|
|
snapLegacy(),
|
|
},
|
|
detailsModels: []*model.BaseModel{
|
|
deetsLegacy(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
{bup: bupLegacy()},
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "MissingFieldsInBackup CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapNoDetails(),
|
|
deetsNoSnapshot(),
|
|
},
|
|
detailsModels: []*model.BaseModel{
|
|
deetsLegacyNoSnapshot(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupNoSnapshot()},
|
|
{bup: bupLegacyNoSnapshot()},
|
|
{bup: bupNoDetails()},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
manifest.ID(bupNoSnapshot().ModelStoreID),
|
|
manifest.ID(bupLegacyNoSnapshot().ModelStoreID),
|
|
manifest.ID(bupNoDetails().ModelStoreID),
|
|
manifest.ID(deetsLegacyNoSnapshot().ModelStoreID),
|
|
snapNoDetails().ID,
|
|
deetsNoSnapshot().ID,
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "MissingSnapshot CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
deetsCurrent(),
|
|
},
|
|
detailsModels: []*model.BaseModel{
|
|
deetsLegacy(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
{bup: bupLegacy()},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
manifest.ID(bupCurrent().ModelStoreID),
|
|
deetsCurrent().ID,
|
|
manifest.ID(bupLegacy().ModelStoreID),
|
|
manifest.ID(deetsLegacy().ModelStoreID),
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "MissingDetails CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
snapLegacy(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
{bup: bupLegacy()},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
manifest.ID(bupCurrent().ModelStoreID),
|
|
manifest.ID(bupLegacy().ModelStoreID),
|
|
snapCurrent().ID,
|
|
snapLegacy().ID,
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.NoError,
|
|
},
|
|
// Tests with various errors from Storer.
|
|
{
|
|
name: "SnapshotsListError Fails",
|
|
snapshotFetchErr: assert.AnError,
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
},
|
|
expectErr: assert.Error,
|
|
},
|
|
{
|
|
name: "LegacyDetailsListError Fails",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
},
|
|
detailsModelListErr: assert.AnError,
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.Error,
|
|
},
|
|
{
|
|
name: "BackupIDsListError Fails",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
deetsCurrent(),
|
|
},
|
|
backupListErr: assert.AnError,
|
|
time: baseTime,
|
|
expectErr: assert.Error,
|
|
},
|
|
{
|
|
name: "BackupModelGetErrorNotFound CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
deetsCurrent(),
|
|
snapLegacy(),
|
|
snapNoDetails(),
|
|
},
|
|
detailsModels: []*model.BaseModel{
|
|
deetsLegacy(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
{
|
|
bup: bupLegacy(),
|
|
err: data.ErrNotFound,
|
|
},
|
|
{
|
|
bup: bupNoDetails(),
|
|
err: data.ErrNotFound,
|
|
},
|
|
},
|
|
// Backup IDs are still included in here because they're added to the
|
|
// deletion set prior to attempting to fetch models. The model store
|
|
// delete operation should ignore missing models though so there's no
|
|
// issue.
|
|
expectDeleteIDs: []manifest.ID{
|
|
snapLegacy().ID,
|
|
manifest.ID(deetsLegacy().ModelStoreID),
|
|
manifest.ID(bupLegacy().ModelStoreID),
|
|
snapNoDetails().ID,
|
|
manifest.ID(bupNoDetails().ModelStoreID),
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "BackupModelGetError Fails",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
deetsCurrent(),
|
|
snapLegacy(),
|
|
snapNoDetails(),
|
|
},
|
|
detailsModels: []*model.BaseModel{
|
|
deetsLegacy(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
{
|
|
bup: bupLegacy(),
|
|
err: assert.AnError,
|
|
},
|
|
{bup: bupNoDetails()},
|
|
},
|
|
time: baseTime,
|
|
expectErr: assert.Error,
|
|
},
|
|
{
|
|
name: "DeleteError Fails",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
snapCurrent(),
|
|
deetsCurrent(),
|
|
snapLegacy(),
|
|
snapNoDetails(),
|
|
},
|
|
detailsModels: []*model.BaseModel{
|
|
deetsLegacy(),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: bupCurrent()},
|
|
{bup: bupLegacy()},
|
|
{bup: bupNoDetails()},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
snapNoDetails().ID,
|
|
manifest.ID(bupNoDetails().ModelStoreID),
|
|
},
|
|
deleteErr: assert.AnError,
|
|
time: baseTime,
|
|
expectErr: assert.Error,
|
|
},
|
|
// Tests dealing with buffer times.
|
|
{
|
|
name: "MissingSnapshot BarelyTooYoungForCleanup Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithTime(baseTime, bupCurrent())},
|
|
},
|
|
time: baseTime.Add(24 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "MissingSnapshot BarelyOldEnough CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithTime(baseTime, bupCurrent())},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
deetsCurrent().ID,
|
|
manifest.ID(bupCurrent().ModelStoreID),
|
|
},
|
|
time: baseTime.Add((24 * time.Hour) + time.Second),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "BackupGetErrorNotFound TooYoung Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
},
|
|
backups: []backupRes{
|
|
{
|
|
bup: backupWithTime(baseTime, bupCurrent()),
|
|
err: data.ErrNotFound,
|
|
},
|
|
},
|
|
time: baseTime,
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
// Tests dealing with assist base cleanup.
|
|
{
|
|
// Test that even if we have multiple assist bases with the same
|
|
// Reason(s), none of them are garbage collected if they are within the
|
|
// buffer period used to exclude recently created backups from garbage
|
|
// collection.
|
|
name: "AssistBase NotYoungest InBufferTime Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
},
|
|
time: baseTime,
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that an assist base that has the same Reasons as a newer assist
|
|
// base is garbage collected when it's outside the buffer period. Also
|
|
// check the most recent assist base isn't garbage collected if it's
|
|
// younger than the most recent merge base.
|
|
name: "AssistAndMergeBases MixedAges CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Minute), snapCurrent3()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Minute), deetsCurrent3()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Minute), bupCurrent3()))},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
snapCurrent().ID,
|
|
deetsCurrent().ID,
|
|
manifest.ID(bupCurrent().ModelStoreID),
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that an assist base that has the same Reasons as a newer merge
|
|
// base but the merge base is from an older version of corso for some
|
|
// reason doesn't cause the assist base to be garbage collected. This is
|
|
// not ideal, but some older versions of corso didn't even populate the
|
|
// resource owner ID.
|
|
//
|
|
// Worst case, the assist base will be cleaned up when the user upgrades
|
|
// corso and generates either a new assist base or merge base with the
|
|
// same reason.
|
|
name: "AssistAndLegacyMergeBases NotYoungest Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithLegacyResource("ro", backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that an assist base that has the same Reasons as a newer merge
|
|
// base but the merge base is from an older version of corso for some
|
|
// reason and an even newer merge base from a current version of corso
|
|
// causes the assist base to be garbage collected.
|
|
//
|
|
// This also tests that bases without a merge or assist tag are not
|
|
// garbage collected as an assist base.
|
|
name: "AssistAndLegacyAndCurrentMergeBases NotYoungest CausesCleanup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Minute), snapCurrent3()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Minute), deetsCurrent3()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithLegacyResource("ro", backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
{bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(time.Minute), bupCurrent3()))},
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectDeleteIDs: []manifest.ID{
|
|
snapCurrent().ID,
|
|
deetsCurrent().ID,
|
|
manifest.ID(bupCurrent().ModelStoreID),
|
|
},
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that the most recent assist base is garbage collected if there's a
|
|
// newer merge base that has the same Reasons as the assist base.
|
|
name: "AssistBasesAndMergeBases NotYoungest CausesCleanupForAssistBase",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Minute), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Minute), deetsCurrent2()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent3()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent3()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(time.Minute), bupCurrent2()))},
|
|
{bup: backupWithResource("ro", false, backupWithTime(baseTime.Add(-time.Second), bupCurrent3()))},
|
|
},
|
|
expectDeleteIDs: []manifest.ID{
|
|
snapCurrent().ID,
|
|
deetsCurrent().ID,
|
|
manifest.ID(bupCurrent().ModelStoreID),
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that an assist base that is not the most recent for Reason A but
|
|
// is the most recent for Reason B is not garbage collected.
|
|
name: "AssistBases YoungestInOneReason Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory),
|
|
identity.NewReason("", "ro", path.ExchangeService, path.ContactsCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that assist bases that have the same tenant, service, and category
|
|
// but different protected resources are not garbage collected. This is
|
|
// a test to ensure the Reason field is properly handled when finding the
|
|
// most recent assist base.
|
|
name: "AssistBases DifferentProtectedResources Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro1", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro2", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro1", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro2", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that assist bases that have the same protected resource, service,
|
|
// and category but different tenants are not garbage collected. This is a
|
|
// test to ensure the Reason field is properly handled when finding the
|
|
// most recent assist base.
|
|
name: "AssistBases DifferentTenants Noops",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant2",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectErr: assert.NoError,
|
|
},
|
|
{
|
|
// Test that if the tenant is not available for a given assist base that
|
|
// it's excluded from the garbage collection set. This behavior is
|
|
// conservative because it's quite likely that we could garbage collect
|
|
// the base without issue.
|
|
name: "AssistBases NoTenant SkipsBackup",
|
|
snapshots: []*manifest.EntryMetadata{
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime, snapCurrent()),
|
|
"",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime, deetsCurrent()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Second), snapCurrent2()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Second), deetsCurrent2()),
|
|
|
|
manifestWithReasons(
|
|
manifestWithTime(baseTime.Add(time.Minute), snapCurrent3()),
|
|
"tenant1",
|
|
identity.NewReason("", "ro", path.ExchangeService, path.EmailCategory)),
|
|
manifestWithTime(baseTime.Add(time.Minute), deetsCurrent3()),
|
|
},
|
|
backups: []backupRes{
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime, bupCurrent()))},
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Second), bupCurrent2()))},
|
|
{bup: backupWithResource("ro", true, backupWithTime(baseTime.Add(time.Minute), bupCurrent3()))},
|
|
},
|
|
time: baseTime.Add(48 * time.Hour),
|
|
buffer: 24 * time.Hour,
|
|
expectDeleteIDs: []manifest.ID{
|
|
snapCurrent2().ID,
|
|
deetsCurrent2().ID,
|
|
manifest.ID(bupCurrent2().ModelStoreID),
|
|
},
|
|
expectErr: assert.NoError,
|
|
},
|
|
}
|
|
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
mbs := mockStorer{
|
|
t: t,
|
|
details: test.detailsModels,
|
|
detailsErr: test.detailsModelListErr,
|
|
backups: test.backups,
|
|
backupListErr: test.backupListErr,
|
|
expectDeleteIDs: test.expectDeleteIDs,
|
|
deleteErr: test.deleteErr,
|
|
}
|
|
|
|
mmf := mockManifestFinder{
|
|
t: t,
|
|
manifests: test.snapshots,
|
|
err: test.snapshotFetchErr,
|
|
}
|
|
|
|
err := cleanupOrphanedData(
|
|
ctx,
|
|
mbs,
|
|
mmf,
|
|
test.buffer,
|
|
func() time.Time { return test.time })
|
|
test.expectErr(t, err, clues.ToCore(err))
|
|
})
|
|
}
|
|
}
|