persist service tag in backup model, list by service (#1233)

## Description

Adds a tag describing the service within the backup model and the backup details model.  Adds a filter builder pattern to the store wrapper for filtering results according to certain tags.  Finally, adds a filter builder to specify the service type when listing backups.

## Type of change

- [x] 🐛 Bugfix

## Issue(s)

* #1226

## Test Plan

- [x] 💪 Manual
- [x]  Unit test
This commit is contained in:
Keepers 2022-10-20 19:04:54 -06:00 committed by GitHub
parent e82cdadc62
commit 286a74e819
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 92 additions and 18 deletions

View File

@ -16,8 +16,10 @@ import (
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/store"
) )
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@ -371,7 +373,7 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error {
return nil return nil
} }
bs, err := r.Backups(ctx) bs, err := r.Backups(ctx, store.Service(path.ExchangeService))
if err != nil { if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository")) return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository"))
} }

View File

@ -15,8 +15,10 @@ import (
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/store"
) )
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@ -270,7 +272,7 @@ func listOneDriveCmd(cmd *cobra.Command, args []string) error {
return nil return nil
} }
bs, err := r.Backups(ctx) bs, err := r.Backups(ctx, store.Service(path.OneDriveService))
if err != nil { if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository")) return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository"))
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/selectors/testdata" "github.com/alcionai/corso/src/pkg/selectors/testdata"
"github.com/alcionai/corso/src/pkg/store"
) )
type ExchangeOptionsTest struct { type ExchangeOptionsTest struct {
@ -407,7 +408,7 @@ func (MockBackupGetter) Backup(
return nil, errors.New("unexpected call to mock") return nil, errors.New("unexpected call to mock")
} }
func (MockBackupGetter) Backups(context.Context) ([]backup.Backup, error) { func (MockBackupGetter) Backups(context.Context, ...store.FilterOption) ([]backup.Backup, error) {
return nil, errors.New("unexpected call to mock") return nil, errors.New("unexpected call to mock")
} }

View File

@ -20,6 +20,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/stats" "github.com/alcionai/corso/src/internal/stats"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
@ -472,6 +473,7 @@ func inflateDirTree(
func (w Wrapper) BackupCollections( func (w Wrapper) BackupCollections(
ctx context.Context, ctx context.Context,
collections []data.Collection, collections []data.Collection,
service path.ServiceType,
) (*BackupStats, *details.Details, error) { ) (*BackupStats, *details.Details, error) {
if w.c == nil { if w.c == nil {
return nil, nil, errNotConnected return nil, nil, errNotConnected
@ -488,6 +490,10 @@ func (w Wrapper) BackupCollections(
deets: &details.Details{}, deets: &details.Details{},
} }
progress.deets.Tags = map[string]string{
model.ServiceTag: service.String(),
}
dirTree, err := inflateDirTree(ctx, collections, progress) dirTree, err := inflateDirTree(ctx, collections, progress)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "building kopia directories") return nil, nil, errors.Wrap(err, "building kopia directories")

View File

@ -20,6 +20,7 @@ import (
"github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/data" "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/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
@ -810,15 +811,16 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections() {
), ),
} }
stats, rp, err := suite.w.BackupCollections(suite.ctx, collections) stats, deets, err := suite.w.BackupCollections(suite.ctx, collections, path.ExchangeService)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, stats.TotalFileCount, 47) assert.Equal(t, stats.TotalFileCount, 47)
assert.Equal(t, stats.TotalDirectoryCount, 6) assert.Equal(t, stats.TotalDirectoryCount, 6)
assert.Equal(t, stats.IgnoredErrorCount, 0) assert.Equal(t, stats.IgnoredErrorCount, 0)
assert.Equal(t, stats.ErrorCount, 0) assert.Equal(t, stats.ErrorCount, 0)
assert.False(t, stats.Incomplete) assert.False(t, stats.Incomplete)
assert.Equal(t, path.ExchangeService.String(), deets.Tags[model.ServiceTag])
// 47 file and 6 folder entries. // 47 file and 6 folder entries.
assert.Len(t, rp.Entries, 47+6) assert.Len(t, deets.Entries, 47+6)
} }
func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() { func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() {
@ -843,8 +845,9 @@ func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() {
fp2, err := suite.testPath2.Append(dc2.Names[0], true) fp2, err := suite.testPath2.Append(dc2.Names[0], true)
require.NoError(t, err) require.NoError(t, err)
stats, _, err := w.BackupCollections(ctx, []data.Collection{dc1, dc2}) stats, deets, err := w.BackupCollections(ctx, []data.Collection{dc1, dc2}, path.ExchangeService)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, path.ExchangeService.String(), deets.Tags[model.ServiceTag])
require.NoError(t, k.Compression(ctx, "gzip")) require.NoError(t, k.Compression(ctx, "gzip"))
@ -908,7 +911,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
}, },
} }
stats, rp, err := suite.w.BackupCollections(suite.ctx, collections) stats, deets, err := suite.w.BackupCollections(suite.ctx, collections, path.ExchangeService)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, stats.ErrorCount) assert.Equal(t, 0, stats.ErrorCount)
@ -916,8 +919,9 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
assert.Equal(t, 6, stats.TotalDirectoryCount) assert.Equal(t, 6, stats.TotalDirectoryCount)
assert.Equal(t, 1, stats.IgnoredErrorCount) assert.Equal(t, 1, stats.IgnoredErrorCount)
assert.False(t, stats.Incomplete) assert.False(t, stats.Incomplete)
assert.Equal(t, path.ExchangeService.String(), deets.Tags[model.ServiceTag])
// 5 file and 6 folder entries. // 5 file and 6 folder entries.
assert.Len(t, rp.Entries, 5+6) assert.Len(t, deets.Entries, 5+6)
} }
type backedupFile struct { type backedupFile struct {
@ -946,11 +950,13 @@ func (suite *KopiaIntegrationSuite) TestBackupCollectionsHandlesNoCollections()
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
s, d, err := suite.w.BackupCollections(ctx, test.collections) s, d, err := suite.w.BackupCollections(ctx, test.collections, path.UnknownService)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, BackupStats{}, *s) assert.Equal(t, BackupStats{}, *s)
assert.Empty(t, d.Entries) assert.Empty(t, d.Entries)
// unknownService resolves to an empty string here.
assert.Equal(t, "", d.Tags[model.ServiceTag])
}) })
} }
} }
@ -1090,15 +1096,16 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() {
collections = append(collections, collection) collections = append(collections, collection)
} }
stats, rp, err := suite.w.BackupCollections(suite.ctx, collections) stats, deets, err := suite.w.BackupCollections(suite.ctx, collections, path.ExchangeService)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, stats.ErrorCount, 0) require.Equal(t, stats.ErrorCount, 0)
require.Equal(t, stats.TotalFileCount, expectedFiles) require.Equal(t, stats.TotalFileCount, expectedFiles)
require.Equal(t, stats.TotalDirectoryCount, expectedDirs) require.Equal(t, stats.TotalDirectoryCount, expectedDirs)
require.Equal(t, stats.IgnoredErrorCount, 0) require.Equal(t, stats.IgnoredErrorCount, 0)
require.False(t, stats.Incomplete) require.False(t, stats.Incomplete)
assert.Equal(t, path.ExchangeService.String(), deets.Tags[model.ServiceTag])
// 6 file and 6 folder entries. // 6 file and 6 folder entries.
assert.Len(t, rp.Entries, expectedFiles+expectedDirs) assert.Len(t, deets.Entries, expectedFiles+expectedDirs)
suite.snapshotID = manifest.ID(stats.SnapshotID) suite.snapshotID = manifest.ID(stats.SnapshotID)
} }

View File

@ -20,6 +20,11 @@ const (
BackupDetailsSchema BackupDetailsSchema
) )
// common tags for filtering
const (
ServiceTag = "service"
)
// Valid returns true if the ModelType value fits within the iota range. // Valid returns true if the ModelType value fits within the iota range.
func (mt Schema) Valid() bool { func (mt Schema) Valid() bool {
return mt > 0 && mt < BackupDetailsSchema+1 return mt > 0 && mt < BackupDetailsSchema+1

View File

@ -142,7 +142,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
opStats.resourceCount = len(data.ResourceOwnerSet(cs)) opStats.resourceCount = len(data.ResourceOwnerSet(cs))
// hand the results to the consumer // hand the results to the consumer
opStats.k, backupDetails, err = op.kopia.BackupCollections(ctx, cs) opStats.k, backupDetails, err = op.kopia.BackupCollections(ctx, cs, op.Selectors.PathService())
if err != nil { if err != nil {
err = errors.Wrap(err, "backing up service data") err = errors.Wrap(err, "backing up service data")
opStats.writeErr = err opStats.writeErr = err
@ -228,7 +228,7 @@ func (op *BackupOperation) createBackupModels(
events.Duration: op.Results.CompletedAt.Sub(op.Results.StartedAt), events.Duration: op.Results.CompletedAt.Sub(op.Results.StartedAt),
events.EndTime: op.Results.CompletedAt, events.EndTime: op.Results.CompletedAt,
events.Resources: op.Results.ResourceOwners, events.Resources: op.Results.ResourceOwners,
events.Service: op.Selectors.Service.String(), events.Service: op.Selectors.PathService().String(),
events.StartTime: op.Results.StartedAt, events.StartTime: op.Results.StartedAt,
events.Status: op.Status, events.Status: op.Status,
}, },

View File

@ -50,6 +50,9 @@ func New(
return &Backup{ return &Backup{
BaseModel: model.BaseModel{ BaseModel: model.BaseModel{
ID: id, ID: id,
Tags: map[string]string{
model.ServiceTag: selector.PathService().String(),
},
}, },
CreationTime: time.Now(), CreationTime: time.Now(),
SnapshotID: snapshotID, SnapshotID: snapshotID,

View File

@ -31,6 +31,9 @@ func stubBackup(t time.Time) backup.Backup {
return backup.Backup{ return backup.Backup{
BaseModel: model.BaseModel{ BaseModel: model.BaseModel{
ID: model.StableID("id"), ID: model.StableID("id"),
Tags: map[string]string{
model.ServiceTag: sel.PathService().String(),
},
}, },
CreationTime: t, CreationTime: t,
SnapshotID: "snapshot", SnapshotID: "snapshot",

View File

@ -24,7 +24,7 @@ import (
// repository. // repository.
type BackupGetter interface { type BackupGetter interface {
Backup(ctx context.Context, id model.StableID) (*backup.Backup, error) Backup(ctx context.Context, id model.StableID) (*backup.Backup, error)
Backups(ctx context.Context) ([]backup.Backup, error) Backups(ctx context.Context, fs ...store.FilterOption) ([]backup.Backup, error)
BackupDetails( BackupDetails(
ctx context.Context, ctx context.Context,
backupID string, backupID string,
@ -214,9 +214,9 @@ func (r repository) Backup(ctx context.Context, id model.StableID) (*backup.Back
} }
// backups lists backups in a repository // backups lists backups in a repository
func (r repository) Backups(ctx context.Context) ([]backup.Backup, error) { func (r repository) Backups(ctx context.Context, fs ...store.FilterOption) ([]backup.Backup, error) {
sw := store.NewKopiaStore(r.modelStore) sw := store.NewKopiaStore(r.modelStore)
return sw.GetBackups(ctx) return sw.GetBackups(ctx, fs...)
} }
// BackupDetails returns the specified backup details object // BackupDetails returns the specified backup details object

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path"
) )
type service int type service int
@ -21,6 +22,12 @@ const (
ServiceOneDrive // OneDrive ServiceOneDrive // OneDrive
) )
var serviceToPathType = map[service]path.ServiceType{
ServiceUnknown: path.UnknownService,
ServiceExchange: path.ExchangeService,
ServiceOneDrive: path.OneDriveService,
}
var ( var (
ErrorBadSelectorCast = errors.New("wrong selector service type") ErrorBadSelectorCast = errors.New("wrong selector service type")
ErrorNoMatchingItems = errors.New("no items match the specified selectors") ErrorNoMatchingItems = errors.New("no items match the specified selectors")
@ -178,6 +185,11 @@ func discreteScopes[T scopeT, C categoryT](
return sl return sl
} }
// Returns the path.ServiceType matching the selector service.
func (s Selector) PathService() path.ServiceType {
return serviceToPathType[s.Service]
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Printing Selectors for Human Reading // Printing Selectors for Human Reading
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -9,8 +9,35 @@ import (
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/path"
) )
type queryFilters struct {
tags map[string]string
}
type FilterOption func(*queryFilters)
func (q *queryFilters) populate(qf ...FilterOption) {
if len(qf) == 0 {
return
}
q.tags = map[string]string{}
for _, fn := range qf {
fn(q)
}
}
// Service ensures the retrieved backups only match
// the specified service.
func Service(pst path.ServiceType) FilterOption {
return func(qf *queryFilters) {
qf.tags[model.ServiceTag] = pst.String()
}
}
// GetBackup gets a single backup by id. // GetBackup gets a single backup by id.
func (w Wrapper) GetBackup(ctx context.Context, backupID model.StableID) (*backup.Backup, error) { func (w Wrapper) GetBackup(ctx context.Context, backupID model.StableID) (*backup.Backup, error) {
b := backup.Backup{} b := backup.Backup{}
@ -24,8 +51,14 @@ func (w Wrapper) GetBackup(ctx context.Context, backupID model.StableID) (*backu
} }
// GetDetailsFromBackupID retrieves all backups in the model store. // GetDetailsFromBackupID retrieves all backups in the model store.
func (w Wrapper) GetBackups(ctx context.Context) ([]backup.Backup, error) { func (w Wrapper) GetBackups(
bms, err := w.GetIDsForType(ctx, model.BackupSchema, nil) ctx context.Context,
filters ...FilterOption,
) ([]backup.Backup, error) {
q := &queryFilters{}
q.populate(filters...)
bms, err := w.GetIDsForType(ctx, model.BackupSchema, q.tags)
if err != nil { if err != nil {
return nil, err return nil, err
} }