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 {