From dd3bdb80867efcbc37de645a5606bfb7bc52614f Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Sat, 29 Aug 2026 17:24:27 +0800 Subject: [PATCH] fix: preserve bucket configs during site adoption Keep existing Object Lock and enabled versioning documents and timestamps when adopting a same-name bucket. Bootstrap missing configs, enable suspended versioning, and retain custom excluded-prefix settings. Signed-off-by: Feng Ruohang --- cmd/bucket-metadata.go | 5 +- cmd/site-replication-bucket-adoption_test.go | 190 +++++++++++++++++++ cmd/site-replication.go | 36 +++- 3 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 cmd/site-replication-bucket-adoption_test.go diff --git a/cmd/bucket-metadata.go b/cmd/bucket-metadata.go index ba4c0eb5b..9c066ba32 100644 --- a/cmd/bucket-metadata.go +++ b/cmd/bucket-metadata.go @@ -344,7 +344,10 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa } if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) { - b.VersioningConfigXML = enabledBucketVersioningConfig + config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML)) + if versioningErr != nil || !config.Enabled() { + b.VersioningConfigXML = enabledBucketVersioningConfig + } } if len(b.ObjectLockConfigXML) != 0 { diff --git a/cmd/site-replication-bucket-adoption_test.go b/cmd/site-replication-bucket-adoption_test.go new file mode 100644 index 000000000..2a5d98b3d --- /dev/null +++ b/cmd/site-replication-bucket-adoption_test.go @@ -0,0 +1,190 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "testing" + "time" + + "github.com/minio/minio/internal/auth" +) + +func TestPeerBucketAdoptionPreservesLockAndVersioningConfigs(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionPreservesLockAndVersioningConfigs, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testPeerBucketAdoptionPreservesLockAndVersioningConfigs(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + objectLockXML := []byte(`EnabledGOVERNANCE30`) + versioningXML := []byte(`Enabledtruetemporary/`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil { + t.Fatal(err) + } + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, versioningXML); err != nil { + t.Fatal(err) + } + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{ + CreatedAt: before.Created.Add(-time.Hour), + LockEnabled: true, + }); err != nil { + t.Fatalf("%s: adopting existing bucket failed: %v", instanceType, err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(after.ObjectLockConfigXML, before.ObjectLockConfigXML) || !after.ObjectLockConfigUpdatedAt.Equal(before.ObjectLockConfigUpdatedAt) { + t.Fatalf("%s: Object Lock config changed during adoption", instanceType) + } + if !bytes.Equal(after.VersioningConfigXML, before.VersioningConfigXML) || !after.VersioningConfigUpdatedAt.Equal(before.VersioningConfigUpdatedAt) { + t.Fatalf("%s: versioning config changed during adoption", instanceType) + } +} + +func TestPeerBucketAdoptionBootstrapsMissingConfigs(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionBootstrapsMissingConfigs, + }) +} + +func TestPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock, + }) +} + +func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + versioningXML := []byte(`Enabledtruetemporary/`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, versioningXML); err != nil { + t.Fatal(err) + } + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{ + CreatedAt: before.Created, + LockEnabled: true, + }); err != nil { + t.Fatal(err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(after.VersioningConfigXML, before.VersioningConfigXML) || !after.VersioningConfigUpdatedAt.Equal(before.VersioningConfigUpdatedAt) { + t.Fatalf("%s: custom versioning changed while enabling Object Lock", instanceType) + } + if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) { + t.Fatalf("%s: Object Lock was not bootstrapped", instanceType) + } +} + +func TestPeerBucketAdoptionEnablesSuspendedVersioning(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionEnablesSuspendedVersioning, + }) +} + +func testPeerBucketAdoptionEnablesSuspendedVersioning(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + suspended := []byte(`Suspended`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, suspended); err != nil { + t.Fatal(err) + } + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{CreatedAt: before.Created}); err != nil { + t.Fatal(err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if after.versioningConfig == nil || !after.versioningConfig.Enabled() { + t.Fatalf("%s: versioning remained disabled: %q", instanceType, after.VersioningConfigXML) + } + if !after.VersioningConfigUpdatedAt.After(before.VersioningConfigUpdatedAt) { + t.Fatalf("%s: versioning update time = %v, want after %v", instanceType, after.VersioningConfigUpdatedAt, before.VersioningConfigUpdatedAt) + } +} + +func TestEnablePeerBucketVersioningRepairsInvalidConfig(t *testing.T) { + meta := newBucketMetadata("bucket") + meta.Created = time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC) + meta.VersioningConfigXML = []byte(``) + if err := enablePeerBucketVersioning(&meta); err != nil { + t.Fatal(err) + } + if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() { + t.Fatalf("invalid versioning was not repaired: xml=%q updatedAt=%v", meta.VersioningConfigXML, meta.VersioningConfigUpdatedAt) + } +} + +func testPeerBucketAdoptionBootstrapsMissingConfigs(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if len(before.ObjectLockConfigXML) != 0 || len(before.VersioningConfigXML) != 0 { + t.Fatalf("%s: invalid bootstrap precondition", instanceType) + } + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{ + CreatedAt: before.Created, + LockEnabled: true, + }); err != nil { + t.Fatalf("%s: adopting existing bucket failed: %v", instanceType, err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) || !bytes.Equal(after.VersioningConfigXML, enabledBucketVersioningConfig) { + t.Fatalf("%s: missing bootstrap configs: objectLock=%q versioning=%q", instanceType, after.ObjectLockConfigXML, after.VersioningConfigXML) + } + if !after.ObjectLockConfigUpdatedAt.Equal(before.Created) || !after.VersioningConfigUpdatedAt.Equal(before.Created) { + t.Fatalf("%s: bootstrap timestamps = (%v, %v), want %v", instanceType, + after.ObjectLockConfigUpdatedAt, after.VersioningConfigUpdatedAt, before.Created) + } +} diff --git a/cmd/site-replication.go b/cmd/site-replication.go index 0196401dc..18b9abca6 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -46,6 +46,7 @@ import ( "github.com/minio/minio/internal/bucket/cors" "github.com/minio/minio/internal/bucket/lifecycle" sreplication "github.com/minio/minio/internal/bucket/replication" + "github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/logger" xldap "github.com/minio/pkg/v3/ldap" "github.com/minio/pkg/v3/policy" @@ -888,6 +889,32 @@ func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string return errors.Unwrap(cerr) } +func enablePeerBucketVersioning(meta *BucketMetadata) error { + if len(meta.VersioningConfigXML) == 0 { + meta.VersioningConfigXML = enabledBucketVersioningConfig + if meta.VersioningConfigUpdatedAt.IsZero() { + meta.VersioningConfigUpdatedAt = meta.Created + } + return nil + } + config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML)) + if err != nil { + meta.VersioningConfigXML = enabledBucketVersioningConfig + meta.VersioningConfigUpdatedAt = UTCNow() + return nil + } + if config.Enabled() { + return nil + } + config.Status = versioning.Enabled + meta.VersioningConfigXML, err = xml.Marshal(config) + if err != nil { + return err + } + meta.VersioningConfigUpdatedAt = UTCNow() + return nil +} + // PeerBucketMakeWithVersioningHandler - creates bucket and enables versioning. func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Context, bucket string, opts MakeBucketOptions) error { objAPI := newObjectLayerFn() @@ -916,9 +943,14 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con meta.SetCreatedAt(opts.CreatedAt) - meta.VersioningConfigXML = enabledBucketVersioningConfig - if opts.LockEnabled { + if err := enablePeerBucketVersioning(&meta); err != nil { + return wrapSRErr(err) + } + if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 { meta.ObjectLockConfigXML = enabledBucketObjectLockConfig + if meta.ObjectLockConfigUpdatedAt.IsZero() { + meta.ObjectLockConfigUpdatedAt = meta.Created + } } if err := meta.Save(context.Background(), objAPI); err != nil {