From 8cca7f12dfe58549a0728efad856af913919e1e9 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 2 May 2023 11:18:40 -0700 Subject: [PATCH] Create basic maintenance operation (#3225) Add a maintenance operation to run kopia maintenance Using this instead of calling kopia directly will allow us to hook into metrics reporting in a more consistent manner if we want metrics --- #### Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No #### Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Supportability/Tests - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup #### Issue(s) * #3077 #### Test Plan - [ ] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/kopia/wrapper.go | 2 +- src/internal/kopia/wrapper_test.go | 12 ++-- src/internal/operations/inject/inject.go | 5 ++ src/internal/operations/maintenance.go | 68 +++++++++++++++++++++ src/internal/operations/maintenance_test.go | 65 ++++++++++++++++++++ src/pkg/repository/repository.go | 17 ++++++ src/pkg/repository/repository_test.go | 20 ++++++ 7 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 src/internal/operations/maintenance.go create mode 100644 src/internal/operations/maintenance_test.go diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index ff7280d1b..6f7dd69eb 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -520,7 +520,7 @@ func isErrEntryNotFound(err error) bool { !strings.Contains(err.Error(), "parent is not a directory") } -func (w Wrapper) Maintenance( +func (w Wrapper) RepoMaintenance( ctx context.Context, opts repository.Maintenance, ) error { diff --git a/src/internal/kopia/wrapper_test.go b/src/internal/kopia/wrapper_test.go index 89e5b134b..7fdcd2907 100644 --- a/src/internal/kopia/wrapper_test.go +++ b/src/internal/kopia/wrapper_test.go @@ -179,7 +179,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_FirstRun_NoChanges() { Type: repository.MetadataMaintenance, } - err = w.Maintenance(ctx, opts) + err = w.RepoMaintenance(ctx, opts) require.NoError(t, err, clues.ToCore(err)) } @@ -200,7 +200,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_NoForce_Fails } // This will set the user. - err = w.Maintenance(ctx, mOpts) + err = w.RepoMaintenance(ctx, mOpts) require.NoError(t, err, clues.ToCore(err)) err = k.Close(ctx) @@ -216,7 +216,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_NoForce_Fails var notOwnedErr maintenance.NotOwnedError - err = w.Maintenance(ctx, mOpts) + err = w.RepoMaintenance(ctx, mOpts) assert.ErrorAs(t, err, ¬OwnedErr, clues.ToCore(err)) } @@ -237,7 +237,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_Force_Succeed } // This will set the user. - err = w.Maintenance(ctx, mOpts) + err = w.RepoMaintenance(ctx, mOpts) require.NoError(t, err, clues.ToCore(err)) err = k.Close(ctx) @@ -254,13 +254,13 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_Force_Succeed mOpts.Force = true // This will set the user. - err = w.Maintenance(ctx, mOpts) + err = w.RepoMaintenance(ctx, mOpts) require.NoError(t, err, clues.ToCore(err)) mOpts.Force = false // Running without force should succeed now. - err = w.Maintenance(ctx, mOpts) + err = w.RepoMaintenance(ctx, mOpts) require.NoError(t, err, clues.ToCore(err)) } diff --git a/src/internal/operations/inject/inject.go b/src/internal/operations/inject/inject.go index f08674c5a..a85bf08ca 100644 --- a/src/internal/operations/inject/inject.go +++ b/src/internal/operations/inject/inject.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" @@ -65,4 +66,8 @@ type ( Wait() *data.CollectionStats } + + RepoMaintenancer interface { + RepoMaintenance(ctx context.Context, opts repository.Maintenance) error + } ) diff --git a/src/internal/operations/maintenance.go b/src/internal/operations/maintenance.go new file mode 100644 index 000000000..aa2a5bebd --- /dev/null +++ b/src/internal/operations/maintenance.go @@ -0,0 +1,68 @@ +package operations + +import ( + "context" + "time" + + "github.com/alcionai/clues" + + "github.com/alcionai/corso/src/internal/common/crash" + "github.com/alcionai/corso/src/internal/events" + "github.com/alcionai/corso/src/internal/kopia" + "github.com/alcionai/corso/src/internal/stats" + "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/control/repository" +) + +// MaintenanceOperation wraps an operation with restore-specific props. +type MaintenanceOperation struct { + operation + Results MaintenanceResults + mOpts repository.Maintenance +} + +// MaintenanceResults aggregate the details of the results of the operation. +type MaintenanceResults struct { + stats.StartAndEndTime +} + +// NewMaintenanceOperation constructs and validates a maintenance operation. +func NewMaintenanceOperation( + ctx context.Context, + opts control.Options, + kw *kopia.Wrapper, + mOpts repository.Maintenance, + bus events.Eventer, +) (MaintenanceOperation, error) { + op := MaintenanceOperation{ + operation: newOperation(opts, bus, kw, nil), + mOpts: mOpts, + } + + // Don't run validation because we don't populate the model store. + + return op, nil +} + +func (op *MaintenanceOperation) Run(ctx context.Context) (err error) { + defer func() { + if crErr := crash.Recovery(ctx, recover(), "maintenance"); crErr != nil { + err = crErr + } + + // TODO(ashmrtn): Send success/failure usage stat? + + op.Results.CompletedAt = time.Now() + }() + + op.Results.StartedAt = time.Now() + + // TODO(ashmrtn): Send usage statistics? + + err = op.operation.kopia.RepoMaintenance(ctx, op.mOpts) + if err != nil { + return clues.Wrap(err, "running maintenance operation") + } + + return nil +} diff --git a/src/internal/operations/maintenance_test.go b/src/internal/operations/maintenance_test.go new file mode 100644 index 000000000..99791a17b --- /dev/null +++ b/src/internal/operations/maintenance_test.go @@ -0,0 +1,65 @@ +package operations + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + evmock "github.com/alcionai/corso/src/internal/events/mock" + "github.com/alcionai/corso/src/internal/kopia" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/control/repository" +) + +type MaintenanceOpIntegrationSuite struct { + tester.Suite +} + +func TestMaintenanceOpIntegrationSuite(t *testing.T) { + suite.Run(t, &MaintenanceOpIntegrationSuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{tester.AWSStorageCredEnvs, tester.M365AcctCredEnvs}), + }) +} + +func (suite *MaintenanceOpIntegrationSuite) TestRepoMaintenance() { + var ( + t = suite.T() + // need to initialize the repository before we can test connecting to it. + st = tester.NewPrefixedS3Storage(t) + k = kopia.NewConn(st) + ) + + ctx, flush := tester.NewContext() + defer flush() + + err := k.Initialize(ctx, repository.Options{}) + require.NoError(t, err, clues.ToCore(err)) + + kw, err := kopia.NewWrapper(k) + // kopiaRef comes with a count of 1 and Wrapper bumps it again so safe + // to close here. + k.Close(ctx) + + require.NoError(t, err, clues.ToCore(err)) + + defer kw.Close(ctx) + + mo, err := NewMaintenanceOperation( + ctx, + control.Defaults(), + kw, + repository.Maintenance{ + Type: repository.MetadataMaintenance, + }, + evmock.NewBus()) + require.NoError(t, err, clues.ToCore(err)) + + err = mo.Run(ctx) + assert.NoError(t, err, clues.ToCore(err)) +} diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index a374d400b..c3f191b96 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -24,6 +24,7 @@ import ( "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" + rep "github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/selectors" @@ -70,6 +71,10 @@ type Repository interface { sel selectors.Selector, dest control.RestoreDestination, ) (operations.RestoreOperation, error) + NewMaintenance( + ctx context.Context, + mOpts rep.Maintenance, + ) (operations.MaintenanceOperation, error) DeleteBackup(ctx context.Context, id string) error BackupGetter } @@ -357,6 +362,18 @@ func (r repository) NewRestore( r.Bus) } +func (r repository) NewMaintenance( + ctx context.Context, + mOpts rep.Maintenance, +) (operations.MaintenanceOperation, error) { + return operations.NewMaintenanceOperation( + ctx, + r.Opts, + r.dataLayer, + mOpts, + r.Bus) +} + // Backup retrieves a backup by id. func (r repository) Backup(ctx context.Context, id string) (*backup.Backup, error) { return getBackup(ctx, id, store.NewKopiaStore(r.modelStore)) diff --git a/src/pkg/repository/repository_test.go b/src/pkg/repository/repository_test.go index 68053a841..649601142 100644 --- a/src/pkg/repository/repository_test.go +++ b/src/pkg/repository/repository_test.go @@ -11,6 +11,7 @@ import ( "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/control" + rep "github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/storage" @@ -225,6 +226,25 @@ func (suite *RepositoryIntegrationSuite) TestNewRestore() { require.NotNil(t, ro) } +func (suite *RepositoryIntegrationSuite) TestNewMaintenance() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + + acct := tester.NewM365Account(t) + + // need to initialize the repository before we can test connecting to it. + st := tester.NewPrefixedS3Storage(t) + + r, err := repository.Initialize(ctx, acct, st, control.Defaults()) + require.NoError(t, err, clues.ToCore(err)) + + mo, err := r.NewMaintenance(ctx, rep.Maintenance{}) + require.NoError(t, err, clues.ToCore(err)) + require.NotNil(t, mo) +} + func (suite *RepositoryIntegrationSuite) TestConnect_DisableMetrics() { ctx, flush := tester.NewContext() defer flush()