mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-09 21:38:19 +01:00
* s3tables: route REST RenameTable and refuse unrouted S3 Tables requests
PUT /tables/<arn>/<ns>/<name>/rename had no route, so it fell through to the
/{bucket} routers and ran as a PutObject into a bucket named "tables",
answering 2xx while the table kept its name.
- Answer any s3tables-signed request no S3 Tables route takes with 404
UnknownOperationException, before the health, domain and bucket routers.
- Route the AWS RenameTable REST call (newNamespaceName, newName, both
optional; versionToken).
- Honor versionToken on RenameTable, checked against the snapshot the claim
asserts so a commit between the two reads cannot slip a stale token through.
* s3tables: match the credential scope's service, not a region named s3tables
The unrouted-request fallback widens what a false match costs: an S3
client whose region is named s3tables had every request answered as an
unknown S3 Tables operation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* s3tables: refuse an empty newName or newNamespaceName on REST rename
An empty field was read as omitted, so {"newNamespaceName":"other",
"newName":""} moved the table under its old name instead of failing
validation the way AWS does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* s3tables: check the rename version token after the snapshot comparison
Once the separately read metadata is known to equal the claimed snapshot,
the token compares against the parsed metadata; no second decode of the
snapshot bytes is needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* s3tables test: cover policy and tag changes in the rename snapshot compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* s3tables test: give each REST rename subtest its own source table
The token subtests renamed the table the first subtest produced, so run
alone they failed on a missing table instead of testing the token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.6 KiB
Go
117 lines
4.6 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
)
|
|
|
|
// emptyObjectBucket deletes every object in bucket.
|
|
func emptyObjectBucket(t *testing.T, ctx context.Context, client *s3.Client, bucket string) {
|
|
t.Helper()
|
|
pages := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{Bucket: aws.String(bucket)})
|
|
for pages.HasMorePages() {
|
|
page, err := pages.NextPage(ctx)
|
|
require.NoError(t, err)
|
|
for _, o := range page.Contents {
|
|
_, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: o.Key})
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The AWS SDK sends RenameTable as PUT /tables/<arn>/<ns>/<name>/rename. With
|
|
// no route for it the gateway ran it as a PutObject into an S3 bucket named
|
|
// "tables" and answered 2xx, so the rename reported success while the table
|
|
// kept its old name. The bucket is created up front so that failure shows as
|
|
// an object landing in it.
|
|
func TestRenameTableREST(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
ctx := context.Background()
|
|
tables := NewS3TablesClient(sharedCluster.s3Endpoint, testRegion, testAccessKey, testSecretKey)
|
|
objects := newObjectClient(t)
|
|
|
|
const decoyBucket = "tables"
|
|
if _, err := objects.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(decoyBucket)}); err != nil {
|
|
require.Equal(t, "BucketAlreadyOwnedByYou", apiErrorCode(err), "create the %q bucket: %v", decoyBucket, err)
|
|
}
|
|
emptyObjectBucket(t, ctx, objects, decoyBucket)
|
|
defer func() {
|
|
emptyObjectBucket(t, ctx, objects, decoyBucket)
|
|
_, _ = objects.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(decoyBucket)})
|
|
}()
|
|
|
|
created, err := tables.CreateTableBucket("rename-rest-"+randomString(8), nil)
|
|
require.NoError(t, err)
|
|
defer tables.DeleteTableBucket(created.ARN)
|
|
bucketARN := created.ARN
|
|
|
|
const namespace, otherNamespace = "rename_ns", "rename_other"
|
|
for _, ns := range []string{namespace, otherNamespace} {
|
|
_, err := tables.CreateNamespace(bucketARN, []string{ns})
|
|
require.NoError(t, err)
|
|
defer tables.DeleteNamespace(bucketARN, []string{ns})
|
|
}
|
|
|
|
// Each subtest owns its source table, so any one of them runs alone.
|
|
createSource := func(t *testing.T, name string) string {
|
|
resp, err := tables.CreateTable(bucketARN, []string{namespace}, name, "ICEBERG", nil, nil)
|
|
require.NoError(t, err)
|
|
return resp.VersionToken
|
|
}
|
|
|
|
t.Run("newName only", func(t *testing.T) {
|
|
createSource(t, "src")
|
|
defer tables.DeleteTable(bucketARN, []string{namespace}, "src")
|
|
defer tables.DeleteTable(bucketARN, []string{namespace}, "dst")
|
|
require.NoError(t, tables.RenameTable(bucketARN, []string{namespace}, "src", "", "dst", ""))
|
|
|
|
got, err := tables.GetTable(bucketARN, []string{namespace}, "dst")
|
|
require.NoError(t, err, "the renamed table must resolve under its new name")
|
|
assert.Equal(t, "dst", got.Name)
|
|
|
|
_, err = tables.GetTable(bucketARN, []string{namespace}, "src")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), s3tables.ErrCodeNoSuchTable)
|
|
|
|
list, err := objects.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String(decoyBucket)})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, list.Contents, "the rename must not store anything in the %q bucket", decoyBucket)
|
|
})
|
|
|
|
t.Run("stale versionToken", func(t *testing.T) {
|
|
token := createSource(t, "stale_src")
|
|
defer tables.DeleteTable(bucketARN, []string{namespace}, "stale_src")
|
|
defer tables.DeleteTable(bucketARN, []string{otherNamespace}, "stale_moved")
|
|
err := tables.RenameTable(bucketARN, []string{namespace}, "stale_src", otherNamespace, "stale_moved", token+"-stale")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), s3tables.ErrCodeConflict)
|
|
|
|
_, err = tables.GetTable(bucketARN, []string{namespace}, "stale_src")
|
|
require.NoError(t, err, "a refused rename must leave the source in place")
|
|
})
|
|
|
|
t.Run("newNamespaceName and newName with the current versionToken", func(t *testing.T) {
|
|
token := createSource(t, "tok_src")
|
|
defer tables.DeleteTable(bucketARN, []string{namespace}, "tok_src")
|
|
defer tables.DeleteTable(bucketARN, []string{otherNamespace}, "moved")
|
|
require.NoError(t, tables.RenameTable(bucketARN, []string{namespace}, "tok_src", otherNamespace, "moved", token))
|
|
|
|
got, err := tables.GetTable(bucketARN, []string{otherNamespace}, "moved")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "moved", got.Name)
|
|
|
|
_, err = tables.GetTable(bucketARN, []string{namespace}, "tok_src")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), s3tables.ErrCodeNoSuchTable)
|
|
})
|
|
}
|