Files
seaweedfs/weed/s3api/s3tables/handler_renamed_source_test.go
T
Nguyễn Lộc Phúcanduser.email 7d8b72c2b4 fix(s3tables): answer 404 for a table name that was renamed away (#12258)
* s3tables: answer 404 for a table name that was renamed away

A rename leaves the source directory in place to hold the data under
it, with its catalog attributes cleared. UpdateTable, DeleteTable, the
table policy calls and the table maintenance calls read the metadata
attribute of that directory and reported its absence as an internal
error, so they answered 500, and GetTablePolicy answered NoSuchPolicy.

An absent metadata attribute now reads as a missing entry. tableMetadata
reports it as both filer_pb.ErrNotFound and the attribute error, so
callers that test for either keep working. The policy and maintenance
handlers read the attribute through the new readTableMetadataAttr, which
reports filer_pb.ErrNotFound, so each call answers NoSuchTable.

* s3tables: a view record without its metadata reads absent too

* s3tables: refuse a catalog write that lands on a metadata-less record

---------

Co-authored-by: user.email <chrislusf@users.noreply.github.com>
2026-10-30 19:53:57 -07:00

181 lines
6.8 KiB
Go

package s3tables
import (
"context"
"net/http"
"path"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// extendedOf copies the attributes held on an entry, so a later write to the
// entry shows up as a difference.
func extendedOf(t *testing.T, fs *s3tablestest.MemFiler, dir, name string) map[string][]byte {
t.Helper()
entry := fs.Get(dir, name)
require.NotNil(t, entry)
copied := make(map[string][]byte, len(entry.Extended))
for key, value := range entry.Extended {
copied[key] = append([]byte(nil), value...)
}
return copied
}
// A rename leaves the source directory in place to hold the data under it, with
// its catalog attributes cleared. That name is no longer a table, so every table
// operation on it answers NoSuchTable and writes nothing to the directory.
func TestRenamedAwayNameIsNoSuchTable(t *testing.T) {
fs, m := startRenameManager(t)
requireOK(t, m, fs, "CreateNamespace", &CreateNamespaceRequest{
TableBucketARN: mustBucketARN(t),
Namespace: []string{"ns2"},
})
requireOK(t, m, fs, "RenameTable", &RenameTableRequest{
TableBucketARN: mustBucketARN(t),
SourceNamespace: []string{"ns"},
SourceName: "t",
DestNamespace: []string{"ns2"},
DestName: "t",
})
nsPath := GetNamespacePath(renameTestBucket, "ns")
before := extendedOf(t, fs, nsPath, "t")
bucketARN := mustBucketARN(t)
ops := []struct {
operation string
input interface{}
}{
{"UpdateTable", &UpdateTableRequest{
TableBucketARN: bucketARN,
Namespace: []string{"ns"},
Name: "t",
MetadataVersion: 4,
MetadataLocation: "s3://" + renameTestBucket + "/ns/t/metadata/v4.metadata.json",
}},
{"DeleteTable", deleteTableRequest(t, "t")},
{"PutTablePolicy", &PutTablePolicyRequest{
TableBucketARN: bucketARN,
Namespace: []string{"ns"},
Name: "t",
ResourcePolicy: `{"Statement":[{"Effect":"Allow","Principal":"writer","Action":"s3tables:*","Resource":"*"}]}`,
}},
{"GetTablePolicy", &GetTablePolicyRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "t"}},
{"DeleteTablePolicy", &DeleteTablePolicyRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "t"}},
{"PutTableMaintenanceConfiguration", &PutTableMaintenanceConfigurationRequest{
TableBucketARN: bucketARN,
Namespace: []string{"ns"},
Name: "t",
Type: MaintenanceTypeIcebergCompaction,
Value: &MaintenanceConfigurationValue{Status: MaintenanceStatusEnabled},
}},
{"GetTableMaintenanceConfiguration", &GetTableMaintenanceConfigurationRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "t"}},
{"GetTableMaintenanceJobStatus", &GetTableMaintenanceJobStatusRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "t"}},
}
for _, op := range ops {
t.Run(op.operation, func(t *testing.T) {
status, resp := runHandler(t, m, fs.Client, op.operation, op.input)
assert.Equal(t, http.StatusNotFound, status, "%+v", resp)
assert.Equal(t, ErrCodeNoSuchTable, resp.Type, "%+v", resp)
})
}
assert.Equal(t, before, extendedOf(t, fs, nsPath, "t"), "the renamed-away directory must not be written to")
status, resp := runHandler(t, m, fs.Client, "GetTable", &GetTableRequest{
TableBucketARN: bucketARN,
Namespace: []string{"ns2"},
Name: "t",
})
assert.Equal(t, http.StatusOK, status, "the renamed table must still resolve: %+v", resp)
}
// Views share the renamer, so a renamed-away view name answers NoSuchView the
// same way, and the leftover view record still takes no writes.
func TestRenamedAwayViewNameIsNoSuchView(t *testing.T) {
fs, m := startRenameManager(t)
seedView(t, fs, "v")
requireOK(t, m, fs, "RenameView", &RenameTableRequest{
TableBucketARN: mustBucketARN(t),
SourceNamespace: []string{"ns"},
SourceName: "v",
DestNamespace: []string{"ns"},
DestName: "v2",
})
nsPath := GetNamespacePath(renameTestBucket, "ns")
before := extendedOf(t, fs, nsPath, "v")
bucketARN := mustBucketARN(t)
ops := []struct {
operation string
input interface{}
}{
{"GetView", &GetViewRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "v"}},
{"UpdateView", &UpdateViewRequest{
TableBucketARN: bucketARN,
Namespace: []string{"ns"},
Name: "v",
MetadataLocation: "s3://" + renameTestBucket + "/ns/v/metadata/v2.metadata.json",
}},
{"DeleteView", &DeleteViewRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "v"}},
}
for _, op := range ops {
t.Run(op.operation, func(t *testing.T) {
status, resp := runHandler(t, m, fs.Client, op.operation, op.input)
assert.Equal(t, http.StatusNotFound, status, "%+v", resp)
assert.Equal(t, ErrCodeNoSuchView, resp.Type, "%+v", resp)
})
}
assert.Equal(t, before, extendedOf(t, fs, nsPath, "v"), "the renamed-away directory must not be written to")
}
// A record that keeps its view marker but lost the metadata — the state a
// rename leaves behind while its clear is in flight — is not a view to its
// readers either.
func TestViewRecordWithoutMetadataIsNoSuchView(t *testing.T) {
fs, m := startRenameManager(t)
nsPath := GetNamespacePath(renameTestBucket, "ns")
fs.Put(nsPath, "half", map[string][]byte{ExtendedKeyEntryType: []byte(EntryTypeView)})
bucketARN := mustBucketARN(t)
ops := []struct {
operation string
input interface{}
}{
{"GetView", &GetViewRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "half"}},
{"UpdateView", &UpdateViewRequest{
TableBucketARN: bucketARN,
Namespace: []string{"ns"},
Name: "half",
MetadataLocation: "s3://" + renameTestBucket + "/ns/half/metadata/v2.metadata.json",
}},
{"DeleteView", &DeleteViewRequest{TableBucketARN: bucketARN, Namespace: []string{"ns"}, Name: "half"}},
}
for _, op := range ops {
t.Run(op.operation, func(t *testing.T) {
status, resp := runHandler(t, m, fs.Client, op.operation, op.input)
assert.Equal(t, http.StatusNotFound, status, "%+v", resp)
assert.Equal(t, ErrCodeNoSuchView, resp.Type, "%+v", resp)
})
}
}
// A write landing after the record lost its metadata — a rename cleared it
// between the handler's read and the mutation — must not write the leftover
// directory.
func TestMutateCatalogRecordRefusesClearedRecord(t *testing.T) {
fs, m := startRenameManager(t)
nsPath := GetNamespacePath(renameTestBucket, "ns")
fs.Put(nsPath, "gone", map[string][]byte{})
err := m.handler.mutateCatalogRecord(context.Background(), fs.Client, path.Join(nsPath, "gone"), func(extended map[string][]byte) error {
extended[ExtendedKeyPolicy] = []byte("x")
return nil
})
assert.ErrorIs(t, err, filer_pb.ErrNotFound)
assert.Empty(t, fs.Get(nsPath, "gone").Extended)
}