Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 4 additions & 17 deletions .docker/docker-compose-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,11 @@ services:
entrypoint: >
/bin/sh -c "
set -e;
for b in supa-storage-bucket warehouse--table-s3 supa-storage-profiles; do
for b in supa-storage-bucket warehouse--table-s3 warehouse-shard-2--table-s3 supa-storage-profiles; do
curl -fsS --aws-sigv4 'aws:amz:us-east-1:s3' -u supa-storage:secret1234 -X PUT http://rustfs:9000/\$$b;
done;
for b in warehouse--table-s3 warehouse-shard-2--table-s3; do
curl -fsS --aws-sigv4 'aws:amz:us-east-1:s3' -u supa-storage:secret1234 -X PUT http://rustfs:9000/iceberg/v1/buckets/\$$b;
done
"

Expand Down Expand Up @@ -189,22 +192,6 @@ services:
- IMGPROXY_PRESETS=default=width:3000/height:8192
- IMGPROXY_FORMAT_QUALITY=jpeg=80,avif=62,webp=80

rest-catalog:
image: apache/iceberg-rest-fixture:1.10.1
container_name: iceberg-rest
depends_on:
rustfs_setup:
condition: service_completed_successfully
ports:
- 8181:8181
environment:
- AWS_ACCESS_KEY_ID=supa-storage
- AWS_SECRET_ACCESS_KEY=secret1234
- AWS_REGION=us-east-1
- CATALOG_WAREHOUSE=s3://warehouse--table-s3/
- CATALOG_IO__IMPL=org.apache.iceberg.aws.s3.S3FileIO
- CATALOG_S3_ENDPOINT=http://rustfs:9000

# Optional for rate-limiting
# redis:
# image: redis:6.2-alpine
Expand Down
11 changes: 6 additions & 5 deletions .env.test.sample
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ REQUEST_X_FORWARDED_HOST_REGEXP=
VECTOR_ENABLED=true
VECTOR_S3_BUCKETS=supa-test-local-dev
ICEBERG_ENABLED=true
ICEBERG_WAREHOUSE=.
ICEBERG_SHARDS=.
ICEBERG_CATALOG_URL=http://127.0.0.1:8181/v1
ICEBERG_CATALOG_AUTH_TYPE=token
ICEBERG_CATALOG_AUTH_TOKEN=token
ICEBERG_WAREHOUSE=warehouse--table-s3
ICEBERG_SHARDS=warehouse--table-s3,warehouse-shard-2--table-s3
ICEBERG_CATALOG_URL=http://127.0.0.1:9000/iceberg/v1
ICEBERG_CATALOG_AUTH_TYPE=sigv4
ICEBERG_CATALOG_SIGV4_SERVICE=s3
ICEBERG_CATALOG_PURGE_ON_DROP=false
ICEBERG_S3_DELETE_ENABLED=true
ICEBERG_BUCKET_DETECTION_MODE="BUCKET"

Expand Down
2 changes: 1 addition & 1 deletion acceptance/specs/iceberg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ function createTablePayload(tableName: string) {
'schema-id': 0,
type: 'struct',
},
spec: {
'partition-spec': {
fields: [],
'spec-id': 0,
},
Expand Down
17 changes: 6 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ services:
S3_PROTOCOL_ACCESS_KEY_SECRET: 850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
# Iceberg Protocol
ICEBERG_BUCKET_DETECTION_MODE: "FULL_PATH"
ICEBERG_WAREHOUSE: .
ICEBERG_CATALOG_URL: http://rest-catalog:8181/v1
ICEBERG_CATALOG_AUTH_TYPE: token
ICEBERG_CATALOG_AUTH_TOKEN: token
ICEBERG_WAREHOUSE: warehouse--table-s3
ICEBERG_SHARDS: warehouse--table-s3,warehouse-shard-2--table-s3
ICEBERG_CATALOG_URL: http://rustfs:9000/iceberg/v1
ICEBERG_CATALOG_AUTH_TYPE: sigv4
ICEBERG_CATALOG_SIGV4_SERVICE: s3
ICEBERG_CATALOG_PURGE_ON_DROP: false
ICEBERG_S3_DELETE_ENABLED: true

tenant_db:
Expand Down Expand Up @@ -81,13 +83,6 @@ services:
service: imgproxy
file: ./.docker/docker-compose-infra.yml

rest-catalog:
depends_on:
rustfs_setup:
condition: service_completed_successfully
extends:
service: rest-catalog
file: ./.docker/docker-compose-infra.yml
# Optional for rate-limiting
# redis:
# extends:
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ type StorageConfigType = {
icebergShards: string[]
icebergCatalogUrl: string
icebergCatalogAuthType: IcebergCatalogAuthType
icebergCatalogSigV4Service: string
icebergCatalogPurgeOnDrop: boolean
icebergCatalogToken?: string
icebergMaxNamespaceCount: number
icebergMaxTableCount: number
Expand Down Expand Up @@ -901,6 +903,10 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
icebergBucketDetectionMode:
getOptionalConfigFromEnv('ICEBERG_BUCKET_DETECTION_MODE') || `BUCKET`,
icebergCatalogAuthType: getOptionalConfigFromEnv('ICEBERG_CATALOG_AUTH_TYPE') || `sigv4`,
icebergCatalogSigV4Service:
getOptionalConfigFromEnv('ICEBERG_CATALOG_SIGV4_SERVICE') || 's3tables',
icebergCatalogPurgeOnDrop:
getOptionalConfigFromEnv('ICEBERG_CATALOG_PURGE_ON_DROP') !== 'false',
icebergCatalogToken: getOptionalConfigFromEnv('ICEBERG_CATALOG_AUTH_TOKEN'),
icebergMaxCatalogsCount: parseInt(getOptionalConfigFromEnv('ICEBERG_MAX_CATALOGS') || '2', 10),
icebergMaxNamespaceCount: parseInt(
Expand Down
4 changes: 2 additions & 2 deletions src/storage/protocols/iceberg/catalog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
import { getConfig } from '../../../../config'

export function getCatalogAuthStrategy(authType: string): CatalogAuthType {
const { storageS3Region, icebergCatalogToken } = getConfig()
const { storageS3Region, icebergCatalogToken, icebergCatalogSigV4Service } = getConfig()

switch (authType) {
case 'sigv4':
return new SignV4Auth({ region: storageS3Region })
return new SignV4Auth({ region: storageS3Region, service: icebergCatalogSigV4Service })
case 'token':
if (!icebergCatalogToken) {
throw new Error('Iceberg catalog token is not configured')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ErrorCode, type StorageBackendError } from '@internal/errors'
import type { SignRequestOptions } from 'aws-sigv4-sign'
import JSONBigint from 'json-bigint'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getConfig } from '../../../../config'
import { spyOnAbortSignalTimeout } from '../../../../test/utils/abort-signal'

const mockSignRequest = vi.fn()
Expand Down Expand Up @@ -105,6 +106,60 @@ describe('RestCatalogClient request pipeline', () => {
expect(fetchedUrl).toBe(signedUrl)
})

it('signs for the s3tables service by default and for a configured service', async () => {
const url = 'https://s3tables.ap-southeast-1.amazonaws.com/iceberg/v1'

await new RestCatalogClient({
catalogUrl: url,
auth: new SignV4Auth({ region: 'us-east-1' }),
}).getConfig({ warehouse: 'wh' })
await new RestCatalogClient({
catalogUrl: url,
auth: new SignV4Auth({ region: 'us-east-1', service: 's3' }),
}).getConfig({ warehouse: 'wh' })

expect(mockSignRequest.mock.calls[0][2]).toMatchObject({ service: 's3tables' })
expect(mockSignRequest.mock.calls[1][2]).toMatchObject({ service: 's3' })
})

it('sends purgeRequested on dropTable only when asked to purge and purging is enabled', async () => {
const client = new RestCatalogClient({
catalogUrl: 'https://catalog.example.com/iceberg/v1',
auth: new BearerTokenAuth({ token: 'token' }),
})
const dropUrl = () => new URL(fetchMock.mock.calls.at(-1)?.[0] as string)

try {
vi.stubEnv('ICEBERG_CATALOG_PURGE_ON_DROP', 'true')
getConfig({ reload: true })

await client.dropTable({ warehouse: 'wh', namespace: 'ns', table: 'tbl' })
expect(dropUrl().searchParams.has('purgeRequested')).toBe(false)

await client.dropTable({
warehouse: 'wh',
namespace: 'ns',
table: 'tbl',
purgeRequested: true,
})
expect(dropUrl().searchParams.get('purgeRequested')).toBe('true')

// Catalogs such as RustFS reject purgeRequested=true
vi.stubEnv('ICEBERG_CATALOG_PURGE_ON_DROP', 'false')
getConfig({ reload: true })
await client.dropTable({
warehouse: 'wh',
namespace: 'ns',
table: 'tbl',
purgeRequested: true,
})
expect(dropUrl().searchParams.has('purgeRequested')).toBe(false)
} finally {
vi.unstubAllEnvs()
getConfig({ reload: true })
}
})

it('produces identical signed and fetched params even when params include empty strings', async () => {
const client = new RestCatalogClient({
catalogUrl: 'https://host.example.com/v1',
Expand Down
10 changes: 8 additions & 2 deletions src/storage/protocols/iceberg/catalog/rest-catalog-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ERRORS, StorageBackendError } from '@internal/errors'
import { type SignRequestOptions, signRequest } from 'aws-sigv4-sign'
import JSONBigint from 'json-bigint'
import { getConfig } from '../../../../config'
import {
createAlreadyExistsError,
createAuthenticationTimeoutError,
Expand Down Expand Up @@ -911,7 +912,9 @@ export class RestCatalogClient {
const warehouse = this.getEncodedWarehouse(params.warehouse)
const query: Record<string, string> = {}

if (params.purgeRequested) {
// AWS S3 Tables only accepts purged drops, while catalogs such as RustFS reject
// `purgeRequested=true`. ICEBERG_CATALOG_PURGE_ON_DROP=false drops the parameter for the latter.
if (params.purgeRequested && getConfig().icebergCatalogPurgeOnDrop) {
query.purgeRequested = 'true'
}

Expand Down Expand Up @@ -972,11 +975,14 @@ export class RestCatalogClient {
* SignV4Auth class for AWS Signature Version 4 authentication
* This class implements the CatalogAuth interface
* to sign requests to the S3Tables service.
* S3-compatible catalogs that are not AWS S3 Tables (e.g. RustFS) can sign with a different
* service name via `service`.
*/
export class SignV4Auth {
constructor(
private readonly opts: {
region: string
service?: string
credentials?: SignRequestOptions['credentials']
}
) {}
Expand All @@ -990,7 +996,7 @@ export class SignV4Auth {
body: req.body,
},
{
service: 's3tables',
service: this.opts.service ?? 's3tables',
region: this.opts.region,
credentials: this.opts.credentials,
}
Expand Down
Loading