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:
parent
15b12e634d
commit
db2c1ec8e2
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user