diff --git a/src/internal/operations/retention_config.go b/src/internal/operations/retention_config.go new file mode 100644 index 000000000..ca8504382 --- /dev/null +++ b/src/internal/operations/retention_config.go @@ -0,0 +1,77 @@ +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" + "github.com/alcionai/corso/src/pkg/count" +) + +// RetentionConfigOperation wraps an operation with restore-specific props. +type RetentionConfigOperation struct { + operation + Results RetentionConfigResults + rcOpts repository.Retention +} + +// RetentionConfigResults aggregate the details of the results of the operation. +type RetentionConfigResults struct { + stats.StartAndEndTime +} + +// NewRetentionConfigOperation constructs and validates an operation to change +// retention parameters. +func NewRetentionConfigOperation( + ctx context.Context, + opts control.Options, + kw *kopia.Wrapper, + rcOpts repository.Retention, + bus events.Eventer, +) (RetentionConfigOperation, error) { + op := RetentionConfigOperation{ + operation: newOperation(opts, bus, count.New(), kw, nil), + rcOpts: rcOpts, + } + + // Don't run validation because we don't populate the model store. + + return op, nil +} + +func (op *RetentionConfigOperation) Run(ctx context.Context) (err error) { + defer func() { + if crErr := crash.Recovery(ctx, recover(), "retention_config"); crErr != nil { + err = crErr + } + }() + + op.Results.StartedAt = time.Now() + + // TODO(ashmrtn): Send telemetry? + + return op.do(ctx) +} + +func (op *RetentionConfigOperation) do(ctx context.Context) error { + defer func() { + op.Results.CompletedAt = time.Now() + }() + + err := op.operation.kopia.SetRetentionParameters(ctx, op.rcOpts) + if err != nil { + op.Status = Failed + return clues.Wrap(err, "running retention config operation") + } + + op.Status = Completed + + return nil +} diff --git a/src/internal/operations/retention_config_test.go b/src/internal/operations/retention_config_test.go new file mode 100644 index 000000000..ce57cd879 --- /dev/null +++ b/src/internal/operations/retention_config_test.go @@ -0,0 +1,74 @@ +package operations + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common/ptr" + 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" + storeTD "github.com/alcionai/corso/src/pkg/storage/testdata" +) + +type RetentionConfigOpIntegrationSuite struct { + tester.Suite +} + +func TestRetentionConfigOpIntegrationSuite(t *testing.T) { + suite.Run(t, &RetentionConfigOpIntegrationSuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{storeTD.AWSStorageCredEnvs}), + }) +} + +func (suite *RetentionConfigOpIntegrationSuite) TestRepoRetentionConfig() { + var ( + t = suite.T() + // need to initialize the repository before we can test connecting to it. + st = storeTD.NewPrefixedS3Storage(t) + k = kopia.NewConn(st) + ) + + ctx, flush := tester.NewContext(t) + defer flush() + + err := k.Initialize(ctx, repository.Options{}, repository.Retention{}) + 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) + + // Only set extend locks parameter as other retention options require a bucket + // with object locking enabled. There's more complete tests in the kopia + // package. + rco, err := NewRetentionConfigOperation( + ctx, + control.DefaultOptions(), + kw, + repository.Retention{ + Extend: ptr.To(true), + }, + evmock.NewBus()) + require.NoError(t, err, clues.ToCore(err)) + + err = rco.Run(ctx) + assert.NoError(t, err, clues.ToCore(err)) + assert.Equal(t, Completed, rco.Status) + assert.NotZero(t, rco.Results.StartedAt) + assert.NotZero(t, rco.Results.CompletedAt) + assert.NotEqual(t, rco.Results.StartedAt, rco.Results.CompletedAt) +} diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index 291ef5fd0..d4b0f6eaa 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -84,6 +84,10 @@ type Repository interface { ctx context.Context, mOpts ctrlRepo.Maintenance, ) (operations.MaintenanceOperation, error) + NewRetentionConfig( + ctx context.Context, + rcOpts ctrlRepo.Retention, + ) (operations.RetentionConfigOperation, error) DeleteBackup(ctx context.Context, id string) error BackupGetter // ConnectToM365 establishes graph api connections @@ -420,6 +424,18 @@ func (r repository) NewMaintenance( r.Bus) } +func (r repository) NewRetentionConfig( + ctx context.Context, + rcOpts ctrlRepo.Retention, +) (operations.RetentionConfigOperation, error) { + return operations.NewRetentionConfigOperation( + ctx, + r.Opts, + r.dataLayer, + rcOpts, + 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))