s3api: make conditional mutations atomic and AWS-compatible (#8802)

* s3api: serialize conditional write finalization

* s3api: add conditional delete mutation checks

* s3api: enforce destination conditions for copy

* s3api: revalidate multipart completion under lock

* s3api: rollback failed put finalization hooks

* s3api: report delete-marker version deletions

* s3api: fix copy destination versioning edge cases

* s3api: make versioned multipart completion idempotent

* test/s3: cover conditional mutation regressions

* s3api: rollback failed copy version finalization

* s3api: resolve suspended delete conditions via latest entry

* s3api: remove copy test null-version injection

* s3api: reject out-of-order multipart completions

* s3api: preserve multipart replay version metadata

* s3api: surface copy destination existence errors

* s3api: simplify delete condition target resolution

* test/s3: make conditional delete assertions order independent

* test/s3: add distributed lock gateway integration

* s3api: fail closed multipart versioned completion

* s3api: harden copy metadata and overwrite paths

* s3api: create delete markers for suspended deletes

* s3api: allow duplicate multipart completion parts
This commit is contained in:
Chris Lu
2026-03-27 19:22:26 -07:00
committed by GitHub
parent bf2a2d2538
commit 0adb78bc6b
19 changed files with 2545 additions and 688 deletions

View File

@@ -8,7 +8,9 @@ import (
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type H map[string]string
@@ -396,6 +398,58 @@ func TestProcessMetadataBytes(t *testing.T) {
}
}
func TestMergeCopyMetadataPreservesInternalFields(t *testing.T) {
existing := map[string][]byte{
s3_constants.SeaweedFSSSEKMSKey: []byte("kms-secret"),
s3_constants.SeaweedFSSSEIV: []byte("iv"),
"X-Amz-Meta-Old": []byte("old"),
"X-Amz-Tagging-Old": []byte("old-tag"),
s3_constants.AmzStorageClass: []byte("STANDARD"),
}
updated := map[string][]byte{
"X-Amz-Meta-New": []byte("new"),
"X-Amz-Tagging-New": []byte("new-tag"),
s3_constants.AmzStorageClass: []byte("GLACIER"),
}
merged := mergeCopyMetadata(existing, updated)
if got := string(merged[s3_constants.SeaweedFSSSEKMSKey]); got != "kms-secret" {
t.Fatalf("expected internal KMS key to be preserved, got %q", got)
}
if got := string(merged[s3_constants.SeaweedFSSSEIV]); got != "iv" {
t.Fatalf("expected internal IV to be preserved, got %q", got)
}
if _, ok := merged["X-Amz-Meta-Old"]; ok {
t.Fatalf("expected stale user metadata to be removed, got %#v", merged)
}
if _, ok := merged["X-Amz-Tagging-Old"]; ok {
t.Fatalf("expected stale tagging metadata to be removed, got %#v", merged)
}
if got := string(merged["X-Amz-Meta-New"]); got != "new" {
t.Fatalf("expected replacement user metadata to be applied, got %q", got)
}
if got := string(merged["X-Amz-Tagging-New"]); got != "new-tag" {
t.Fatalf("expected replacement tagging metadata to be applied, got %q", got)
}
if got := string(merged[s3_constants.AmzStorageClass]); got != "GLACIER" {
t.Fatalf("expected storage class to be updated, got %q", got)
}
}
func TestCopyEntryETagPrefersStoredETag(t *testing.T) {
entry := &filer_pb.Entry{
Extended: map[string][]byte{
s3_constants.ExtETagKey: []byte("\"stored-etag\""),
},
Attributes: &filer_pb.FuseAttributes{},
}
if got := copyEntryETag(util.FullPath("/buckets/test-bucket/object.txt"), entry); got != "\"stored-etag\"" {
t.Fatalf("copyEntryETag() = %q, want %q", got, "\"stored-etag\"")
}
}
func fmtTagging(maps ...map[string]string) {
for _, m := range maps {
if tagging := m[s3_constants.AmzObjectTagging]; len(tagging) > 0 {
@@ -556,9 +610,8 @@ func TestCleanupVersioningMetadata(t *testing.T) {
}
}
// TestCopyVersioningIntegration validates the interaction between
// shouldCreateVersionForCopy and cleanupVersioningMetadata functions.
// This integration test ensures the complete fix for issue #7505.
// TestCopyVersioningIntegration validates the metadata shaping that happens
// before copy finalization for each destination versioning mode.
func TestCopyVersioningIntegration(t *testing.T) {
testCases := []struct {
name string
@@ -581,7 +634,7 @@ func TestCopyVersioningIntegration(t *testing.T) {
},
},
{
name: "SuspendedCleansMetadata",
name: "SuspendedCleansVersionMetadataBeforeFinalize",
versioningState: s3_constants.VersioningSuspended,
sourceMetadata: map[string][]byte{
s3_constants.ExtVersionIdKey: []byte("v123"),