Skip to content

fix(iceberg): one catalog-kind table, so every emitter agrees on location.catalog - #707

Merged
fas89 merged 3 commits into
mainfrom
fix/iceberg-catalog-kinds
Oct 7, 2026
Merged

fas89 merged 3 commits into
mainfrom
fix/iceberg-catalog-kinds

Conversation

@fas89

@fas89 fas89 commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Description

binding.location.catalog is a free string, and every emitter classified it by hand. They disagreed:

  • catalog: lakekeeper: streamed over REST, while dbt's catalogs.yml wrote a Snowflake-managed table and the AWS IaC created a second, metadata-less Glue table of the same name.
  • The sink validator: checked only the literal rest.
  • catalog: hive on Snowflake: became a Snowflake-managed table.
  • A Confluent binding: published to Glue whatever catalog it named.

A separate defect: the Kafka Connect deriver set both iceberg.catalog.type and iceberg.catalog.catalog-impl for Glue. Apache Iceberg's CatalogUtil refuses that, so every Glue sink failed at startup:

java.lang.IllegalArgumentException: Cannot create catalog iceberg, both type and catalog-impl are set

This PR adds one catalog-kind table in fluid_build/providers/_iceberg_catalog.py, and every emitter now reads from it.

  • Shape: borrowed from dbt-adapters' _V2_TO_V1_TYPE alias hook and Airbyte's typed Polaris preset.
  • Wire values: come only from Iceberg's CatalogUtil set. No engine or SDK has a vendor type such as lakekeeper.
  • Readers: the streaming sinks, dbt catalogs.yml, the Snowflake/AWS/Confluent IaC, the native AWS planner, fluid policy compile, fluid diff, fluid test and fluid validate.

Behaviour changes, all fail-closed:

  • Kafka Connect: emits catalog-impl or type, never both (the Debezium twin already did). fluid validate refuses overrides that would add the other key back.
  • AWS: an Iceberg expose in another catalog keeps its S3 bucket but gets no Glue database, table, import, Glue IAM grant or Lake Formation resource.
    • LF, column restrictions and row filters on such an expose are refused by catalog name, at validate and at apply.
    • Unknown catalog values are refused at apply.
    • No catalog or glue produces byte-identical output.
  • fluid apply (AWS): stops with iceberg_catalog_move_blocked when state holds Glue resources an earlier release created for such a table, and prints the tofu state rm commands. Without the guard, the next plan would destroy them, and destroying a Glue database deletes every table in it. This mirrors the packaging ownership-transition guard.
  • fluid validate refuses:
    • unknown kinds;
    • a sink.catalog that disagrees with the expose;
    • a missing uri/warehouse on any REST catalog;
    • Snowflake catalogs that have no integration (hive, jdbc, hadoop, dynamodb);
    • two catalog: snowflake exposes that share one EXTERNAL VOLUME.
  • fluid validate gate crashes: a crash in an Iceberg or Confluent gate now fails validation instead of printing only with --verbose.
  • Runner preflight: the Kafka Connect runner and the embedded Debezium Server runner run the same checks before they create anything.
  • RFC-streaming-extension: corrected. The Apache sink creates namespaces on auto-create since Iceberg 1.6.0 (Kafka-connect: Handle namespace creation for auto table creation apache/iceberg#10186).

See CHANGELOG [Unreleased] for the upgrade note and every fixed behaviour.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would break existing functionality)
  • Documentation update
  • Refactor / code cleanup
  • New provider or provider enhancement

Contracts that were previously accepted and wrote to the wrong catalog now fail fluid validate on purpose. Each case has a CHANGELOG line.

Documentation

Checklist

  • I have read the Contributing Guide
  • My code follows the project's coding standards
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (pytest) — see Testing for 4 local-environment failures that fail identically on main
  • I have run ruff check and black (24.10.0) with no errors
  • New maintained Python files include license headers

Testing

Unit and contract tests. The new tests fail against the old code (negative controls run per slice):

  • tests/providers/test_iceberg_catalog_kinds.py: the cross-emitter agreement matrix. Every emitter runs over every kind, including absent and unknown, and each answer is checked against the kind's row.
  • test_iac_aws_iceberg_catalog.py and test_iac_catalog_moves.py.
  • The sink-validator and runner-preflight suites.
  • The dbt, Snowflake, Confluent, planner and policy-compiler suites.
  • The schema suite.

Full suite: 20772 passed, 552 skipped on the rebased commit. Four failures also fail identically on main (e0a8b258) with the same venv:

  • tests/iac/test_iac_moto_e2e.py: three tests.
  • tests/test_coding_agent_live.py: "NOT run in CI"; it drives a local claude CLI.

Lint: ruff check, black --check (24.10.0) and scripts/mypy_strict.py are clean.

Security review of e0a8b258..aa940e36: no findings.

  • Every governance input on a non-Glue Iceberg table is refused up front, never dropped without an error.
  • The tofu state rm commands are shlex-quoted.
  • No connector config is logged.

Tested live

New CI lane: lakekeeper-integration in the emulated-heavy workflow. It runs a real Kafka Connect worker (cp-kafka-connect 7.9.10, Apache Iceberg sink 1.9.2) against a real Lakekeeper v0.13.6. Credentials are vended over STS from an S3-compatible store, so no static storage keys are used.

Test Asserts
test_derived_lakekeeper_config_streams_into_lakekeeper Forge's derived config for catalog: Lakekeeper streams 5 records and commits a snapshot with total-records = 5. The sink auto-creates the namespace.
test_both_type_and_catalog_impl_fail_on_a_real_worker The task FAILS. Its status trace and the worker log both carry the CatalogUtil refusal above.
test_derived_glue_config_gets_past_the_catalog_gate Forge's derived Glue config (impl only) reaches RUNNING with no catalog refusal.

assert_lane_coverage.py --require fails the job if every test skipped.

Local smoke run:

tests/integration/test_lakekeeper_kafka_connect_live.py::test_both_type_and_catalog_impl_fail_on_a_real_worker PASSED
tests/integration/test_lakekeeper_kafka_connect_live.py::test_derived_glue_config_gets_past_the_catalog_gate PASSED
tests/integration/test_lakekeeper_kafka_connect_live.py::test_derived_lakekeeper_config_streams_into_lakekeeper PASSED
======================== 3 passed in 157.82s (0:02:37) =========================

Negative control: restoring the old "type always, plus impl" emission fails the Glue test and the unit matrix.

fas89 added 3 commits October 6, 2026 23:09
…tion.catalog

The streaming sinks, dbt catalogs.yml, the Snowflake, AWS and Confluent IaC, the
native AWS planner, policy compile, fluid diff and fluid test each classified the
free-string location.catalog by hand, and disagreed: `catalog: lakekeeper`
streamed over REST while dbt wrote a Snowflake-managed table and the AWS IaC
created a metadata-less Glue table of the same name. One table in
providers/_iceberg_catalog.py now answers for every emitter (shape borrowed from
dbt-adapters' _V2_TO_V1_TYPE hook and Airbyte's typed Polaris preset; wire
values only from Iceberg's CatalogUtil set).

Also fixes the Kafka Connect Glue sink, which set both iceberg.catalog.type and
catalog-impl and so failed at startup ("both type and catalog-impl are set";
reproduced against cp-kafka-connect 7.9.10 + the Apache Iceberg sink 1.9.2).

- AWS: no Glue database/table/import/grant/LF resource for an Iceberg table in
  another catalog; LF, column restrictions and row filters on one are refused by
  catalog name; unknown catalog values are refused at apply. Absent or glue is
  byte-identical.
- fluid apply stops with iceberg_catalog_move_blocked (and prints tofu state rm
  commands) when state holds Glue resources an earlier release created for such
  a table, instead of planning to destroy them.
- fluid validate: unknown kinds, sink/expose disagreement, uri+warehouse for
  every REST catalog, type+impl overrides, snowflake volume collisions; a crash
  in an Iceberg/Confluent gate now fails validation instead of passing silently.
- The Kafka Connect and embedded Debezium Server runners preflight the same
  checks before they create anything.
- RFC-streaming-extension: the Apache sink creates namespaces on auto-create
  since Iceberg 1.6.0 (apache/iceberg#10186); the deriver emits impl XOR type.
…orker

Real Kafka Connect + Apache Iceberg sink 1.9.2 + Lakekeeper v0.13.6 (STS-vended
credentials, no storage keys) in the admin-gated emulated-heavy lane:
- forge's derived config for catalog: lakekeeper streams and commits records;
- a config with both iceberg.catalog.type and catalog-impl fails on the worker
  (the startup crash every Glue sink hit);
- forge's derived Glue config (impl only) gets past that gate.
assert_lane_coverage --require keeps the job from passing with every test
skipped. Local smoke: 3 passed in 158s.
warehouse_is_name and uri_suffix had no row that set them in this change, so
their two sink checks could never fire. They return with the Lakekeeper-specific
validation that sets them.
@fas89 fas89 added the ci:integration-emulated Maintainer vouch — run the admin-gated heavy-emulated lane (bigquery-emulator + LocalStack) label Oct 6, 2026
@fas89
fas89 deployed to integration-emulated October 6, 2026 21:41 — with GitHub Actions Active
@fas89
fas89 had a problem deploying to integration-emulated October 6, 2026 21:41 — with GitHub Actions Failure
@github-actions github-actions Bot added provider Changes or requests related to providers ci Continuous integration and automation changes cli Changes to the CLI surface or implementation docs Documentation changes tests Test coverage or test infrastructure changes labels Oct 6, 2026
@fas89
fas89 merged commit 9acc511 into main Oct 7, 2026
41 of 42 checks passed
fas89 added a commit that referenced this pull request Oct 7, 2026
…atalog-move error to its docs

The upgrade note named rest/iceberg_rest/polaris/unity/nessie as having had a
Snowflake EXTERNAL VOLUME; they never did (the pre-#707 external set). The kinds
that lost one are lakekeeper, bigquery, the iceberg-rest spelling, hive, jdbc,
hadoop and dynamodb, which is what the guard checks.

iceberg_catalog_move_blocked was not in the error catalog, so the CLI printed no
docs link for it; it now links the guard's section in cli/apply.
fas89 added a commit that referenced this pull request Oct 7, 2026
…709)

* fix(iceberg): close the gaps the post-merge inspection of #707 found

- CLI output keeps square brackets: validate messages, CLIError context and
  suggestions, and FluidCLIError.format_for_user print literally (Rich ate
  exposes[orders]); printed tofu state rm commands stay on one line.
- Runners check the sink they push: one iceberg_sink_plan gate shared by the
  validator, the preflight and both runners; hand-written configs are
  preflighted; each runner reads the build it executes, not builds[0].
- GCP: an Iceberg sink target with no location.catalog is refused (sink REST vs
  dbt-bigquery/GCP IaC BigLake); a sink.catalog/hand-written type that reaches
  BigLake is not a split.
- catalog: bigquery on Kafka Connect warns (sink 1.9.2 has no bigquery type);
  runtime warnings follow the catalog type that reaches the worker.
- Unknown catalog values: the native AWS planner refuses them like the IaC,
  without a plan_failed log line; a typo no longer also draws a Lake Formation
  refusal.
- Catalog-move guard: a Glue database is flagged only when the moved expose's
  own table is in state; Snowflake external volumes are guarded too; probe
  failures log at WARNING; wiring proven by behaviour tests through
  apply_via_opentofu.
- Agreement matrix extended to the planner, policy compiler, Confluent and
  dbt-BigQuery.

* fix(iceberg): correct which Snowflake kinds lost a volume; link the catalog-move error to its docs

The upgrade note named rest/iceberg_rest/polaris/unity/nessie as having had a
Snowflake EXTERNAL VOLUME; they never did (the pre-#707 external set). The kinds
that lost one are lakekeeper, bigquery, the iceberg-rest spelling, hive, jdbc,
hadoop and dynamodb, which is what the guard checks.

iceberg_catalog_move_blocked was not in the error catalog, so the CLI printed no
docs link for it; it now links the guard's section in cli/apply.

* style: one import style per module in the bracket tests; explicit concatenation in the catalog-move guidance

* style: import the apply engine module directly in the bracket tests

* fix(iceberg): four gaps the defect scan found in this PR's own checks

- Catalog-move guard: a moved Iceberg expose that names no table had its
  v0.19.0 Glue database planned for destroy (the evidence rule needed a table);
  a database is now flagged when the expose's own table is in state OR the
  expose names no table. The shared-database false positive stays fixed.
- Snowflake guard: the old volume is found by its contract-derived name, so an
  upgrade that also set location.warehouse to a catalog name or dropped
  iam_role_arn is still stopped.
- GCP split gate: keyed on the catalog that reaches the worker, so a sink that
  reaches Glue/DynamoDB through catalog-impl (sink.catalog: glue, hand-written
  catalog-impl) is refused like a REST one.
- A hand-written sink config or override whose type/catalog-impl selects a
  different catalog than the expose's is an error on every platform (following
  the GCP remedy while a hand-written config wrote REST used to pass).

Catalog impl class names checked against apache/iceberg CatalogUtil.

This branch had an error being deployed

1 failed deployment
integration-emulated — 05dbe18b Deployed Oct 6, 2026 by fas89 via Heavy emulated integration (GCP emulators + LocalStack) #384
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:integration-emulated Maintainer vouch — run the admin-gated heavy-emulated lane (bigquery-emulator + LocalStack) ci Continuous integration and automation changes cli Changes to the CLI surface or implementation docs Documentation changes provider Changes or requests related to providers tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant