mirror of
https://github.com/pgsty/minio.git
synced 2026-11-05 23:32:05 +03:00
fix(object): preserve retention and federated copy destination state
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -18,13 +18,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/auth"
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
)
|
||||
|
||||
// enableBucketObjectLock puts a lock-enabled configuration on an existing
|
||||
@@ -47,9 +50,108 @@ func enableBucketObjectLock(t *testing.T, bucket string) {
|
||||
globalBucketMetadataSys.Set(bucket, updated)
|
||||
}
|
||||
|
||||
// Exercise #165 and #166 together, including hold OFF, default retention and
|
||||
// explicit subsecond retention. Ordinary copies must not inherit source locks.
|
||||
func TestAPIFederatedCopyObjectLockParity(t *testing.T) {
|
||||
defer DetectTestLeak(t)()
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||
t: t,
|
||||
endpoints: []string{"CopyObject", "PutObject", "HeadObject", "GetObject"},
|
||||
objAPITest: func(obj ObjectLayer, instanceType, bucket string, router http.Handler, cred auth.Credentials, t *testing.T) {
|
||||
testKMS, err := kms.NewBuiltin(federationTestKMSKeyID, bytes.Repeat([]byte{0x58}, 32))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previousKMS := GlobalKMS
|
||||
GlobalKMS = testKMS
|
||||
defer func() { GlobalKMS = previousKMS }()
|
||||
remoteBucket, capture, cleanup := setupCopyObjectFederation(t, obj, router, instanceType, bucket)
|
||||
defer cleanup()
|
||||
enableBucketObjectLock(t, bucket)
|
||||
until := UTCNow().Add(7 * 24 * time.Hour).Truncate(time.Second).Add(789 * time.Millisecond).Format(time.RFC3339Nano)
|
||||
for _, sourceType := range []string{"plain", "s3"} {
|
||||
source := sourceType + "-held-source"
|
||||
headers := federationSSEHeaders(sourceType, 0, false)
|
||||
headers[xhttp.AmzObjectLockLegalHold] = "ON"
|
||||
putCopyChecksumSource(t, router, cred, bucket, source, []byte("held source"), headers)
|
||||
for _, tc := range []struct {
|
||||
name, hold, defaultMode, explicitMode string
|
||||
replace bool
|
||||
}{
|
||||
{name: "on", hold: "ON"},
|
||||
{name: "off", hold: "OFF"},
|
||||
{name: "no source inheritance"},
|
||||
{name: "on with default", hold: "ON", defaultMode: "GOVERNANCE"},
|
||||
{name: "off with default", hold: "OFF", defaultMode: "COMPLIANCE"},
|
||||
{name: "explicit retention", explicitMode: "GOVERNANCE"},
|
||||
{name: "hold and explicit retention", hold: "ON", explicitMode: "COMPLIANCE"},
|
||||
{name: "replace metadata", hold: "ON", explicitMode: "GOVERNANCE", replace: true},
|
||||
} {
|
||||
t.Run(instanceType+"/"+sourceType+"/"+tc.name, func(t *testing.T) {
|
||||
setTestBucketDefaultRetention(t, bucket, tc.defaultMode)
|
||||
setTestBucketDefaultRetention(t, remoteBucket, tc.defaultMode)
|
||||
headers := map[string]string{}
|
||||
if tc.hold != "" {
|
||||
headers[xhttp.AmzObjectLockLegalHold] = tc.hold
|
||||
}
|
||||
if tc.explicitMode != "" {
|
||||
headers[xhttp.AmzObjectLockMode] = tc.explicitMode
|
||||
headers[xhttp.AmzObjectLockRetainUntilDate] = until
|
||||
}
|
||||
if tc.replace {
|
||||
headers[xhttp.AmzMetadataDirective] = "REPLACE"
|
||||
headers["X-Amz-Meta-Origin"] = "replacement"
|
||||
}
|
||||
capture.mu.Lock()
|
||||
capture.headers = nil
|
||||
capture.mu.Unlock()
|
||||
for _, destinationBucket := range []string{bucket, remoteBucket} {
|
||||
destination := sourceType + "-copy-" + strings.ReplaceAll(tc.name, " ", "-")
|
||||
rec := federatedCopyRequest(t, router, cred, bucket, source, destinationBucket, destination, headers)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("copy to %s: %d %s", destinationBucket, rec.Code, rec.Body.String())
|
||||
}
|
||||
info, err := obj.GetObjectInfo(t.Context(), destinationBucket, destination, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := objectlock.GetObjectLegalHoldMeta(info.UserDefined).Status; string(got) != tc.hold {
|
||||
t.Errorf("stored hold = %q, want %q", got, tc.hold)
|
||||
}
|
||||
retention := objectlock.GetObjectRetentionMeta(info.UserDefined)
|
||||
mode := tc.explicitMode
|
||||
if mode == "" {
|
||||
mode = tc.defaultMode
|
||||
}
|
||||
if string(retention.Mode) != mode {
|
||||
t.Errorf("stored retention = %q, want %q", retention.Mode, mode)
|
||||
}
|
||||
if tc.explicitMode != "" && retention.RetainUntilDate.Format(time.RFC3339Nano) != until {
|
||||
t.Errorf("retention date lost precision: %s, want %s", retention.RetainUntilDate, until)
|
||||
}
|
||||
for key := range info.UserDefined {
|
||||
if stringsHasPrefixFold(key, "X-Amz-Meta-X-Amz-Object-Lock-") {
|
||||
t.Errorf("lock state became user metadata: %s", key)
|
||||
}
|
||||
}
|
||||
if tc.replace && info.UserDefined["X-Amz-Meta-Origin"] != "replacement" {
|
||||
t.Errorf("replacement metadata was lost: %v", info.UserDefined)
|
||||
}
|
||||
}
|
||||
typed, asMetadata := capture.legalHoldHeaders()
|
||||
if len(asMetadata) != 0 || (tc.hold != "" && strings.Join(typed, "") != tc.hold) || (tc.hold == "" && len(typed) != 0) {
|
||||
t.Errorf("forwarded hold headers = %v, metadata = %v; want %q", typed, asMetadata, tc.hold)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// legalHoldHeaders returns the forwarded legal-hold headers the remote saw,
|
||||
// separated into the real Object Lock header and the user-metadata spelling
|
||||
// minio-go produces for an unrecognised UserMetadata key.
|
||||
// minio-go produces for an unrecognized UserMetadata key.
|
||||
func (c *federationRemoteCapture) legalHoldHeaders() (typed, asMetadata []string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -72,7 +174,7 @@ func (c *federationRemoteCapture) legalHoldHeaders() (typed, asMetadata []string
|
||||
//
|
||||
// Before the fix the resolved hold was forwarded inside
|
||||
// PutObjectOptions.UserMetadata. minio-go's Header() prefixes every
|
||||
// UserMetadata key it does not recognise with "x-amz-meta-", and
|
||||
// UserMetadata key it does not recognize with "x-amz-meta-", and
|
||||
// x-amz-object-lock-legal-hold is in neither supportedHeaders nor isAmzHeader,
|
||||
// so the hold reached the remote as X-Amz-Meta-X-Amz-Object-Lock-Legal-Hold.
|
||||
// The destination stored no hold and the copy still answered 200 -- a silent
|
||||
|
||||
Reference in New Issue
Block a user