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

@@ -778,6 +778,7 @@ func NewS3ApiServerForTest() *S3ApiServer {
option: &S3ApiServerOption{
BucketsPath: "/buckets",
},
bucketConfigCache: NewBucketConfigCache(60 * time.Minute),
}
}
@@ -928,3 +929,56 @@ func TestConditionalHeadersMultipartUpload(t *testing.T) {
}
})
}
func TestConditionalHeadersTreatDeleteMarkerAsMissing(t *testing.T) {
bucket := "test-bucket"
object := "/deleted-object"
deleteMarkerEntry := &filer_pb.Entry{
Name: "deleted-object",
Extended: map[string][]byte{
s3_constants.ExtDeleteMarkerKey: []byte("true"),
},
Attributes: &filer_pb.FuseAttributes{
Mtime: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
},
}
t.Run("WriteIfNoneMatchAsteriskSucceeds", func(t *testing.T) {
getter := createMockEntryGetter(deleteMarkerEntry)
req := createTestPutRequest(bucket, object, "new content")
req.Header.Set(s3_constants.IfNoneMatch, "*")
s3a := NewS3ApiServerForTest()
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
if errCode != s3err.ErrNone {
t.Fatalf("expected ErrNone for delete marker with If-None-Match=*, got %v", errCode)
}
})
t.Run("WriteIfMatchAsteriskFails", func(t *testing.T) {
getter := createMockEntryGetter(deleteMarkerEntry)
req := createTestPutRequest(bucket, object, "new content")
req.Header.Set(s3_constants.IfMatch, "*")
s3a := NewS3ApiServerForTest()
errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
if errCode != s3err.ErrPreconditionFailed {
t.Fatalf("expected ErrPreconditionFailed for delete marker with If-Match=*, got %v", errCode)
}
})
t.Run("ReadIfMatchAsteriskFails", func(t *testing.T) {
getter := createMockEntryGetter(deleteMarkerEntry)
req := &http.Request{Method: http.MethodGet, Header: make(http.Header)}
req.Header.Set(s3_constants.IfMatch, "*")
s3a := NewS3ApiServerForTest()
result := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
if result.ErrorCode != s3err.ErrPreconditionFailed {
t.Fatalf("expected ErrPreconditionFailed for read against delete marker with If-Match=*, got %v", result.ErrorCode)
}
if result.Entry != nil {
t.Fatalf("expected no entry to be returned for delete marker, got %#v", result.Entry)
}
})
}