Skip to content

Iceberg REST Catalog API

IOLake includes an Apache Iceberg REST Catalog API, enabling query engines like Spark, Trino, and Flink to discover and manage Iceberg tables.

Base URL

All Iceberg endpoints are served under /iceberg/v1/. Clients are given the catalog URI without /v1 and add it themselves:

http://gateway:9000/iceberg

Every catalog lives in a warehouse, named by the client's warehouse setting.

Authentication

Every request carries a credential, as on S3: a SigV4 signature with an access key (the same keys as S3), an OIDC bearer token from the configured identity provider (--oidc-issuer-url), or a console session. A request with none, or with one that doesn't check out (a bad signature, an unknown key, a token the provider doesn't accept), is refused with 401. A key scoped to a bucket or prefix is refused with 403: it is for that data, not for the catalog. Only under --no-auth are the endpoints open, and then nothing is checked.

Warehouses

Every request names a warehouse: ?warehouse=<name> on /v1/config, and from then on the ws/<name> prefix the configuration returns (/iceberg/v1/ws/<name>/namespaces/...). A warehouse belongs to a tenant; a caller sees only its own tenant's warehouses (a catalog admin sees all), and one of another tenant is answered as unknown.

Warehouses are managed on the admin API, not here: POST /_admin/warehouses ({"name": "..."}, created in the caller's tenant), GET /_admin/warehouses and DELETE /_admin/warehouses/{name}, by the system admin or an admin of the warehouse's tenant. Each has a backing bucket iceberg-<name>; a warehouse is deleted only once that bucket is empty (409 otherwise), since its bucket goes with it.


Warehouses

A warehouse is a catalog with its own namespaces and tables, and a bucket, iceberg-<name>, made with it for its data. Warehouses are managed on the admin API (iolakectl warehouse …):

Call Who
GET /_admin/warehouses system admin (every tenant's), or a tenant's admin (its tenant's) {"warehouses": [{"name", "bucket", "location", "tenant", "created_at", "properties"}]}
POST /_admin/warehouses system admin, or the tenant's admin Body {"name": "...", "tenant": "...", "properties": {...}}; tenant defaults to the caller's. Answers the new warehouse; 400 if it or its bucket exists
DELETE /_admin/warehouses/{name} system admin, or its tenant's admin
iolakectl warehouse create analytics      # also creates bucket iceberg-analytics
iolakectl warehouse list

Configuration

GetConfig

Get catalog configuration for a warehouse (the warehouse parameter is required).

GET /iceberg/v1/config?warehouse=analytics

Response:

{
  "defaults": {
    "warehouse": "s3://iceberg-analytics"
  },
  "overrides": {
    "prefix": "ws/analytics"
  }
}

The client then sends every call under /iceberg/v1/ws/analytics/….


Namespace Operations

ListNamespaces

List all namespaces, optionally filtered by parent.

GET /iceberg/v1/namespaces
GET /iceberg/v1/namespaces?parent=parent_ns

Response:

{
  "namespaces": [
    ["analytics"],
    ["production"]
  ]
}

CreateNamespace

Create a new namespace.

POST /iceberg/v1/namespaces

Request Body:

{
  "namespace": ["analytics"],
  "properties": {
    "owner": "alice",
    "description": "Analytics data"
  }
}

Response:

{
  "namespace": ["analytics"],
  "properties": {
    "owner": "alice",
    "description": "Analytics data"
  }
}

LoadNamespace

Get namespace details.

GET /iceberg/v1/namespaces/{namespace}

Response:

{
  "namespace": ["analytics"],
  "properties": {
    "owner": "alice"
  }
}

NamespaceExists

Check if a namespace exists.

HEAD /iceberg/v1/namespaces/{namespace}

Response: 204 No Content (exists) or 404 Not Found


DropNamespace

Delete a namespace. Must be empty (no tables).

DELETE /iceberg/v1/namespaces/{namespace}

Response: 204 No Content


UpdateNamespaceProperties

Update namespace properties.

POST /iceberg/v1/namespaces/{namespace}/properties

Request Body:

{
  "updates": {
    "description": "Updated description"
  },
  "removals": ["old_property"]
}

Response:

{
  "updated": ["description"],
  "removed": ["old_property"],
  "missing": []
}

Table Operations

ListTables

List tables in a namespace.

GET /iceberg/v1/namespaces/{namespace}/tables

Response:

{
  "identifiers": [
    {
      "namespace": ["analytics"],
      "name": "events"
    }
  ]
}

CreateTable

Create a new table.

POST /iceberg/v1/namespaces/{namespace}/tables

Request Body:

{
  "name": "events",
  "schema": {
    "type": "struct",
    "fields": [
      {"id": 1, "name": "id", "type": "long", "required": true},
      {"id": 2, "name": "data", "type": "string", "required": false}
    ]
  },
  "properties": {}
}

Response:

{
  "metadata": {
    "format-version": 2,
    "table-uuid": "...",
    "location": "s3://iceberg-analytics/analytics/events",
    "schema": { ... }
  },
  "metadata-location": "s3://iceberg-analytics/analytics/events/metadata/00000-....metadata.json"
}

A table lives inside its warehouse: a location in the request, or one set later by a commit (set-location), must be the warehouse's location or a path under it, or the request is refused with 400. The table's vended credentials are scoped to its location.


LoadTable

Get table metadata.

GET /iceberg/v1/namespaces/{namespace}/tables/{table}

Response: Same format as CreateTable response, with vended credentials for the table's data in config (s3.access-key-id, s3.secret-access-key, s3.session-token, s3.endpoint). They are scoped to the table's location, last an hour, and are read-write only for a caller who may also commit to the table (iceberg:UpdateTable); anyone else gets read-only ones. They stay inside the warehouse's tenant: they reach nothing in another tenant's buckets.

A caller that iceberg:LoadTable isn't granted to gets 403 and no credentials.


TableExists

Check if a table exists.

HEAD /iceberg/v1/namespaces/{namespace}/tables/{table}

Response: 204 No Content (exists) or 404 Not Found


UpdateTable

Update table metadata (schema evolution, property changes).

POST /iceberg/v1/namespaces/{namespace}/tables/{table}

DropTable

Delete a table.

DELETE /iceberg/v1/namespaces/{namespace}/tables/{table}

Response: 204 No Content


RenameTable

Rename a table (optionally across namespaces).

POST /iceberg/v1/tables/rename

Request Body:

{
  "source": {
    "namespace": ["analytics"],
    "name": "events"
  },
  "destination": {
    "namespace": ["analytics"],
    "name": "events_v2"
  }
}

Response: 204 No Content


Access Control

Policies, tags, quotas, encryption requirements and role bindings are set by the warehouse's administrators: the system admin, the principals named by --oidc-admin-roles, and the admins of the warehouse's tenant. The namespace properties that hold them (names starting __) can be changed only by them, through these endpoints or through UpdateNamespaceProperties. Data filters are managed by the system admin and the --oidc-admin-roles principals only.

Namespace Policy

Set a policy on a namespace (admin only).

PUT /iceberg/v1/namespaces/{namespace}/policy

Request Body:

{
  "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"OBIO\":[\"arn:objectio:iam::user/alice\"]},\"Action\":[\"iceberg:*\"],\"Resource\":[\"arn:obio:iceberg:::analytics/*\"]}]}"
}

Table Policy

Set a policy on a table (admin only).

PUT /iceberg/v1/namespaces/{namespace}/tables/{table}/policy

Request Body: Same format as namespace policy.

Policy Evaluation

A request is decided by the warehouse's catalog policy (PUT /v1/catalog/policy), the policy of each level of the namespace and, for a table, the table's policy, the way S3 decides on a bucket policy:

  • Explicit Deny anywhere refuses the request (403).
  • Explicit Allow anywhere grants it.
  • With neither (no policy, or none naming the caller), only the warehouse's administrators go ahead, as only a bucket's owner does on S3. Anyone else is refused: a tenant's users need a policy that allows them.
  • A stored policy that can't be read refuses the request rather than being skipped, and so does a policy meta can't be asked for (503, retry).
  • The system admin is not subject to policies, so no policy can lock the operator out.

Iceberg Actions

Action Description
iceberg:ListNamespaces List namespaces
iceberg:CreateNamespace Create a namespace
iceberg:LoadNamespace Load namespace details
iceberg:DropNamespace Delete a namespace
iceberg:UpdateNamespaceProperties Update namespace properties
iceberg:ListTables List tables in a namespace
iceberg:CreateTable Create a table
iceberg:LoadTable Load table metadata
iceberg:UpdateTable Update table metadata
iceberg:DropTable Delete a table
iceberg:RenameTable Rename a table

Resource ARNs

  • Namespace: arn:obio:iceberg:::{namespace}
  • Table: arn:obio:iceberg:::{namespace}/{table}

Client Configuration

The URI is .../iceberg: Iceberg's REST clients add /v1 themselves, so .../iceberg/v1 makes them call /iceberg/v1/v1/…. The warehouse is a warehouse's name, not an s3:// location. The pyiceberg example in the code repo (examples/pyiceberg) signs with SigV4 (rest.sigv4-enabled, rest.signing-name s3) and runs against iolake-aio. PyIceberg signs with botocore's generic SigV4 signer, over the hash of each request's body without sending it in x-amz-content-sha256; the gateway hashes the body to check the signature, up to 2 MiB a request (the signed body); no query engine is tested against the catalog in CI yet (iolake#8), so treat the Spark and Trino settings below as a starting point.

PyIceberg

from pyiceberg.catalog.rest import RestCatalog

catalog = RestCatalog(
    name="iolake",
    uri="http://gateway:9000/iceberg",
    warehouse="analytics",
    **{
        "rest.sigv4-enabled": "true",
        "rest.signing-name": "s3",
        "rest.signing-region": "us-east-1",
        "s3.endpoint": "http://gateway:9000",
        "s3.path-style-access": "true",
        "s3.access-key-id": "...",
        "s3.secret-access-key": "...",
    },
)

Apache Spark

spark = SparkSession.builder \
    .config("spark.sql.catalog.iolake", "org.apache.iceberg.spark.SparkCatalog") \
    .config("spark.sql.catalog.iolake.type", "rest") \
    .config("spark.sql.catalog.iolake.uri", "http://gateway:9000/iceberg") \
    .config("spark.sql.catalog.iolake.warehouse", "analytics") \
    .getOrCreate()

# Use the catalog
spark.sql("CREATE NAMESPACE iolake.analytics")
spark.sql("CREATE TABLE iolake.analytics.events (id BIGINT, data STRING)")

Trino

connector.name=iceberg
iceberg.catalog.type=rest
iceberg.rest-catalog.uri=http://gateway:9000/iceberg
iceberg.rest-catalog.warehouse=analytics

Error Responses

Iceberg errors follow the Iceberg REST API error format:

{
  "error": {
    "message": "Namespace does not exist: analytics",
    "type": "NoSuchNamespaceException",
    "code": 404
  }
}

Error Types

Type HTTP Status Description
NoSuchNamespaceException 404 Namespace not found
NamespaceAlreadyExistsException 409 Namespace already exists
NoSuchTableException 404 Table not found
AlreadyExistsException 409 Table already exists
ForbiddenException 403 Access denied by policy
BadRequestException 400 Invalid request