Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/skade

.nornir/catalog-connectors-design.md

skade::connect — one trait for opening a lake's catalog

Design (chosen path only). skade-flight, knut-bifrost and korp all need "open that lake's Iceberg catalog". One trait in the skade LIBRARY answers it, so no consumer grows its own Nessie or S3 client.

f0 — the whole idea

#[async_trait]
pub trait CatalogConnector: Send + Sync + Debug {
    fn kind(&self) -> &'static str;   // "skade-local" | "nessie-v2" | "iceberg-rest" | "polaris" | "skade-grpc" | …
    fn describe(&self) -> String;     // endpoint / ref / warehouse — never a secret
    fn read_only(&self) -> bool;
    async fn open(&self) -> skade::Result<Arc<dyn iceberg::Catalog>>;
}

A connector is a plain config struct; open yields an iceberg::Catalog, which is all a reader needs — skade's read path (read_all, read_delta, read_changelog, …) works on any loaded iceberg::table::Table.

connector kind feature read-only
SkadeLocal { dir } skade-local always no
SkadeObjectStore { db_path, warehouse_uri, store } skade-object-store always (backend features rustfs/s3/aws-s3) no
NessieV2 { uri, reference, token, tls, s3 } nessie-v2 nessie always
IcebergRest { uri, warehouse, prefix_or_ref, token, credential, scope, oauth2_server_uri, headers, vended_credentials, tls, s3, read_only, sigv4 } iceberg-rest rest-catalog configurable
AIStor { s3, warehouse, read_only } aistor catalog-aistor configurable
Polaris { uri, catalog, credential, scope, token, tls, vended_credentials, s3, read_only } polaris catalog-polaris configurable
UnityCatalog { uri, catalog, token, tls, vended_credentials, s3, read_only } unity catalog-unity by default (new)
Gravitino { uri, warehouse, simple_user, credential, scope, token, tls, vended_credentials, s3, read_only } gravitino catalog-gravitino configurable
SkadeGrpc { uri, tls, s3, read_only } (s3: None → local fs) skade-grpc grpc-catalog configurable

catalogs turns on the four vendor presets. One feature per connector, so a consumer (knut's bifrost-falkordb-<vendor> build rows) carries only the catalog it talks to.

NessieV2 — an existing Nessie lake over API v2

For a Nessie that must not be reconfigured and whose Iceberg-REST side is not set up (Spark on Nessie's native API, table locations s3a://…):

need Nessie API v2
resolve the reference GET {uri}/trees/{ref} → reference.hash
namespaces, tables GET {uri}/trees/{ref}@{hash}/entries (paged by page-token)
metadata location POST {uri}/trees/{ref}@{hash}/contents {"requestedKeys":[{"elements":[…]}]}

then the metadata JSON through the storage factory, table built readonly(true).

IcebergRest — a lake behind an Iceberg REST catalog

Nessie's /iceberg endpoint once it is set up, Polaris, or a remote skade-katalog serve: the vendored iceberg-catalog-rest-arrow59 client (the one skade's bench drives Nessie and Polaris with). Its reqwest client is built from IcebergRest.tls (use_preconfigured_tls with the one rustls config) and handed in with RestCatalogBuilder::with_client, so it survives the /v1/config refresh. The branch rides the REST prefix; opening makes no network call; read_only: true wraps the catalog so every write is FeatureUnsupported.

OAuth2, headers, vended credentials

credential (client_id:client_secret) is exchanged for a token at oauth2_server_uri (default <uri>/v1/oauth/tokens) with scope; headers go on every catalog request. No secret is ever printed by describe. vended_credentials: true sends X-Iceberg-Access-Delegation: vended-credentials and reads each table's data with the s3.* keys the catalog returns for it (s3.access-key-id, s3.secret-access-key, s3.session-token). The vendored REST client picks the longest matching storage-credential prefix over the config's keys; aegir signs the STS session token as x-amz-security-token. IcebergRest.s3 stays the base: endpoint, bucket, TLS, and the fallback keys when the catalog vends none.

Vendor presets — config over IcebergRest

Each preset is a plain struct whose rest() returns the IcebergRest it is; rest_preset! implements CatalogConnector by delegating. No new dependency.

preset catalog URI warehouse auth vended
AIStor <s3 endpoint>/_iceberg table bucket SigV4 s3tables, the S3 keys no
Polaris <root>/api/catalog Polaris catalog OAuth2 client creds, scope PRINCIPAL_ROLE:ALL on
UnityCatalog <root>/api/2.1/unity-catalog/iceberg Unity catalog bearer token on; read-only by default
Gravitino <root>/iceberg warehouse none, simple_user (HTTP Basic), OAuth2 or bearer configurable

SkadeGrpc — a remote skade-katalog over gRPC

skade-katalog serve --grpc, over skade's own skade.katalog.v1 wire (tonic, rustls). The client is skade_katalog_grpc::GrpcCatalog (crate feature tls; connect_lazy makes no network call). Metadata travels on the wire; data is read through the connector's own storage — a shared file:// warehouse or S3/MinIO over aegir. TLS is ClientTls, the same type as every other leg.

Shared S3 wiring

One helper builds the iceberg storage from an S3Config: skade's aegir store (ObjectStoreConfig::Aegir, SigV4 over ureq). S3Config::strip_to_key maps s3://, s3a:// and s3n:// to the same key — Spark's Hadoop schemes read like s3://. Credentials are the connector's unless vended_credentials is on (see above); one bucket per connector.

TLS — one type, every leg, mode by configuration

skade::tls::ClientTls { ca_pem, client_cert_pem, client_key_pem } (PEM file paths, serde-defaulted) and ONE builder, ClientTls::rustls_config (pure rustls, ring provider). Every leg takes it: S3Config.tls, NessieV2.tls and IcebergRest.tls (and through it every preset) and SkadeGrpc.tls.

endpoint tls mode
http:// any plain
https:// None TLS, webpki roots
https:// ca_pem TLS, that CA
https:// ca_pem + cert + key mutual TLS

Production today — Nessie plain or TLS without a client certificate, MinIO mutual TLS — is configuration, not code; either leg switches mode by settings. A certificate without its key (or the reverse) is refused by name.

Proven by