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?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #3077

#### Test Plan

- [ ] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-05-02 11:18:40 -07:00 committed by GitHub
parent e72fa49018
commit 8cca7f12df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 7 deletions

View File

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

View File

@ -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, &notOwnedErr, 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))
}

View File

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

View File

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

View File

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

View File

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

View File

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