Browse Source
s3tables: fix shared table-location bucket mapping collisions (#8286)
s3tables: fix shared table-location bucket mapping collisions (#8286)
* s3tables: prevent shared table-location bucket mapping overwrite * Update weed/s3api/bucket_paths.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>pull/8289/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 307 additions and 10 deletions
-
2weed/s3api/bucket_metadata.go
-
82weed/s3api/bucket_paths.go
-
45weed/s3api/bucket_paths_test.go
-
83weed/s3api/s3tables/handler_table.go
-
77weed/s3api/s3tables/table_location_mapping_test.go
-
28weed/s3api/s3tables/utils.go
@ -0,0 +1,45 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import "testing" |
||||
|
|
||||
|
func TestNormalizeTableLocationMappingPath(t *testing.T) { |
||||
|
testCases := []struct { |
||||
|
name string |
||||
|
raw string |
||||
|
want string |
||||
|
}{ |
||||
|
{ |
||||
|
name: "legacy table path maps to bucket root", |
||||
|
raw: "/buckets/warehouse/analytics/orders", |
||||
|
want: "/buckets/warehouse", |
||||
|
}, |
||||
|
{ |
||||
|
name: "already bucket root", |
||||
|
raw: "/buckets/warehouse", |
||||
|
want: "/buckets/warehouse", |
||||
|
}, |
||||
|
{ |
||||
|
name: "relative buckets path normalized and reduced", |
||||
|
raw: "buckets/warehouse/analytics/orders", |
||||
|
want: "/buckets/warehouse", |
||||
|
}, |
||||
|
{ |
||||
|
name: "non buckets path preserved", |
||||
|
raw: "/tmp/custom/path", |
||||
|
want: "/tmp/custom/path", |
||||
|
}, |
||||
|
{ |
||||
|
name: "empty path", |
||||
|
raw: "", |
||||
|
want: "", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, tc := range testCases { |
||||
|
t.Run(tc.name, func(t *testing.T) { |
||||
|
if got := normalizeTableLocationMappingPath(tc.raw); got != tc.want { |
||||
|
t.Fatalf("normalizeTableLocationMappingPath(%q)=%q, want %q", tc.raw, got, tc.want) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
package s3tables |
||||
|
|
||||
|
import ( |
||||
|
"strings" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
func TestGetTableLocationMappingEntryPathPerTable(t *testing.T) { |
||||
|
tableLocationBucket := "shared-location--table-s3" |
||||
|
tablePathA := GetTablePath("warehouse", "analytics", "orders") |
||||
|
tablePathB := GetTablePath("warehouse", "analytics", "customers") |
||||
|
|
||||
|
entryPathA := GetTableLocationMappingEntryPath(tableLocationBucket, tablePathA) |
||||
|
entryPathARepeat := GetTableLocationMappingEntryPath(tableLocationBucket, tablePathA) |
||||
|
entryPathB := GetTableLocationMappingEntryPath(tableLocationBucket, tablePathB) |
||||
|
|
||||
|
if entryPathA != entryPathARepeat { |
||||
|
t.Fatalf("mapping entry path should be deterministic: %q != %q", entryPathA, entryPathARepeat) |
||||
|
} |
||||
|
if entryPathA == entryPathB { |
||||
|
t.Fatalf("mapping entry path should differ per table path: %q == %q", entryPathA, entryPathB) |
||||
|
} |
||||
|
|
||||
|
expectedPrefix := GetTableLocationMappingPath(tableLocationBucket) + "/" |
||||
|
if !strings.HasPrefix(entryPathA, expectedPrefix) { |
||||
|
t.Fatalf("mapping entry path %q should start with %q", entryPathA, expectedPrefix) |
||||
|
} |
||||
|
if strings.TrimPrefix(entryPathA, expectedPrefix) == "" { |
||||
|
t.Fatalf("mapping entry name should not be empty: %q", entryPathA) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestTableBucketPathFromTablePath(t *testing.T) { |
||||
|
testCases := []struct { |
||||
|
name string |
||||
|
tablePath string |
||||
|
expected string |
||||
|
ok bool |
||||
|
}{ |
||||
|
{ |
||||
|
name: "valid table path", |
||||
|
tablePath: GetTablePath("warehouse", "analytics", "orders"), |
||||
|
expected: GetTableBucketPath("warehouse"), |
||||
|
ok: true, |
||||
|
}, |
||||
|
{ |
||||
|
name: "valid table bucket root", |
||||
|
tablePath: GetTableBucketPath("warehouse"), |
||||
|
expected: GetTableBucketPath("warehouse"), |
||||
|
ok: true, |
||||
|
}, |
||||
|
{ |
||||
|
name: "invalid non-tables path", |
||||
|
tablePath: "/tmp/warehouse/analytics/orders", |
||||
|
expected: "", |
||||
|
ok: false, |
||||
|
}, |
||||
|
{ |
||||
|
name: "invalid empty bucket segment", |
||||
|
tablePath: "/buckets/", |
||||
|
expected: "", |
||||
|
ok: false, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, tc := range testCases { |
||||
|
t.Run(tc.name, func(t *testing.T) { |
||||
|
actual, ok := tableBucketPathFromTablePath(tc.tablePath) |
||||
|
if ok != tc.ok { |
||||
|
t.Fatalf("tableBucketPathFromTablePath(%q) ok=%v, want %v", tc.tablePath, ok, tc.ok) |
||||
|
} |
||||
|
if actual != tc.expected { |
||||
|
t.Fatalf("tableBucketPathFromTablePath(%q)=%q, want %q", tc.tablePath, actual, tc.expected) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue