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:
parent
e72fa49018
commit
8cca7f12df
@ -520,7 +520,7 @@ func isErrEntryNotFound(err error) bool {
|
|||||||
!strings.Contains(err.Error(), "parent is not a directory")
|
!strings.Contains(err.Error(), "parent is not a directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w Wrapper) Maintenance(
|
func (w Wrapper) RepoMaintenance(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
opts repository.Maintenance,
|
opts repository.Maintenance,
|
||||||
) error {
|
) error {
|
||||||
|
|||||||
@ -179,7 +179,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_FirstRun_NoChanges() {
|
|||||||
Type: repository.MetadataMaintenance,
|
Type: repository.MetadataMaintenance,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = w.Maintenance(ctx, opts)
|
err = w.RepoMaintenance(ctx, opts)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_NoForce_Fails
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This will set the user.
|
// This will set the user.
|
||||||
err = w.Maintenance(ctx, mOpts)
|
err = w.RepoMaintenance(ctx, mOpts)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = k.Close(ctx)
|
err = k.Close(ctx)
|
||||||
@ -216,7 +216,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_NoForce_Fails
|
|||||||
|
|
||||||
var notOwnedErr maintenance.NotOwnedError
|
var notOwnedErr maintenance.NotOwnedError
|
||||||
|
|
||||||
err = w.Maintenance(ctx, mOpts)
|
err = w.RepoMaintenance(ctx, mOpts)
|
||||||
assert.ErrorAs(t, err, ¬OwnedErr, clues.ToCore(err))
|
assert.ErrorAs(t, err, ¬OwnedErr, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_Force_Succeed
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This will set the user.
|
// This will set the user.
|
||||||
err = w.Maintenance(ctx, mOpts)
|
err = w.RepoMaintenance(ctx, mOpts)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = k.Close(ctx)
|
err = k.Close(ctx)
|
||||||
@ -254,13 +254,13 @@ func (suite *BasicKopiaIntegrationSuite) TestMaintenance_WrongUser_Force_Succeed
|
|||||||
mOpts.Force = true
|
mOpts.Force = true
|
||||||
|
|
||||||
// This will set the user.
|
// This will set the user.
|
||||||
err = w.Maintenance(ctx, mOpts)
|
err = w.RepoMaintenance(ctx, mOpts)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
mOpts.Force = false
|
mOpts.Force = false
|
||||||
|
|
||||||
// Running without force should succeed now.
|
// Running without force should succeed now.
|
||||||
err = w.Maintenance(ctx, mOpts)
|
err = w.RepoMaintenance(ctx, mOpts)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"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"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"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/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
@ -65,4 +66,8 @@ type (
|
|||||||
|
|
||||||
Wait() *data.CollectionStats
|
Wait() *data.CollectionStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RepoMaintenancer interface {
|
||||||
|
RepoMaintenance(ctx context.Context, opts repository.Maintenance) error
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
68
src/internal/operations/maintenance.go
Normal file
68
src/internal/operations/maintenance.go
Normal 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
|
||||||
|
}
|
||||||
65
src/internal/operations/maintenance_test.go
Normal file
65
src/internal/operations/maintenance_test.go
Normal 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))
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ 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"
|
||||||
|
rep "github.com/alcionai/corso/src/pkg/control/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
@ -70,6 +71,10 @@ type Repository interface {
|
|||||||
sel selectors.Selector,
|
sel selectors.Selector,
|
||||||
dest control.RestoreDestination,
|
dest control.RestoreDestination,
|
||||||
) (operations.RestoreOperation, error)
|
) (operations.RestoreOperation, error)
|
||||||
|
NewMaintenance(
|
||||||
|
ctx context.Context,
|
||||||
|
mOpts rep.Maintenance,
|
||||||
|
) (operations.MaintenanceOperation, error)
|
||||||
DeleteBackup(ctx context.Context, id string) error
|
DeleteBackup(ctx context.Context, id string) error
|
||||||
BackupGetter
|
BackupGetter
|
||||||
}
|
}
|
||||||
@ -357,6 +362,18 @@ func (r repository) NewRestore(
|
|||||||
r.Bus)
|
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.
|
// Backup retrieves a backup by id.
|
||||||
func (r repository) Backup(ctx context.Context, id string) (*backup.Backup, error) {
|
func (r repository) Backup(ctx context.Context, id string) (*backup.Backup, error) {
|
||||||
return getBackup(ctx, id, store.NewKopiaStore(r.modelStore))
|
return getBackup(ctx, id, store.NewKopiaStore(r.modelStore))
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"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/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
"github.com/alcionai/corso/src/pkg/storage"
|
"github.com/alcionai/corso/src/pkg/storage"
|
||||||
@ -225,6 +226,25 @@ func (suite *RepositoryIntegrationSuite) TestNewRestore() {
|
|||||||
require.NotNil(t, ro)
|
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() {
|
func (suite *RepositoryIntegrationSuite) TestConnect_DisableMetrics() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user