From db2c1ec8e2eea53654a7cff6d9176ee96c6cf04b Mon Sep 17 00:00:00 2001 From: Keepers Date: Wed, 24 Aug 2022 10:30:27 -0600 Subject: [PATCH] delete backups in modelStore and snapshot (#640) Introduces manual deletion of existing backups. The delete includes: the modelStore backup, modelStore details, and the kopia snapshot of the backup itself. --- src/internal/kopia/wrapper.go | 32 ++++++++ src/internal/kopia/wrapper_test.go | 50 ++++++++++++ src/pkg/backup/details/details_test.go | 93 ---------------------- src/pkg/repository/repository.go | 15 ++++ src/pkg/store/backup.go | 12 +++ src/pkg/store/backup_test.go | 102 +++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 93 deletions(-) diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index 4a29fbf16..947ae9a34 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -666,3 +666,35 @@ func (w Wrapper) RestoreMultipleItems( } return dcs, errs.ErrorOrNil() } + +// DeleteSnapshot removes the provided manifest from kopia. +func (w Wrapper) DeleteSnapshot( + ctx context.Context, + snapshotID string, +) error { + mid := manifest.ID(snapshotID) + + if len(mid) == 0 { + return errors.New("attempt to delete unidentified snapshot") + } + + err := repo.WriteSession( + ctx, + w.c, + repo.WriteSessionOptions{Purpose: "KopiaWrapperBackupDeletion"}, + func(innerCtx context.Context, rw repo.RepositoryWriter) error { + if err := rw.DeleteManifest(ctx, mid); err != nil { + return errors.Wrap(err, "deleting snapshot") + } + + return nil + }, + ) + // Telling kopia to always flush may hide other errors if it fails while + // flushing the write session (hence logging above). + if err != nil { + return errors.Wrap(err, "kopia deleting backup manifest") + } + + return nil +} diff --git a/src/internal/kopia/wrapper_test.go b/src/internal/kopia/wrapper_test.go index 95d9e0f45..70b388657 100644 --- a/src/internal/kopia/wrapper_test.go +++ b/src/internal/kopia/wrapper_test.go @@ -986,3 +986,53 @@ func (suite *KopiaSimpleRepoIntegrationSuite) TestRestoreMultipleItems_Errors() }) } } + +func (suite *KopiaIntegrationSuite) TestDeleteSnapshot() { + t := suite.T() + + collections := []data.Collection{ + mockconnector.NewMockExchangeCollection( + []string{"a-tenant", "user1", "emails"}, + 5, + ), + mockconnector.NewMockExchangeCollection( + []string{"a-tenant", "user2", "emails"}, + 42, + ), + } + + bs, _, err := suite.w.BackupCollections(suite.ctx, collections) + require.NoError(t, err) + + snapshotID := bs.SnapshotID + assert.NoError(t, suite.w.DeleteSnapshot(suite.ctx, snapshotID)) + + // assert the deletion worked + dirPath := []string{testTenant, testUser} + _, err = suite.w.RestoreDirectory(suite.ctx, snapshotID, dirPath) + assert.Error(t, err, "snapshot should be deleted") +} + +func (suite *KopiaIntegrationSuite) TestDeleteSnapshot_BadIDs() { + table := []struct { + name string + snapshotID string + expect assert.ErrorAssertionFunc + }{ + { + name: "no id", + snapshotID: "", + expect: assert.Error, + }, + { + name: "unknown id", + snapshotID: uuid.NewString(), + expect: assert.NoError, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, suite.w.DeleteSnapshot(suite.ctx, test.snapshotID)) + }) + } +} diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go index 67285d026..5053a9b83 100644 --- a/src/pkg/backup/details/details_test.go +++ b/src/pkg/backup/details/details_test.go @@ -1,47 +1,19 @@ package details_test import ( - "context" "testing" "time" - "github.com/google/uuid" - "github.com/kopia/kopia/repo/manifest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/alcionai/corso/internal/model" - "github.com/alcionai/corso/pkg/backup" "github.com/alcionai/corso/pkg/backup/details" - "github.com/alcionai/corso/pkg/store" - storeMock "github.com/alcionai/corso/pkg/store/mock" ) // ------------------------------------------------------------ // unit tests // ------------------------------------------------------------ -var ( - detailsID = uuid.NewString() - bu = backup.Backup{ - BaseModel: model.BaseModel{ - ID: model.StableID(uuid.NewString()), - ModelStoreID: manifest.ID(uuid.NewString()), - }, - CreationTime: time.Now(), - SnapshotID: uuid.NewString(), - DetailsID: detailsID, - } - deets = details.Details{ - DetailsModel: details.DetailsModel{ - BaseModel: model.BaseModel{ - ID: model.StableID(detailsID), - ModelStoreID: manifest.ID(uuid.NewString()), - }, - }, - } -) - type DetailsUnitSuite struct { suite.Suite } @@ -50,71 +22,6 @@ func TestDetailsUnitSuite(t *testing.T) { suite.Run(t, new(DetailsUnitSuite)) } -func (suite *DetailsUnitSuite) TestGetDetails() { - ctx := context.Background() - - table := []struct { - name string - mock *storeMock.MockModelStore - expect assert.ErrorAssertionFunc - }{ - { - name: "gets details", - mock: storeMock.NewMock(nil, &deets, nil), - expect: assert.NoError, - }, - { - name: "errors", - mock: storeMock.NewMock(nil, &deets, assert.AnError), - expect: assert.Error, - }, - } - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - sm := &store.Wrapper{Storer: test.mock} - result, err := sm.GetDetails(ctx, manifest.ID(uuid.NewString())) - test.expect(t, err) - if err != nil { - return - } - assert.Equal(t, deets.ID, result.ID) - }) - } -} - -func (suite *DetailsUnitSuite) TestGetDetailsFromBackupID() { - ctx := context.Background() - - table := []struct { - name string - mock *storeMock.MockModelStore - expect assert.ErrorAssertionFunc - }{ - { - name: "gets details from backup id", - mock: storeMock.NewMock(&bu, &deets, nil), - expect: assert.NoError, - }, - { - name: "errors", - mock: storeMock.NewMock(&bu, &deets, assert.AnError), - expect: assert.Error, - }, - } - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - store := &store.Wrapper{Storer: test.mock} - dResult, bResult, err := store.GetDetailsFromBackupID(ctx, model.StableID(uuid.NewString())) - test.expect(t, err) - if err != nil { - return - } - assert.Equal(t, deets.ID, dResult.ID) - assert.Equal(t, bu.ID, bResult.ID) - }) - } -} - func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { now := time.Now() nowStr := now.Format(time.RFC3339Nano) diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index 5f1d44a43..caa8244e8 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -178,3 +178,18 @@ func (r Repository) BackupDetails(ctx context.Context, backupID string) (*detail sw := store.NewKopiaStore(r.modelStore) return sw.GetDetailsFromBackupID(ctx, model.StableID(backupID)) } + +// DeleteBackup removes the backup from both the model store and the backup storage. +func (r Repository) DeleteBackup(ctx context.Context, id model.StableID) error { + bu, err := r.Backup(ctx, id) + if err != nil { + return err + } + + if err := r.dataLayer.DeleteSnapshot(ctx, bu.SnapshotID); err != nil { + return err + } + + sw := store.NewKopiaStore(r.modelStore) + return sw.DeleteBackup(ctx, id) +} diff --git a/src/pkg/store/backup.go b/src/pkg/store/backup.go index 3c2366a06..007c06d98 100644 --- a/src/pkg/store/backup.go +++ b/src/pkg/store/backup.go @@ -39,6 +39,18 @@ func (w Wrapper) GetBackups(ctx context.Context) ([]backup.Backup, error) { return bs, nil } +// DeleteBackup deletes the backup and its details entry from the model store. +func (w Wrapper) DeleteBackup(ctx context.Context, backupID model.StableID) error { + deets, _, err := w.GetDetailsFromBackupID(ctx, backupID) + if err != nil { + return err + } + if err := w.Delete(ctx, model.BackupDetailsSchema, deets.ID); err != nil { + return err + } + return w.Delete(ctx, model.BackupSchema, backupID) +} + // GetDetails gets the backup details by ID. func (w Wrapper) GetDetails(ctx context.Context, detailsID manifest.ID) (*details.Details, error) { d := details.Details{} diff --git a/src/pkg/store/backup_test.go b/src/pkg/store/backup_test.go index 5bf0a913c..bcfd76282 100644 --- a/src/pkg/store/backup_test.go +++ b/src/pkg/store/backup_test.go @@ -12,6 +12,7 @@ import ( "github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/store" storeMock "github.com/alcionai/corso/pkg/store/mock" ) @@ -31,6 +32,14 @@ var ( SnapshotID: uuid.NewString(), DetailsID: detailsID, } + deets = details.Details{ + DetailsModel: details.DetailsModel{ + BaseModel: model.BaseModel{ + ID: model.StableID(detailsID), + ModelStoreID: manifest.ID(uuid.NewString()), + }, + }, + } ) type StoreBackupUnitSuite struct { @@ -105,3 +114,96 @@ func (suite *StoreBackupUnitSuite) TestGetBackups() { }) } } + +func (suite *StoreBackupUnitSuite) TestDeleteBackup() { + ctx := context.Background() + + table := []struct { + name string + mock *storeMock.MockModelStore + expect assert.ErrorAssertionFunc + }{ + { + name: "deletes backup", + mock: storeMock.NewMock(&bu, &deets, nil), + expect: assert.NoError, + }, + { + name: "errors", + mock: storeMock.NewMock(&bu, &deets, assert.AnError), + expect: assert.Error, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sm := &store.Wrapper{Storer: test.mock} + err := sm.DeleteBackup(ctx, model.StableID(uuid.NewString())) + test.expect(t, err) + }) + } +} + +func (suite *StoreBackupUnitSuite) TestGetDetails() { + ctx := context.Background() + + table := []struct { + name string + mock *storeMock.MockModelStore + expect assert.ErrorAssertionFunc + }{ + { + name: "gets details", + mock: storeMock.NewMock(nil, &deets, nil), + expect: assert.NoError, + }, + { + name: "errors", + mock: storeMock.NewMock(nil, &deets, assert.AnError), + expect: assert.Error, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sm := &store.Wrapper{Storer: test.mock} + result, err := sm.GetDetails(ctx, manifest.ID(uuid.NewString())) + test.expect(t, err) + if err != nil { + return + } + assert.Equal(t, deets.ID, result.ID) + }) + } +} + +func (suite *StoreBackupUnitSuite) TestGetDetailsFromBackupID() { + ctx := context.Background() + + table := []struct { + name string + mock *storeMock.MockModelStore + expect assert.ErrorAssertionFunc + }{ + { + name: "gets details from backup id", + mock: storeMock.NewMock(&bu, &deets, nil), + expect: assert.NoError, + }, + { + name: "errors", + mock: storeMock.NewMock(&bu, &deets, assert.AnError), + expect: assert.Error, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + store := &store.Wrapper{Storer: test.mock} + dResult, bResult, err := store.GetDetailsFromBackupID(ctx, model.StableID(uuid.NewString())) + test.expect(t, err) + if err != nil { + return + } + assert.Equal(t, deets.ID, dResult.ID) + assert.Equal(t, bu.ID, bResult.ID) + }) + } +}