mirror of
https://github.com/pgsty/minio.git
synced 2026-09-05 18:16:16 +03:00
Merge pull request #89 from pgsty/codex/issue-76-object-lock-wire
fix: replicate object lock config in its own field
This commit is contained in:
@@ -255,7 +255,7 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht
|
|||||||
case madmin.SRBucketMetaTypeTags:
|
case madmin.SRBucketMetaTypeTags:
|
||||||
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt)
|
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt)
|
||||||
case madmin.SRBucketMetaTypeObjectLockConfig:
|
case madmin.SRBucketMetaTypeObjectLockConfig:
|
||||||
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt)
|
err = globalSiteReplicationSys.peerBucketObjectLockConfigItem(ctx, item)
|
||||||
case madmin.SRBucketMetaTypeSSEConfig:
|
case madmin.SRBucketMetaTypeSSEConfig:
|
||||||
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
||||||
case madmin.SRBucketMetaTypeCorsConfig:
|
case madmin.SRBucketMetaTypeCorsConfig:
|
||||||
|
|||||||
@@ -1844,12 +1844,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
// errors.
|
// errors.
|
||||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, newSRBucketObjectLockMeta(bucket, &cfgStr, updatedAt)))
|
||||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
|
||||||
Bucket: bucket,
|
|
||||||
ObjectLockConfig: &cfgStr,
|
|
||||||
UpdatedAt: updatedAt,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
|
"github.com/minio/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSRBucketObjectLockMetadata(t *testing.T) {
|
||||||
|
updatedAt := time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC)
|
||||||
|
current := "current"
|
||||||
|
legacy := "legacy"
|
||||||
|
|
||||||
|
event := newSRBucketObjectLockMeta("bucket", ¤t, updatedAt)
|
||||||
|
if event.Type != madmin.SRBucketMetaTypeObjectLockConfig || event.Bucket != "bucket" ||
|
||||||
|
event.ObjectLockConfig == nil || *event.ObjectLockConfig != current || event.Tags != nil || !event.UpdatedAt.Equal(updatedAt) {
|
||||||
|
t.Fatalf("unexpected Object Lock event: %#v", event)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var roundTrip madmin.SRBucketMeta
|
||||||
|
if err := json.Unmarshal(encoded, &roundTrip); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if roundTrip.ObjectLockConfig == nil || *roundTrip.ObjectLockConfig != current || roundTrip.Tags != nil {
|
||||||
|
t.Fatalf("unexpected JSON round trip: %#v", roundTrip)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
item madmin.SRBucketMeta
|
||||||
|
want *string
|
||||||
|
}{
|
||||||
|
{name: "current", item: madmin.SRBucketMeta{ObjectLockConfig: ¤t}, want: ¤t},
|
||||||
|
{name: "legacy", item: madmin.SRBucketMeta{Tags: &legacy}, want: &legacy},
|
||||||
|
{name: "current wins", item: madmin.SRBucketMeta{ObjectLockConfig: ¤t, Tags: &legacy}, want: ¤t},
|
||||||
|
{name: "missing", item: madmin.SRBucketMeta{}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got := srObjectLockPayload(test.item)
|
||||||
|
if test.want == nil {
|
||||||
|
if got != nil {
|
||||||
|
t.Fatalf("payload = %q, want nil", *got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got == nil || *got != *test.want {
|
||||||
|
t.Fatalf("payload = %v, want %q", got, *test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeerBucketObjectLockMetadataCurrentAndLegacyPayloads(t *testing.T) {
|
||||||
|
defer DetectTestLeak(t)()
|
||||||
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
t: t,
|
||||||
|
objAPITest: testPeerBucketObjectLockMetadataCurrentAndLegacyPayloads,
|
||||||
|
makeBucketOptions: MakeBucketOptions{LockEnabled: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySRBucketMetaViaAdmin(t *testing.T, credentials auth.Credentials, item madmin.SRBucketMeta) *httptest.ResponseRecorder {
|
||||||
|
t.Helper()
|
||||||
|
body, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
adminRouter := mux.NewRouter()
|
||||||
|
registerAdminRouter(adminRouter, true)
|
||||||
|
path := adminPathPrefix + adminAPIVersionPrefix + "/site-replication/peer/bucket-meta"
|
||||||
|
req, err := newTestSignedRequestV4(http.MethodPut, path, int64(len(body)), bytes.NewReader(body),
|
||||||
|
credentials.AccessKey, credentials.SecretKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
adminRouter.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPeerBucketObjectLockMetadataCurrentAndLegacyPayloads(_ ObjectLayer, instanceType, bucketName string,
|
||||||
|
_ http.Handler, credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
|
apply := func(item madmin.SRBucketMeta, wantDays uint64) {
|
||||||
|
t.Helper()
|
||||||
|
rec := applySRBucketMetaViaAdmin(t, credentials, item)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("%s: admin Object Lock apply returned %d: %s", instanceType, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if config.Rule == nil || config.Rule.DefaultRetention.Mode != "GOVERNANCE" ||
|
||||||
|
config.Rule.DefaultRetention.Days == nil || *config.Rule.DefaultRetention.Days != wantDays {
|
||||||
|
t.Fatalf("%s: persisted Object Lock config = %s, want GOVERNANCE/%d days", instanceType, config, wantDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config30 := base64.StdEncoding.EncodeToString([]byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`))
|
||||||
|
apply(newSRBucketObjectLockMeta(bucketName, &config30, UTCNow().Add(time.Hour)), 30)
|
||||||
|
|
||||||
|
config45 := base64.StdEncoding.EncodeToString([]byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>45</Days></DefaultRetention></Rule></ObjectLockConfiguration>`))
|
||||||
|
apply(madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||||
|
Bucket: bucketName,
|
||||||
|
Tags: &config45,
|
||||||
|
UpdatedAt: UTCNow().Add(2 * time.Hour),
|
||||||
|
}, 45)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeerBucketObjectLockMetadataWithoutLockEnabled(t *testing.T) {
|
||||||
|
defer DetectTestLeak(t)()
|
||||||
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
t: t,
|
||||||
|
objAPITest: testPeerBucketObjectLockMetadataWithoutLockEnabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPeerBucketObjectLockMetadataWithoutLockEnabled(_ ObjectLayer, instanceType, bucketName string,
|
||||||
|
_ http.Handler, credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
|
config := base64.StdEncoding.EncodeToString([]byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`))
|
||||||
|
item := newSRBucketObjectLockMeta(bucketName, &config, UTCNow().Add(time.Hour))
|
||||||
|
rec := applySRBucketMetaViaAdmin(t, credentials, item)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("%s: admin Object Lock apply returned %d: %s", instanceType, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
meta, err := globalBucketMetadataSys.Get(bucketName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if meta.objectLockConfig == nil || len(meta.VersioningConfigXML) != 0 {
|
||||||
|
t.Fatalf("%s: unlocked bucket metadata = objectLock:%v versioning:%q", instanceType, meta.objectLockConfig, meta.VersioningConfigXML)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealObjectLockMetadataUsesObjectLockField(t *testing.T) {
|
||||||
|
defer DetectTestLeak(t)()
|
||||||
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||||
|
t: t,
|
||||||
|
objAPITest: testHealObjectLockMetadataUsesObjectLockField,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHealObjectLockMetadataUsesObjectLockField(obj ObjectLayer, instanceType, bucketName string,
|
||||||
|
_ http.Handler, credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
|
ctx := t.Context()
|
||||||
|
localID := globalDeploymentID()
|
||||||
|
remoteID := "remote-object-lock-heal"
|
||||||
|
updatedAt := UTCNow().Add(time.Hour)
|
||||||
|
createdAt := updatedAt.Add(-time.Hour)
|
||||||
|
config := base64.StdEncoding.EncodeToString([]byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`))
|
||||||
|
|
||||||
|
remoteApplies := make(chan madmin.SRBucketMeta, 1)
|
||||||
|
remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var applied madmin.SRBucketMeta
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&applied); err != nil {
|
||||||
|
t.Errorf("%s: decode remote apply: %v", instanceType, err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteApplies <- applied
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer remote.Close()
|
||||||
|
|
||||||
|
serviceCred, err := auth.CreateCredentials("object-lock-heal-svc", "object-lock-heal-service-secret")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
serviceCred.ParentUser = credentials.AccessKey
|
||||||
|
if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false)
|
||||||
|
|
||||||
|
globalSiteReplicationSys.Lock()
|
||||||
|
oldEnabled := globalSiteReplicationSys.enabled
|
||||||
|
oldState := globalSiteReplicationSys.state
|
||||||
|
globalSiteReplicationSys.enabled = true
|
||||||
|
globalSiteReplicationSys.state = srState{
|
||||||
|
Name: "object-lock-heal-test",
|
||||||
|
ServiceAccountAccessKey: serviceCred.AccessKey,
|
||||||
|
Peers: map[string]madmin.PeerInfo{
|
||||||
|
localID: {Name: "local", DeploymentID: localID},
|
||||||
|
remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
globalSiteReplicationSys.Unlock()
|
||||||
|
defer func() {
|
||||||
|
globalSiteReplicationSys.Lock()
|
||||||
|
globalSiteReplicationSys.enabled = oldEnabled
|
||||||
|
globalSiteReplicationSys.state = oldState
|
||||||
|
globalSiteReplicationSys.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
status := srStatusInfo{
|
||||||
|
Sites: map[string]madmin.PeerInfo{
|
||||||
|
localID: {Name: "local", DeploymentID: localID},
|
||||||
|
remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL},
|
||||||
|
},
|
||||||
|
BucketStats: map[string]map[string]srBucketStatsSummary{
|
||||||
|
bucketName: {
|
||||||
|
localID: {
|
||||||
|
SRBucketStatsSummary: madmin.SRBucketStatsSummary{OLockConfigMismatch: true},
|
||||||
|
meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{
|
||||||
|
Bucket: bucketName,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
ObjectLockConfig: &config,
|
||||||
|
ObjectLockConfigUpdatedAt: updatedAt,
|
||||||
|
}, DeploymentID: localID},
|
||||||
|
},
|
||||||
|
remoteID: {
|
||||||
|
SRBucketStatsSummary: madmin.SRBucketStatsSummary{OLockConfigMismatch: true},
|
||||||
|
meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{
|
||||||
|
Bucket: bucketName,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
}, DeploymentID: remoteID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := globalSiteReplicationSys.healOLockConfigMetadata(ctx, obj, bucketName, status); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case applied := <-remoteApplies:
|
||||||
|
if applied.Type != madmin.SRBucketMetaTypeObjectLockConfig || applied.Bucket != bucketName ||
|
||||||
|
applied.ObjectLockConfig == nil || *applied.ObjectLockConfig != config || applied.Tags != nil || !applied.UpdatedAt.Equal(updatedAt) {
|
||||||
|
t.Fatalf("%s: remote heal apply = %#v", instanceType, applied)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("%s: remote heal did not dispatch Object Lock metadata", instanceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-12
@@ -1727,6 +1727,26 @@ func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucke
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSRBucketObjectLockMeta(bucket string, config *string, updatedAt time.Time) madmin.SRBucketMeta {
|
||||||
|
return madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
ObjectLockConfig: config,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func srObjectLockPayload(item madmin.SRBucketMeta) *string {
|
||||||
|
if item.ObjectLockConfig != nil {
|
||||||
|
return item.ObjectLockConfig
|
||||||
|
}
|
||||||
|
return item.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SiteReplicationSys) peerBucketObjectLockConfigItem(ctx context.Context, item madmin.SRBucketMeta) error {
|
||||||
|
return c.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, srObjectLockPayload(item), item.UpdatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
// PeerBucketObjectLockConfigHandler - sets object lock on local bucket.
|
// PeerBucketObjectLockConfigHandler - sets object lock on local bucket.
|
||||||
func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Context, bucket string, objectLockData *string, updatedAt time.Time) error {
|
func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Context, bucket string, objectLockData *string, updatedAt time.Time) error {
|
||||||
if objectLockData != nil {
|
if objectLockData != nil {
|
||||||
@@ -2176,12 +2196,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context, addOpts madmin.
|
|||||||
objLockCfgData, tm := meta.ObjectLockConfigXML, meta.ObjectLockConfigUpdatedAt
|
objLockCfgData, tm := meta.ObjectLockConfigXML, meta.ObjectLockConfigUpdatedAt
|
||||||
if len(objLockCfgData) > 0 {
|
if len(objLockCfgData) > 0 {
|
||||||
objLockStr := base64.StdEncoding.EncodeToString(objLockCfgData)
|
objLockStr := base64.StdEncoding.EncodeToString(objLockCfgData)
|
||||||
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
err = c.BucketMetaHook(ctx, newSRBucketObjectLockMeta(bucket, &objLockStr, tm))
|
||||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
|
||||||
Bucket: bucket,
|
|
||||||
Tags: &objLockStr,
|
|
||||||
UpdatedAt: tm,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errSRBucketMetaError(err)
|
return errSRBucketMetaError(err)
|
||||||
}
|
}
|
||||||
@@ -5322,12 +5337,7 @@ func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI
|
|||||||
return wrapSRErr(err)
|
return wrapSRErr(err)
|
||||||
}
|
}
|
||||||
peerName := info.Sites[dID].Name
|
peerName := info.Sites[dID].Name
|
||||||
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
err = admClient.SRPeerReplicateBucketMeta(ctx, newSRBucketObjectLockMeta(bucket, latestObjLockConfig, lastUpdate))
|
||||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
|
||||||
Bucket: bucket,
|
|
||||||
Tags: latestObjLockConfig,
|
|
||||||
UpdatedAt: lastUpdate,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||||
fmt.Errorf("Unable to heal object lock config metadata for peer %s from peer %s : %w",
|
fmt.Errorf("Unable to heal object lock config metadata for peer %s from peer %s : %w",
|
||||||
|
|||||||
Reference in New Issue
Block a user