diff --git a/cli/command_repository_set_parameters.go b/cli/command_repository_set_parameters.go index 82ee55df0..5d0d4f595 100644 --- a/cli/command_repository_set_parameters.go +++ b/cli/command_repository_set_parameters.go @@ -157,6 +157,7 @@ func (c *commandRepositorySetParameters) run(ctx context.Context, rep repo.Direc c.setSizeMBParameter(ctx, c.maxPackSizeMB, "maximum pack size", &mp.MaxPackSize, &anyChange) + // prevent downgrade of index format if c.indexFormatVersion > mp.IndexVersion { c.setIntParameter(ctx, c.indexFormatVersion, "index format version", &mp.IndexVersion, &anyChange) } else if c.indexFormatVersion != mp.IndexVersion { diff --git a/cli/command_repository_set_parameters_test.go b/cli/command_repository_set_parameters_test.go index 136785ac9..21e23ef10 100644 --- a/cli/command_repository_set_parameters_test.go +++ b/cli/command_repository_set_parameters_test.go @@ -182,6 +182,71 @@ func (s *formatSpecificTestSuite) TestRepositorySetParametersUpgrade(t *testing. env.RunAndExpectSuccess(t, "index", "epoch", "list") } +func (s *formatSpecificTestSuite) TestRepositorySetParametersDowngrade(t *testing.T) { + env := s.setupInMemoryRepo(t) + out := env.RunAndExpectSuccess(t, "repository", "status") + + // default values + require.Contains(t, out, "Max pack length: 21 MB") + + switch s.formatVersion { + case format.FormatVersion1: + require.Contains(t, out, "Format version: 1") + require.Contains(t, out, "Epoch Manager: disabled") + env.RunAndExpectFailure(t, "index", "epoch", "list") + case format.FormatVersion2: + require.Contains(t, out, "Format version: 2") + require.Contains(t, out, "Epoch Manager: enabled") + env.RunAndExpectSuccess(t, "index", "epoch", "list") + default: + require.Contains(t, out, "Format version: 3") + require.Contains(t, out, "Epoch Manager: enabled") + env.RunAndExpectSuccess(t, "index", "epoch", "list") + } + + env.Environment["KOPIA_UPGRADE_LOCK_ENABLED"] = "1" + + { + cmd := []string{ + "repository", "upgrade", + "--upgrade-owner-id", "owner", + "--io-drain-timeout", "1s", "--allow-unsafe-upgrade", + "--status-poll-interval", "1s", + "--max-permitted-clock-drift", "1s", + } + + // You can only upgrade when you are not already upgraded + if s.formatVersion < format.MaxFormatVersion { + env.RunAndExpectSuccess(t, cmd...) + } else { + env.RunAndExpectFailure(t, cmd...) + } + } + + env.RunAndExpectSuccess(t, "repository", "set-parameters", "--upgrade") + env.RunAndExpectSuccess(t, "repository", "set-parameters", "--epoch-min-duration", "3h") + env.RunAndExpectSuccess(t, "repository", "set-parameters", "--epoch-cleanup-safety-margin", "23h") + env.RunAndExpectSuccess(t, "repository", "set-parameters", "--epoch-advance-on-size-mb", "77") + env.RunAndExpectSuccess(t, "repository", "set-parameters", "--epoch-advance-on-count", "22") + env.RunAndExpectSuccess(t, "repository", "set-parameters", "--epoch-checkpoint-frequency", "9") + + env.RunAndExpectFailure(t, "repository", "set-parameters", "--epoch-min-duration", "1s") + env.RunAndExpectFailure(t, "repository", "set-parameters", "--epoch-refresh-frequency", "10h") + env.RunAndExpectFailure(t, "repository", "set-parameters", "--epoch-checkpoint-frequency", "-10") + env.RunAndExpectFailure(t, "repository", "set-parameters", "--epoch-cleanup-safety-margin", "10s") + env.RunAndExpectFailure(t, "repository", "set-parameters", "--epoch-advance-on-count", "1") + + out = env.RunAndExpectSuccess(t, "repository", "status") + require.Contains(t, out, "Epoch Manager: enabled") + require.Contains(t, out, "Index Format: v2") + require.Contains(t, out, "Format version: 3") + require.Contains(t, out, "Epoch cleanup margin: 23h0m0s") + require.Contains(t, out, "Epoch advance on: 22 blobs or 80.7 MB, minimum 3h0m0s") + require.Contains(t, out, "Epoch checkpoint every: 9 epochs") + + env.RunAndExpectSuccess(t, "index", "epoch", "list") +} + func (s *formatSpecificTestSuite) TestRepositorySetParametersRequiredFeatures(t *testing.T) { env := s.setupInMemoryRepo(t) diff --git a/cli/command_repository_upgrade.go b/cli/command_repository_upgrade.go index ac01189a0..ef8f6a2d3 100644 --- a/cli/command_repository_upgrade.go +++ b/cli/command_repository_upgrade.go @@ -116,9 +116,8 @@ func loadIndexBlobs(ctx context.Context, indexEntries map[content.ID][2]index.In return nil } -// validateAction returns an array of strings (report) that describes differences between -// the V0 index blob and V1 index blob content. This is used to check that the upgraded index -// (V1 index) reflects the content of the old V0 index. +// validateAction returns an error of the new V1 index blob content does not match the source V0 index blob content. +// This is used to check that the upgraded index (V1 index) reflects the content of the old V0 index. func (c *commandRepositoryUpgrade) validateAction(ctx context.Context, rep repo.DirectRepositoryWriter) error { indexEntries := map[content.ID][2]index.Info{} @@ -171,28 +170,22 @@ func (c *commandRepositoryUpgrade) validateAction(ctx context.Context, rep repo. // checkIndexInfo compare two index infos. If a mismatch exists, return an error with diagnostic information. func checkIndexInfo(i0, i1 index.Info) error { var err error - if i0.GetFormatVersion() != i1.GetFormatVersion() { + switch { + case i0.GetFormatVersion() != i1.GetFormatVersion(): err = errors.Errorf("mismatched FormatVersions: %v %v", i0.GetFormatVersion(), i1.GetFormatVersion()) - } - if i0.GetOriginalLength() != i1.GetOriginalLength() { + case i0.GetOriginalLength() != i1.GetOriginalLength(): err = errors.Errorf("mismatched OriginalLengths: %v %v", i0.GetOriginalLength(), i1.GetOriginalLength()) - } - if i0.GetPackBlobID() != i1.GetPackBlobID() { + case i0.GetPackBlobID() != i1.GetPackBlobID(): err = errors.Errorf("mismatched PackBlobIDs: %v %v", i0.GetPackBlobID(), i1.GetPackBlobID()) - } - if i0.GetPackedLength() != i1.GetPackedLength() { + case i0.GetPackedLength() != i1.GetPackedLength(): err = errors.Errorf("mismatched PackedLengths: %v %v", i0.GetPackedLength(), i1.GetPackedLength()) - } - if i0.GetPackOffset() != i1.GetPackOffset() { + case i0.GetPackOffset() != i1.GetPackOffset(): err = errors.Errorf("mismatched PackOffsets: %v %v", i0.GetPackOffset(), i1.GetPackOffset()) - } - if i0.GetEncryptionKeyID() != i1.GetEncryptionKeyID() { + case i0.GetEncryptionKeyID() != i1.GetEncryptionKeyID(): err = errors.Errorf("mismatched EncryptionKeyIDs: %v %v", i0.GetEncryptionKeyID(), i1.GetEncryptionKeyID()) - } - if i0.GetDeleted() != i1.GetDeleted() { + case i0.GetDeleted() != i1.GetDeleted(): err = errors.Errorf("mismatched Deleted flags: %v %v", i0.GetDeleted(), i1.GetDeleted()) - } - if i0.GetTimestampSeconds() != i1.GetTimestampSeconds() { + case i0.GetTimestampSeconds() != i1.GetTimestampSeconds(): err = errors.Errorf("mismatched TimestampSeconds: %v %v", i0.GetTimestampSeconds(), i1.GetTimestampSeconds()) } if err != nil { diff --git a/repo/format/upgrade_lock.go b/repo/format/upgrade_lock.go index 713a7e2da..45def4877 100644 --- a/repo/format/upgrade_lock.go +++ b/repo/format/upgrade_lock.go @@ -9,12 +9,13 @@ "github.com/kopia/kopia/repo/blob" ) -// BackupBlobIDPrefix is the prefix for all identifiers of the BLOBs that -// keep a backup copy of the FormatBlobID BLOB for the purposes of rollback -// during upgrade. const ( + // BackupBlobIDPrefix is the prefix for all identifiers of the BLOBs that + // keep a backup copy of the FormatBlobID BLOB for the purposes of rollback + // during upgrade. BackupBlobIDPrefix = "kopia.repository.backup." + // LegacyIndexPoisonBlobID used to pollute V0 indexes after upgrade to prevent legacy clients from corrupting V1 indexes LegacyIndexPoisonBlobID = "n00000000000000000000000000000000-repository_unreadable_by_this_kopia_version_upgrade_required" )