diff --git a/src/internal/kopia/conn.go b/src/internal/kopia/conn.go index 0fc1d6c98..fb0687d45 100644 --- a/src/internal/kopia/conn.go +++ b/src/internal/kopia/conn.go @@ -24,6 +24,7 @@ import ( "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/kopia/retention" "github.com/alcionai/corso/src/pkg/control/repository" + "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/storage" ) @@ -38,6 +39,24 @@ const ( var ( ErrSettingDefaultConfig = clues.New("setting default repo config values") ErrorRepoAlreadyExists = clues.New("repo already exists") + + // minEpochDurationLowerBound is the minimum corso will allow the kopia epoch + // duration to be set to. This number can still be tuned further, right now + // it's just to make sure it's not set to something totally wild. + // + // Note that there are still other parameters kopia checks when deciding if + // the epoch should be changed. This is just the min amount of time since the + // last epoch change that's required. + minEpochDurationLowerBound = 2 * time.Hour + + // minEpochDurationUpperBound is the maximum corso will allow the kopia epoch + // duration to be set to. This number can still be tuned further, right now + // it's just to make sure it's not set to something totally wild. + // + // Note that there are still other parameters kopia checks when deciding if + // the epoch should be changed. This is just the min amount of time since the + // last epoch change that's required. + minEpochDurationUpperBound = 7 * 24 * time.Hour ) // Having all fields set to 0 causes it to keep max-int versions of snapshots. @@ -611,3 +630,96 @@ func (w *conn) UpdatePassword( return nil } + +// getPersistentConfig returns the current mutable parameters and blob storage +// config for the repo. It doesn't return the required parameters because it's +// unable to name the type they represent since it's in the internal kopia +// package. +func (w *conn) getPersistentConfig( + ctx context.Context, +) (format.MutableParameters, format.BlobStorageConfiguration, error) { + directRepo, ok := w.Repository.(repo.DirectRepository) + if !ok { + return format.MutableParameters{}, + format.BlobStorageConfiguration{}, + clues.NewWC(ctx, "getting repo handle") + } + + formatManager := directRepo.FormatManager() + + mutableParams, err := formatManager.GetMutableParameters() + if err != nil { + return format.MutableParameters{}, + format.BlobStorageConfiguration{}, + clues.WrapWC(ctx, err, "getting mutable parameters") + } + + blobCfg, err := formatManager.BlobCfgBlob() + if err != nil { + return format.MutableParameters{}, + format.BlobStorageConfiguration{}, + clues.WrapWC(ctx, err, "getting blob config") + } + + return mutableParams, blobCfg, nil +} + +func (w *conn) updatePersistentConfig( + ctx context.Context, + config repository.PersistentConfig, +) error { + directRepo, ok := w.Repository.(repo.DirectRepository) + if !ok { + return clues.NewWC(ctx, "getting repo handle") + } + + formatManager := directRepo.FormatManager() + + // Get current config structs. + mutableParams, blobCfg, err := w.getPersistentConfig(ctx) + if err != nil { + return clues.Stack(err) + } + + reqFeatures, err := formatManager.RequiredFeatures() + if err != nil { + return clues.WrapWC(ctx, err, "getting required features") + } + + // Apply requested updates. + var changed bool + + if d, ok := ptr.ValOK(config.MinEpochDuration); ok { + // Don't let the user set this value too small or too large. + if d < minEpochDurationLowerBound || d > minEpochDurationUpperBound { + return clues.NewWC(ctx, fmt.Sprintf( + "min epoch duration outside allowed limits of [%v, %v]", + minEpochDurationLowerBound, + minEpochDurationUpperBound)) + } + + if d != mutableParams.EpochParameters.MinEpochDuration { + ctx = clues.Add( + ctx, + "old_min_epoch_duration", mutableParams.EpochParameters.MinEpochDuration, + "new_min_epoch_duration", d) + + changed = true + mutableParams.EpochParameters.MinEpochDuration = d + } + } + + // Exit or update config structs if there were changes. + if !changed { + logger.Ctx(ctx).Info("no config parameter changes") + return nil + } + + logger.Ctx(ctx).Info("persisting config parameter changes") + + return clues.WrapWC( + ctx, + formatManager.SetParameters(ctx, mutableParams, blobCfg, reqFeatures), + "persisting updated config"). + OrNil() +} diff --git a/src/internal/kopia/conn_test.go b/src/internal/kopia/conn_test.go index bb629a31f..973f28e2c 100644 --- a/src/internal/kopia/conn_test.go +++ b/src/internal/kopia/conn_test.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/clues" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/format" "github.com/kopia/kopia/snapshot" "github.com/kopia/kopia/snapshot/policy" "github.com/stretchr/testify/assert" @@ -475,6 +476,153 @@ func (suite *WrapperIntegrationSuite) TestSetUserAndHost() { assert.NoError(t, err, clues.ToCore(err)) } +func (suite *WrapperIntegrationSuite) TestUpdatePersistentConfig() { + table := []struct { + name string + opts func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) repository.PersistentConfig + expectErr assert.ErrorAssertionFunc + expectConfig func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) (format.MutableParameters, format.BlobStorageConfiguration) + }{ + { + name: "NoOptionsSet NoChange", + opts: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) repository.PersistentConfig { + return repository.PersistentConfig{} + }, + expectErr: assert.NoError, + expectConfig: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) (format.MutableParameters, format.BlobStorageConfiguration) { + return startParams, startBlobConfig + }, + }, + { + name: "NoValueChange NoChange", + opts: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) repository.PersistentConfig { + return repository.PersistentConfig{ + MinEpochDuration: ptr.To(startParams.EpochParameters.MinEpochDuration), + } + }, + expectErr: assert.NoError, + expectConfig: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) (format.MutableParameters, format.BlobStorageConfiguration) { + return startParams, startBlobConfig + }, + }, + { + name: "MinEpochLessThanLowerBound Errors", + opts: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) repository.PersistentConfig { + return repository.PersistentConfig{ + MinEpochDuration: ptr.To(minEpochDurationLowerBound - time.Second), + } + }, + expectErr: assert.Error, + expectConfig: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) (format.MutableParameters, format.BlobStorageConfiguration) { + return startParams, startBlobConfig + }, + }, + { + name: "MinEpochGreaterThanUpperBound Errors", + opts: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) repository.PersistentConfig { + return repository.PersistentConfig{ + MinEpochDuration: ptr.To(minEpochDurationUpperBound + time.Second), + } + }, + expectErr: assert.Error, + expectConfig: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) (format.MutableParameters, format.BlobStorageConfiguration) { + return startParams, startBlobConfig + }, + }, + { + name: "UpdateMinEpoch Succeeds", + opts: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) repository.PersistentConfig { + return repository.PersistentConfig{ + MinEpochDuration: ptr.To(minEpochDurationLowerBound), + } + }, + expectErr: assert.NoError, + expectConfig: func( + startParams format.MutableParameters, + startBlobConfig format.BlobStorageConfiguration, + ) (format.MutableParameters, format.BlobStorageConfiguration) { + startParams.EpochParameters.MinEpochDuration = minEpochDurationLowerBound + return startParams, startBlobConfig + }, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + repoNameHash := strTD.NewHashForRepoConfigName() + + ctx, flush := tester.NewContext(t) + defer flush() + + st1 := storeTD.NewPrefixedS3Storage(t) + + connection := NewConn(st1) + err := connection.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash) + require.NoError(t, err, "initializing repo: %v", clues.ToCore(err)) + + startParams, startBlobConfig, err := connection.getPersistentConfig(ctx) + require.NoError(t, err, clues.ToCore(err)) + + opts := test.opts(startParams, startBlobConfig) + + err = connection.updatePersistentConfig(ctx, opts) + test.expectErr(t, err, clues.ToCore(err)) + + // Need to close and reopen the repo since the format manager will cache + // the old value for some amount of time. + err = connection.Close(ctx) + require.NoError(t, err, clues.ToCore(err)) + + // Open another connection to the repo to verify params are as expected + // across repo connect calls. + err = connection.Connect(ctx, repository.Options{}, repoNameHash) + require.NoError(t, err, clues.ToCore(err)) + + gotParams, gotBlobConfig, err := connection.getPersistentConfig(ctx) + require.NoError(t, err, clues.ToCore(err)) + + expectParams, expectBlobConfig := test.expectConfig(startParams, startBlobConfig) + + assert.Equal(t, expectParams, gotParams) + assert.Equal(t, expectBlobConfig, gotBlobConfig) + }) + } +} + // --------------- // integration tests that require object locking to be enabled on the bucket. // --------------- diff --git a/src/pkg/control/repository/persistent_config.go b/src/pkg/control/repository/persistent_config.go new file mode 100644 index 000000000..6518e614e --- /dev/null +++ b/src/pkg/control/repository/persistent_config.go @@ -0,0 +1,13 @@ +package repository + +import ( + "time" +) + +// PersistentConfig represents configuration info that is persisted in the corso +// repo and can be updated with repository.UpdatePersistentConfig. Leaving a +// field as nil will result in no updates being made to it (i.e. PATCH +// semantics). +type PersistentConfig struct { + MinEpochDuration *time.Duration +}