Browse Source
Fix S3 conditional writes with versioning (Issue #8073) (#8080)
Fix S3 conditional writes with versioning (Issue #8073) (#8080)
* Fix S3 conditional writes with versioning (Issue #8073) Refactors conditional header checks to properly resolve the latest object version when versioning is enabled. This prevents incorrect validation against non-versioned root objects. * Add integration test for S3 conditional writes with versioning (Issue #8073) * Refactor: Propagate internal errors in conditional header checks - Make resolveObjectEntry return errors from isVersioningConfigured - Update checkConditionalHeaders checks to return 500 on internal resolve errors * Refactor: Stricter error handling and test assertions - Propagate internal errors in checkConditionalHeaders*WithGetter functions - Enforce strict 412 PreconditionFailed check in integration test * Perf: Add early return for conditional headers + safety improvements - Add fast path to skip resolveObjectEntry when no conditional headers present - Avoids expensive getLatestObjectVersion retries in common case - Add nil checks before dereferencing pointers in integration test - Fix grammar in test comments - Remove duplicate comment in resolveObjectEntry * Refactor: Use errors.Is for robust ErrNotFound checking - Update checkConditionalHeaders* to use errors.Is(err, filer_pb.ErrNotFound) - Update resolveObjectEntry to use errors.Is for wrapped error compatibility - Remove duplicate comment lines in s3api handlers * Perf: Optimize resolveObjectEntry for conditional checks - Refactor getLatestObjectVersion to doGetLatestObjectVersion supporting variable retries - Use 1-retry path in resolveObjectEntry to avoid exponential backoff latency * Test: Enhance integration test with content verification - Verify actual object content equals expected content after successful conditional write - Add missing io and errors imports to test file * Refactor: Final refinements based on feedback - Optimize header validation by passing parsed headers to avoid redundant parsing - Simplify integration test assertions using require.Error and assert.True - Fix build errors in s3api handler and test imports * Test: Use smithy.APIError for robust error code checking - Replace string-based error checking with structured API error - Add smithy-go import for AWS SDK v2 error handling * Test: Use types.PreconditionFailed and handle io.ReadAll error - Replace smithy.APIError with more specific types.PreconditionFailed - Add proper error handling for io.ReadAll in content verification * Refactor: Use combined error checking and add nil guards - Use smithy.APIError with ErrorCode() for robust error checking - Add nil guards for entry.Attributes before accessing Mtime - Prevents potential panics when Attributes is uninitializedpull/4306/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 257 additions and 56 deletions
-
101test/s3/versioning/s3_conditional_writes_test.go
-
23weed/s3api/s3api_object_handlers.go
-
182weed/s3api/s3api_object_handlers_put.go
-
7weed/s3api/s3api_object_versioning.go
@ -0,0 +1,101 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"io" |
||||
|
"strings" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/aws/aws-sdk-go-v2/aws" |
||||
|
"github.com/aws/aws-sdk-go-v2/service/s3" |
||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types" |
||||
|
"github.com/aws/smithy-go" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
// TestConditionalWritesWithVersioning verifies that conditional writes (If-Match)
|
||||
|
// work correctly with versioned buckets, specifically ensuring they validate against
|
||||
|
// the LATEST version of the object, not the base object.
|
||||
|
// reproduces issue #8073
|
||||
|
func TestConditionalWritesWithVersioning(t *testing.T) { |
||||
|
client := getS3Client(t) |
||||
|
bucketName := getNewBucketName() |
||||
|
|
||||
|
// Create bucket
|
||||
|
createBucket(t, client, bucketName) |
||||
|
defer deleteBucket(t, client, bucketName) |
||||
|
|
||||
|
// Enable versioning
|
||||
|
enableVersioning(t, client, bucketName) |
||||
|
checkVersioningStatus(t, client, bucketName, types.BucketVersioningStatusEnabled) |
||||
|
|
||||
|
key := "cond-write-test" |
||||
|
|
||||
|
// 1. Create Version 1
|
||||
|
v1Resp := putObject(t, client, bucketName, key, "content-v1") |
||||
|
require.NotNil(t, v1Resp.ETag) |
||||
|
require.NotNil(t, v1Resp.VersionId) |
||||
|
v1ETag := *v1Resp.ETag |
||||
|
t.Logf("Created Version 1: ETag=%s, VersionId=%s", v1ETag, *v1Resp.VersionId) |
||||
|
|
||||
|
// 2. Create Version 2 (This is now the LATEST version)
|
||||
|
v2Resp := putObject(t, client, bucketName, key, "content-v2") |
||||
|
require.NotNil(t, v2Resp.ETag) |
||||
|
require.NotNil(t, v2Resp.VersionId) |
||||
|
v2ETag := *v2Resp.ETag |
||||
|
t.Logf("Created Version 2: ETag=%s, VersionId=%s", v2ETag, *v2Resp.VersionId) |
||||
|
|
||||
|
require.NotEqual(t, v1ETag, v2ETag, "ETags should be different for different content") |
||||
|
|
||||
|
// 3. Attempt conditional PUT using Version 1's ETag (If-Match: v1ETag)
|
||||
|
// EXPECTATION: Should FAIL with 412 Precondition Failed because the latest version is V2.
|
||||
|
// BUG (Issue #8073): Previously, this might have succeeded if it checked against an old/stale entry or base entry.
|
||||
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ |
||||
|
Bucket: aws.String(bucketName), |
||||
|
Key: aws.String(key), |
||||
|
Body: strings.NewReader("content-v3-should-fail"), |
||||
|
IfMatch: aws.String(v1ETag), |
||||
|
}) |
||||
|
|
||||
|
require.Error(t, err, "Conditional PUT with stale ETag should have failed") |
||||
|
|
||||
|
// Verify strict error checking for 412 Precondition Failed using AWS SDK v2 structured errors
|
||||
|
var apiErr smithy.APIError |
||||
|
if assert.True(t, errors.As(err, &apiErr), "Expected a smithy.APIError, but got %T", err) { |
||||
|
assert.Equal(t, "PreconditionFailed", apiErr.ErrorCode(), "Expected PreconditionFailed error code") |
||||
|
t.Logf("Received expected 412 Precondition Failed error: %v", err) |
||||
|
} |
||||
|
|
||||
|
// 4. Attempt conditional PUT using Version 2's ETag (If-Match: v2ETag)
|
||||
|
// EXPECTATION: Should SUCCEED because V2 is the latest version.
|
||||
|
v4Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ |
||||
|
Bucket: aws.String(bucketName), |
||||
|
Key: aws.String(key), |
||||
|
Body: strings.NewReader("content-v4-should-succeed"), |
||||
|
IfMatch: aws.String(v2ETag), |
||||
|
}) |
||||
|
require.NoError(t, err, "Conditional PUT with correct latest ETag should succeed") |
||||
|
require.NotNil(t, v4Resp, "PutObject response should not be nil on success") |
||||
|
require.NotNil(t, v4Resp.ETag, "ETag should not be nil on successful PutObject") |
||||
|
require.NotNil(t, v4Resp.VersionId, "VersionId should not be nil on successful PutObject") |
||||
|
t.Logf("Created Version 4: ETag=%s, VersionId=%s", *v4Resp.ETag, *v4Resp.VersionId) |
||||
|
|
||||
|
// 5. Verify the updates
|
||||
|
// The content should be "content-v4-should-succeed"
|
||||
|
headResp := headObject(t, client, bucketName, key) |
||||
|
require.NotNil(t, headResp.VersionId, "VersionId should not be nil on HeadObject response") |
||||
|
assert.Equal(t, *v4Resp.VersionId, *headResp.VersionId) |
||||
|
|
||||
|
// Verify actual content
|
||||
|
getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ |
||||
|
Bucket: aws.String(bucketName), |
||||
|
Key: aws.String(key), |
||||
|
}) |
||||
|
require.NoError(t, err) |
||||
|
defer getResp.Body.Close() |
||||
|
body, err := io.ReadAll(getResp.Body) |
||||
|
require.NoError(t, err) |
||||
|
assert.Equal(t, "content-v4-should-succeed", string(body), "Content should match the successful conditional write") |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue