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

@@ -2,6 +2,7 @@ package s3api
import (
"encoding/hex"
"net/http"
"testing"
"time"
@@ -54,6 +55,42 @@ func TestListPartsResult(t *testing.T) {
}
func TestCompleteMultipartResultIncludesVersionId(t *testing.T) {
r := &http.Request{Host: "localhost", Header: make(http.Header)}
input := &s3.CompleteMultipartUploadInput{
Bucket: aws.String("example-bucket"),
Key: aws.String("example-object"),
}
entry := &filer_pb.Entry{
Extended: map[string][]byte{
s3_constants.ExtVersionIdKey: []byte("version-123"),
},
}
result := completeMultipartResult(r, input, "\"etag-value\"", entry)
if assert.NotNil(t, result.VersionId) {
assert.Equal(t, "version-123", *result.VersionId)
}
}
func TestCompleteMultipartResultOmitsNullVersionId(t *testing.T) {
r := &http.Request{Host: "localhost", Header: make(http.Header)}
input := &s3.CompleteMultipartUploadInput{
Bucket: aws.String("example-bucket"),
Key: aws.String("example-object"),
}
entry := &filer_pb.Entry{
Extended: map[string][]byte{
s3_constants.ExtVersionIdKey: []byte("null"),
},
}
result := completeMultipartResult(r, input, "\"etag-value\"", entry)
assert.Nil(t, result.VersionId)
}
func Test_parsePartNumber(t *testing.T) {
tests := []struct {
name string
@@ -190,3 +227,41 @@ func TestValidateCompletePartETag(t *testing.T) {
assert.True(t, invalid)
})
}
func TestCompleteMultipartUploadRejectsOutOfOrderParts(t *testing.T) {
s3a := NewS3ApiServerForTest()
input := &s3.CompleteMultipartUploadInput{
Bucket: aws.String("bucket"),
Key: aws.String("object"),
UploadId: aws.String("upload"),
}
parts := &CompleteMultipartUpload{
Parts: []CompletedPart{
{PartNumber: 2, ETag: "\"etag-2\""},
{PartNumber: 1, ETag: "\"etag-1\""},
},
}
result, errCode := s3a.completeMultipartUpload(&http.Request{Header: make(http.Header)}, input, parts)
assert.Nil(t, result)
assert.Equal(t, s3err.ErrInvalidPartOrder, errCode)
}
func TestCompleteMultipartUploadAllowsDuplicatePartNumbers(t *testing.T) {
s3a := NewS3ApiServerForTest()
input := &s3.CompleteMultipartUploadInput{
Bucket: aws.String("bucket"),
Key: aws.String("object"),
UploadId: aws.String("upload"),
}
parts := &CompleteMultipartUpload{
Parts: []CompletedPart{
{PartNumber: 1, ETag: "\"etag-older\""},
{PartNumber: 1, ETag: "\"etag-newer\""},
},
}
result, errCode := s3a.completeMultipartUpload(&http.Request{Header: make(http.Header)}, input, parts)
assert.Nil(t, result)
assert.Equal(t, s3err.ErrNoSuchUpload, errCode)
}