iceberg: let clients select their table bucket as the catalog warehouse (#10549)

* iceberg: accept bare bucket names and ARNs as the catalog warehouse

Only s3://<bucket>/ was recognized. A warehouse spelled as a bare table
bucket name or as the s3tables bucket ARN -- the two forms users reach for
first, the latter being what AWS S3 Tables itself takes -- was silently
dropped, so every call landed on the default "warehouse" bucket and failed
with "table bucket warehouse not found".

* iceberg: report a missing table bucket as 404, not 500

Pointing a client at a table bucket that does not exist -- which every
client with no warehouse set does, since the default bucket "warehouse"
rarely exists -- returned InternalServerError with a message naming a
bucket the client never asked for. Answer 404 and say how to select one.

* admin: show the warehouse in the PyIceberg example

The example connected without one, so it always resolved to the default
table bucket and every client that copied it failed on the first call.

* test: pin bearer auth against a table bucket that exists

The subtest called the catalog with no warehouse and accepted 500 as proof
that auth had passed, since the default bucket does not exist. A missing
table bucket now answers 404, which the test read as an auth failure. Give
it a real table bucket so only 200 passes.

* test: assert the missing-bucket guidance reaches the client

The status and error type were checked but not the message, which is the
part of the mapping that tells a user how to select a table bucket.

* test: encode the warehouse query value

The ARN case pasted raw colons and slashes into the query string. Go's
parser tolerates them, so the test passed without modelling how a client
actually sends the request.
This commit is contained in:
Chris Lu
2026-08-03 13:25:37 -07:00
committed by GitHub
parent 63a180ef75
commit c191b2fe01
11 changed files with 189 additions and 38 deletions
+8 -9
View File
@@ -216,8 +216,12 @@ func TestOAuthTokenEndpoint(t *testing.T) {
t.Run("bearer token auth on catalog endpoint", func(t *testing.T) {
token := requestOAuthToken(t, env, env.accessKey, env.secretKey)
// Use the token to call the catalog
req, err := http.NewRequest(http.MethodGet, env.icebergURL()+"/v1/namespaces", nil)
// Point the call at a table bucket that exists, so anything but 200 is an
// auth or routing fault rather than the catalog reporting a missing bucket.
bucketName := "oauth-bearer-" + randomSuffix()
createTableBucketViaShell(t, env, bucketName)
req, err := http.NewRequest(http.MethodGet, env.icebergURL()+"/v1/"+bucketName+"/namespaces", nil)
if err != nil {
t.Fatalf("create request: %v", err)
}
@@ -225,17 +229,12 @@ func TestOAuthTokenEndpoint(t *testing.T) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("GET /v1/namespaces with Bearer: %v", err)
t.Fatalf("GET /v1/%s/namespaces with Bearer: %v", bucketName, err)
}
defer resp.Body.Close()
// Auth should pass. We accept 200 (success) or 500 (missing warehouse bucket
// is an internal error, not an auth error). Reject 401/403/404/405.
body, _ := io.ReadAll(resp.Body)
switch resp.StatusCode {
case http.StatusOK, http.StatusInternalServerError:
t.Logf("Bearer token auth succeeded, status=%d", resp.StatusCode)
default:
if resp.StatusCode != http.StatusOK {
t.Fatalf("Bearer auth failed unexpectedly: status=%d body=%s", resp.StatusCode, body)
}
})