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.
This commit is contained in:
Keepers 2022-08-24 10:30:27 -06:00 committed by GitHub
parent 15b12e634d
commit db2c1ec8e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 211 additions and 93 deletions

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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{}

View File

@ -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)
})
}
}