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).
- One commit per catalog walk. A top-level
list_namespaces— the first call of every DataFusion provider build — re-resolves the reference; later lists and loads readref@hash. Opening resolves once, so a bad URI/ref/token fails at open. - Keys in a POST body, never a path segment: no escaping for dots or slashes.
- Namespaces derived from entries (
NAMESPACEcontent + every table's parent). - Every write →
ErrorKind::FeatureUnsupported, message names read-only. - HTTP is the ureq already in the tree (rustls), on
spawn_blocking; bearer token when set. No new dependency.
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.
- Why aegir for S3: rust-s3 0.35 builds its own rustls 0.21 config with
with_no_client_auth()and takes none from outside, so it cannot present a client certificate. aegir (skade's pure-Rust client) gainedClient::with_tls_config(Arc<rustls::ClientConfig>).RustS3StoreandAwsS3Storerefuse anS3Configwithtls, by name. S3Configwithouttls(old JSON) still deserializes.
Proven by
skade/tests/tls.rs— the builder in all three https modes + refusals; a REAL S3 round-trip over mutual TLS throughAegirS3Storeagainst an in-process rustls server that requires a client certificate (CA-only and roots-only clients refused);NessieV2over TLS with a custom CA (no client cert, no auth) against an in-process rustls server; oldS3ConfigJSON.skade/tests/connect.rs— aVec<Arc<dyn CatalogConnector>>(local, object store, Nessie mock) opened and walked through one code path; NessieV2 against an in-process mock of Nessie 0.105's v2 shapes reading a real skade table whose metadata and data sit unders3a://in aMemoryStore(pinning, paging, views not tables,TableNotFound, write refusals, bearer token on every request).object_store.rss3_key_tests— the three schemes map to one key.connect/rest.rsunit tests — REST props and redaction, the http client built plain / webpki / custom CA and a half identity refused by name, read-only refusals;tests/connect.rswalks anIcebergRestover a plain-http mock of the Iceberg REST catalog through the same trait path as the others.connect/{aistor,polaris,unity,gravitino}.rsunit tests — each preset's URI, warehouse, auth props and read-only default;rest.rs— OAuth2, headers and vended-credentials props, and thatdescriberedacts them;object_store.rsvendedtests — the per-table keys and session token reach aegir.- vendored
iceberg-catalog-rest-arrow59—the_longest_matching_storage_credential_wins_over_config. bench/aegira_session_token_is_sent_and_signed.grpc/tests/client_roundtrip.rsandconnect/grpc.rs—GrpcCatalogandSkadeGrpcagainst an in-processskade-kataloggRPC server.