introduce idnamer (#2934)

Adds a common interface: idNamer, which is used
to pass around tuples of an id and a name for some
resource.  Also adds compliance to this iface in
selectors, where a selector's ID and Name are the
DiscreteOwner values.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🌻 Feature

#### Issue(s)

* #2825

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-04-03 16:42:48 -06:00 committed by GitHub
parent 9a8ec2505f
commit 99774e754b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 140 additions and 42 deletions

View File

@ -0,0 +1,9 @@
package common
type IDNamer interface {
// the canonical id of the thing, generated and usable
// by whichever system has ownership of it.
ID() string
// the human-readable name of the thing.
Name() string
}

View File

@ -6,6 +6,7 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/discovery/api"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
@ -34,7 +35,7 @@ import (
// prior history (ie, incrementals) and run a full backup. // prior history (ie, incrementals) and run a full backup.
func (gc *GraphConnector) ProduceBackupCollections( func (gc *GraphConnector) ProduceBackupCollections(
ctx context.Context, ctx context.Context,
ownerID, ownerName string, owner common.IDNamer,
sels selectors.Selector, sels selectors.Selector,
metadata []data.RestoreCollection, metadata []data.RestoreCollection,
ctrlOpts control.Options, ctrlOpts control.Options,

View File

@ -208,7 +208,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestDataCollections_invali
collections, excludes, err := connector.ProduceBackupCollections( collections, excludes, err := connector.ProduceBackupCollections(
ctx, ctx,
owners[0], owners[0], test.getSelector(t),
test.getSelector(t), test.getSelector(t),
nil, nil,
control.Options{}, control.Options{},
@ -340,7 +340,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar
cols, excludes, err := gc.ProduceBackupCollections( cols, excludes, err := gc.ProduceBackupCollections(
ctx, ctx,
siteIDs[0], siteIDs[0], sel.Selector,
sel.Selector, sel.Selector,
nil, nil,
control.Options{}, control.Options{},
@ -379,7 +379,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar
cols, excludes, err := gc.ProduceBackupCollections( cols, excludes, err := gc.ProduceBackupCollections(
ctx, ctx,
siteIDs[0], siteIDs[0], sel.Selector,
sel.Selector, sel.Selector,
nil, nil,
control.Options{}, control.Options{},

View File

@ -459,8 +459,7 @@ func runBackupAndCompare(
start := time.Now() start := time.Now()
dcs, excludes, err := backupGC.ProduceBackupCollections( dcs, excludes, err := backupGC.ProduceBackupCollections(
ctx, ctx,
backupSel.DiscreteOwner, backupSel,
backupSel.DiscreteOwner,
backupSel, backupSel,
nil, nil,
config.opts, config.opts,
@ -1000,8 +999,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames
dcs, excludes, err := backupGC.ProduceBackupCollections( dcs, excludes, err := backupGC.ProduceBackupCollections(
ctx, ctx,
backupSel.DiscreteOwner, backupSel,
backupSel.DiscreteOwner,
backupSel, backupSel,
nil, nil,
control.Options{ control.Options{
@ -1154,8 +1152,7 @@ func (suite *GraphConnectorIntegrationSuite) TestBackup_CreatesPrefixCollections
dcs, excludes, err := backupGC.ProduceBackupCollections( dcs, excludes, err := backupGC.ProduceBackupCollections(
ctx, ctx,
backupSel.DiscreteOwner, backupSel,
backupSel.DiscreteOwner,
backupSel, backupSel,
nil, nil,
control.Options{ control.Options{

View File

@ -3,6 +3,7 @@ package mockconnector
import ( import (
"context" "context"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
@ -24,7 +25,7 @@ type GraphConnector struct {
func (gc GraphConnector) ProduceBackupCollections( func (gc GraphConnector) ProduceBackupCollections(
_ context.Context, _ context.Context,
_, _ string, _ common.IDNamer,
_ selectors.Selector, _ selectors.Selector,
_ []data.RestoreCollection, _ []data.RestoreCollection,
_ control.Options, _ control.Options,

View File

@ -33,8 +33,7 @@ import (
type BackupOperation struct { type BackupOperation struct {
operation operation
ResourceOwner string `json:"resourceOwner"` ResourceOwner common.IDNamer
ResourceOwnerName string `json:"resourceOwnerName"`
Results BackupResults `json:"results"` Results BackupResults `json:"results"`
Selectors selectors.Selector `json:"selectors"` Selectors selectors.Selector `json:"selectors"`
@ -63,22 +62,17 @@ func NewBackupOperation(
bp inject.BackupProducer, bp inject.BackupProducer,
acct account.Account, acct account.Account,
selector selectors.Selector, selector selectors.Selector,
ownerName string, owner common.IDNamer,
bus events.Eventer, bus events.Eventer,
) (BackupOperation, error) { ) (BackupOperation, error) {
op := BackupOperation{ op := BackupOperation{
operation: newOperation(opts, bus, kw, sw), operation: newOperation(opts, bus, kw, sw),
ResourceOwner: selector.DiscreteOwner, ResourceOwner: owner,
ResourceOwnerName: ownerName, Selectors: selector,
Selectors: selector, Version: "v0",
Version: "v0", account: acct,
account: acct, incremental: useIncrementalBackup(selector, opts),
incremental: useIncrementalBackup(selector, opts), bp: bp,
bp: bp,
}
if len(ownerName) == 0 {
op.ResourceOwnerName = op.ResourceOwner
} }
if err := op.validate(); err != nil { if err := op.validate(); err != nil {
@ -89,10 +83,14 @@ func NewBackupOperation(
} }
func (op BackupOperation) validate() error { func (op BackupOperation) validate() error {
if len(op.ResourceOwner) == 0 { if op.ResourceOwner == nil {
return clues.New("backup requires a resource owner") return clues.New("backup requires a resource owner")
} }
if len(op.ResourceOwner.ID()) == 0 {
return clues.New("backup requires a resource owner with a populated ID")
}
if op.bp == nil { if op.bp == nil {
return clues.New("missing backup producer") return clues.New("missing backup producer")
} }
@ -165,7 +163,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
// Execution // Execution
// ----- // -----
observe.Message(ctx, "Backing Up", observe.Bullet, clues.Hide(op.ResourceOwner)) observe.Message(ctx, "Backing Up", observe.Bullet, clues.Hide(op.ResourceOwner.Name()))
deets, err := op.do( deets, err := op.do(
ctx, ctx,
@ -253,7 +251,6 @@ func (op *BackupOperation) do(
ctx, ctx,
op.bp, op.bp,
op.ResourceOwner, op.ResourceOwner,
op.ResourceOwnerName,
op.Selectors, op.Selectors,
mdColls, mdColls,
op.Options, op.Options,
@ -327,7 +324,7 @@ func useIncrementalBackup(sel selectors.Selector, opts control.Options) bool {
func produceBackupDataCollections( func produceBackupDataCollections(
ctx context.Context, ctx context.Context,
bp inject.BackupProducer, bp inject.BackupProducer,
ownerID, ownerName string, resourceOwner common.IDNamer,
sel selectors.Selector, sel selectors.Selector,
metadata []data.RestoreCollection, metadata []data.RestoreCollection,
ctrlOpts control.Options, ctrlOpts control.Options,
@ -340,7 +337,7 @@ func produceBackupDataCollections(
closer() closer()
}() }()
return bp.ProduceBackupCollections(ctx, ownerID, ownerName, sel, metadata, ctrlOpts, errs) return bp.ProduceBackupCollections(ctx, resourceOwner, sel, metadata, ctrlOpts, errs)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -717,8 +714,8 @@ func (op *BackupOperation) createBackupModels(
op.Status.String(), op.Status.String(),
backupID, backupID,
op.Selectors, op.Selectors,
op.ResourceOwner, op.ResourceOwner.ID(),
op.ResourceOwnerName, op.ResourceOwner.Name(),
op.Results.ReadWrites, op.Results.ReadWrites,
op.Results.StartAndEndTime, op.Results.StartAndEndTime,
op.Errors.Errors()) op.Errors.Errors())

View File

@ -154,7 +154,7 @@ func newTestBackupOp(
opts.ToggleFeatures = featureToggles opts.ToggleFeatures = featureToggles
bo, err := NewBackupOperation(ctx, opts, kw, sw, gc, acct, sel, sel.DiscreteOwner, bus) bo, err := NewBackupOperation(ctx, opts, kw, sw, gc, acct, sel, sel, bus)
if !assert.NoError(t, err, clues.ToCore(err)) { if !assert.NoError(t, err, clues.ToCore(err)) {
closer() closer()
t.FailNow() t.FailNow()
@ -566,6 +566,8 @@ func (suite *BackupOpIntegrationSuite) TestNewBackupOperation() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
sel := selectors.Selector{DiscreteOwner: "test"}
_, err := NewBackupOperation( _, err := NewBackupOperation(
ctx, ctx,
test.opts, test.opts,
@ -573,8 +575,8 @@ func (suite *BackupOpIntegrationSuite) TestNewBackupOperation() {
test.sw, test.sw,
test.bp, test.bp,
test.acct, test.acct,
selectors.Selector{DiscreteOwner: "test"}, sel,
"test-name", sel,
evmock.NewBus()) evmock.NewBus())
test.errCheck(suite.T(), err, clues.ToCore(err)) test.errCheck(suite.T(), err, clues.ToCore(err))
}) })

View File

@ -415,7 +415,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_PersistResults() {
gc, gc,
acct, acct,
sel, sel,
sel.DiscreteOwner, sel,
evmock.NewBus()) evmock.NewBus())
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))

View File

@ -3,6 +3,7 @@ package inject
import ( import (
"context" "context"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/kopia" "github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -17,7 +18,7 @@ type (
BackupProducer interface { BackupProducer interface {
ProduceBackupCollections( ProduceBackupCollections(
ctx context.Context, ctx context.Context,
ownerID, ownerName string, resourceOwner common.IDNamer,
sels selectors.Selector, sels selectors.Selector,
metadata []data.RestoreCollection, metadata []data.RestoreCollection,
ctrlOpts control.Options, ctrlOpts control.Options,

View File

@ -293,7 +293,7 @@ func setupExchangeBackup(
gc, gc,
acct, acct,
bsel.Selector, bsel.Selector,
bsel.Selector.DiscreteOwner, bsel.Selector,
evmock.NewBus()) evmock.NewBus())
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -350,7 +350,7 @@ func setupSharePointBackup(
gc, gc,
acct, acct,
spsel.Selector, spsel.Selector,
spsel.Selector.DiscreteOwner, spsel.Selector,
evmock.NewBus()) evmock.NewBus())
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))

View File

@ -296,6 +296,9 @@ func (r repository) NewBackup(
return operations.BackupOperation{}, errors.Wrap(err, "connecting to m365") return operations.BackupOperation{}, errors.Wrap(err, "connecting to m365")
} }
// TODO: retrieve display name from gc
sel = sel.SetDiscreteOwnerIDName(sel.DiscreteOwner, "")
return operations.NewBackupOperation( return operations.NewBackupOperation(
ctx, ctx,
r.Opts, r.Opts,
@ -304,7 +307,7 @@ func (r repository) NewBackup(
gc, gc,
r.Account, r.Account,
sel, sel,
sel.DiscreteOwner, sel,
r.Bus) r.Bus)
} }

View File

@ -112,6 +112,8 @@ type Selector struct {
// iterate over the results, where each one will populate this field // iterate over the results, where each one will populate this field
// with a different owner. // with a different owner.
DiscreteOwner string `json:"discreteOwner,omitempty"` DiscreteOwner string `json:"discreteOwner,omitempty"`
// display name for the DiscreteOwner.
DiscreteOwnerName string `json:"discreteOwnerName,omitempty"`
// A slice of exclusion scopes. Exclusions apply globally to all // A slice of exclusion scopes. Exclusions apply globally to all
// inclusions/filters, with any-match behavior. // inclusions/filters, with any-match behavior.
@ -146,6 +148,48 @@ func (s Selector) DiscreteResourceOwners() []string {
return split(s.ResourceOwners.Target) return split(s.ResourceOwners.Target)
} }
// SetDiscreteOwnerIDName ensures the selector has the correct discrete owner
// id and name. Assumes that these values are sourced using the current
// s.DiscreteOwner as input. The reason for taking in both the id and name, and
// not just the name, is so that constructors can input owner aliases in place
// of ids, with the expectation that the two will get sorted and re-written
// later on with this setter.
//
// If the id is empty, the original DiscreteOwner value is retained.
// If the name is empty, the id is duplicated as the name.
func (s Selector) SetDiscreteOwnerIDName(id, name string) Selector {
r := s
if len(id) == 0 {
// assume a the discreteOwner is already set, and don't replace anything.
id = s.DiscreteOwner
}
r.DiscreteOwner = id
r.DiscreteOwnerName = name
if len(name) == 0 {
r.DiscreteOwnerName = id
}
return r
}
// ID returns s.discreteOwner, which is assumed to be a stable ID.
func (s Selector) ID() string {
return s.DiscreteOwner
}
// Name returns s.discreteOwnerName. If that value is empty, it returns
// s.DiscreteOwner instead.
func (s Selector) Name() string {
if len(s.DiscreteOwnerName) == 0 {
return s.DiscreteOwner
}
return s.DiscreteOwnerName
}
// isAnyResourceOwner returns true if the selector includes all resource owners. // isAnyResourceOwner returns true if the selector includes all resource owners.
func isAnyResourceOwner(s Selector) bool { func isAnyResourceOwner(s Selector) bool {
return s.ResourceOwners.Comparator == filters.Passes return s.ResourceOwners.Comparator == filters.Passes
@ -336,7 +380,7 @@ func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// scope helpers // scope constructors
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type scopeConfig struct { type scopeConfig struct {

View File

@ -248,6 +248,49 @@ func (suite *SelectorSuite) TestSplitByResourceOnwer() {
} }
} }
func (suite *SelectorSuite) TestIDName() {
table := []struct {
title string
id, name string
expectID, expectName string
}{
{"empty", "", "", "", ""},
{"only id", "id", "", "id", "id"},
{"only name", "", "name", "", "name"},
{"both", "id", "name", "id", "name"},
}
for _, test := range table {
suite.Run(test.title, func() {
sel := Selector{DiscreteOwner: test.id, DiscreteOwnerName: test.name}
assert.Equal(suite.T(), test.expectID, sel.ID())
assert.Equal(suite.T(), test.expectName, sel.Name())
})
}
}
func (suite *SelectorSuite) TestSetDiscreteOwnerIDName() {
table := []struct {
title string
initID, initName string
id, name string
expectID, expectName string
}{
{"empty", "", "", "", "", "", ""},
{"only id", "", "", "id", "", "id", "id"},
{"only name", "", "", "", "", "", ""},
{"both", "", "", "id", "name", "id", "name"},
{"both", "init-id", "", "", "name", "init-id", "name"},
}
for _, test := range table {
suite.Run(test.title, func() {
sel := Selector{DiscreteOwner: test.initID, DiscreteOwnerName: test.initName}
sel = sel.SetDiscreteOwnerIDName(test.id, test.name)
assert.Equal(suite.T(), test.expectID, sel.ID())
assert.Equal(suite.T(), test.expectName, sel.Name())
})
}
}
// TestPathCategories verifies that no scope produces a `path.UnknownCategory` // TestPathCategories verifies that no scope produces a `path.UnknownCategory`
func (suite *SelectorSuite) TestPathCategories_includes() { func (suite *SelectorSuite) TestPathCategories_includes() {
users := []string{"someuser@onmicrosoft.com"} users := []string{"someuser@onmicrosoft.com"}