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:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user