Skip to content

fix(contracts): resolve contract DNS hosts via the renderer's naming rule - #1028

Merged
RonaldHensbergen merged 2 commits into
mainfrom
fix/1010-contract-hostname
Oct 30, 2026
Merged

RonaldHensbergen merged 2 commits into
mainfrom
fix/1010-contract-hostname

Conversation

@RonaldHensbergen

@RonaldHensbergen RonaldHensbergen commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1010

Problem

resolve_provided_contracts substituted ${service.host} with the module instance id, while the renderer names each compose service through its service-naming rule (<id> when the service already carries the id prefix, <id>-<service> otherwise). Every checked-in profile uses id = module name, so this was invisible, but any profile instantiating a module with a different id (e.g. id: object-storage for the object-storage module) silently produced contract endpoints no sibling container could resolve — and nothing diagnosed it: the reverse-proxy renderer cross-checks backends (E136), but plain consumed contracts were not validated at all.

Solution

Both halves of the issue's proposal, with one design correction found during implementation:

  • New ${service.dns} placeholder. The issue proposed redefining ${service.host} through the renderer's naming rule, but a full-suite run caught the dlt module using ${service.host} as an id prefix for its backup volume name (${service.host}-dlt-state), which the DNS semantics would break. The vocabulary is therefore split: ${service.host} keeps resolving to the instance id (stable for name prefixes), while the new ${service.dns} resolves to the Compose DNS name via the shared module_dns_host helper (single-service modules: the renderer's compose service name; modules with a namesake compose service next to auxiliary job containers, e.g. postgres next to postgres-backup/postgres-restore: that service's compose name; other multi-service modules: the instance id, with the explicit serviceName mechanism unchanged).
  • Migrated the endpoint advertisers with a resolvable serving service. The cache, identity-provider, reverse-proxy, object-storage, Postgres warehouse (sql-database host/connectionUri), and Superset BI (http-service host) modules build their network endpoints from ${service.dns}, so the issue's exact reproductions now yield http://object-storage-garage:3900 and analytics-db-postgres and render cleanly.
  • New E150 render diagnostic, mirroring E136. (code renamed in review: E147-E149 are taken by open PR feat(k8s): generate NetworkPolicies and per-module ServiceAccounts (#75) #1022) Every provided http-service, sql-database, cache-service, and object-storage contract's advertised host (the host field, or the endpoint's hostname part; serviceName mapped through the naming rule first) is cross-checked against the compose services the render actually produced. Mismatches fail closed instead of shipping a broken endpoint. backup-target is deliberately excluded: its endpoint may legitimately be a path or symbolic name for file protocol.

Existing profiles are unaffected: all checked-in profiles use convention ids, where the new placeholder resolves to the same value the old one did (verified by an unchanged local-haven-reference compile).

Testing

  • 21 new tests in tests/test_contract_host_resolution.py: placeholder resolution unit tests, E150 validation unit tests (positive, negative, serviceName mapping, external-host skipping, unrendered-module skipping), and the end-to-end garage id: object-storage plan+render reproduction.
  • Full suite: 1946 tests OK (22 Docker-gated skips).
  • ruff check, yamllint, and markdownlint clean.

Docs

AGENT.md vocabulary bullet, docs/modules.md (placeholder reference + E150 section), and a README troubleshooting-table row for E150, garage module comment/README, shared/contracts/http-service.yaml, and a CHANGELOG.md [Unreleased] entry.

Follow-up found while implementing (not addressed here)

The experimental marquez module sets MARQUEZ_HOST: ${service.host} = marquez, but its services are marquez-api/marquez-web, so that host does not resolve on the Compose network — a pre-existing bug unrelated to this change (its lineage-sink kind is not E150-checked).

…rule (#1010)

Provided contracts substituted ${service.host} with the module instance
id while the renderer names each compose service through its
service-naming rule, so any profile instantiating a module with an id
different from its service name produced contract endpoints no sibling
container could resolve. The interpolation vocabulary now separates the
two roles: ${service.host} keeps resolving to the instance id (name
prefixes such as backup volume names depend on it), while the new
${service.dns} resolves to the Compose DNS name (the renderer's compose
service name for single-service modules; the explicit serviceName
mechanism keeps covering multi-service modules). The single-service
modules advertising network endpoints (cache, identity provider,
reverse proxy, object storage) build them from ${service.dns}, and the
render cross-checks every provided http-service, sql-database,
cache-service, and object-storage contract's advertised host against
the compose services it actually produced, failing with E147 instead
of shipping a broken endpoint.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
@RonaldHensbergen

Copy link
Copy Markdown
Owner Author

The marquez follow-up noted in the description is now tracked in #1029.

Comment thread cli/renderer.py Resolved

@RonaldHensbergen RonaldHensbergen left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — PR #1028 (fixes #1010)

Reviewed the full diff, ran the suite locally in the PR worktree (1906 passed, 43 skipped — skips are Docker/SQLAlchemy-gated in this environment; the single SQLAlchemy error also occurs on main, so it is environmental), plus ruff/yamllint/markdownlint after the fixes below.

Blocking

  • B1 — E-code collision (inline comment on cli/renderer.py:334). E147 is already claimed by open PR #1022 (k8s serviceAccount/RBAC checks use E147–E149; fresh-fetched all open PR branches — main stops at E146). Renamed the new diagnostic to E150 in renderer, utils docstring, docs/modules.md, CHANGELOG, and tests.

Major

  • M1 — migration of single-service endpoint advertisers is incomplete. modules/warehouse/postgres/module.yaml (4× sql-database host + connectionUri) and modules/bi/superset/module.yaml (http-service host) still use ${service.host}, so a non-convention id (the issue's own id: analytics-db postgres example) now fails the new check instead of resolving. Migrated both to ${service.dns}. Verified every checked-in profile uses id == module name, so rendered output is byte-identical for them (full suite + reference compile confirm). backup-target endpoint and implementation hostname: deliberately stay on ${service.host}. Multi-service dbt/dagster stay on ${service.host} + serviceName, which E150 maps through the naming rule.

Minor

  • m1 — README troubleshooting table had an E136 row but no row for the new provided-contract host check; added an E150 row next to E136.
  • m2 (follow-up, not blocking): the k8s render path has no provide-side host cross-check (E084 covers the consume side only), and scheme-less host:port endpoints are intentionally skipped by the DNS-label regex. Both documented in code; candidates for a follow-up issue.

What I verified positively

  • module_dns_host semantics (single-service prefixing, multi-service id fallback) and both substitution contexts (planner contracts + renderer _build_context) agree; _compose_service_name move to cli/utils.py has all call sites updated (renderer, security, k8s_renderer).
  • E150 placement: runs after all modules rendered, skips modules whose services failed to render, skips external/dotted/host:port hosts, maps serviceName through the naming rule exactly like E136 — fail-closed mirrors E136.
  • The issue's exact reproduction (garage as id: object-storage) is covered end-to-end by GarageCustomInstanceIdTest: plan endpoint http://object-storage-garage:3900, render produces service object-storage-garage, no errors.
  • backup-target exclusion is correct (file protocol endpoint is symbolic); marquez's lineage-sink is not E150-checked, matching the PR note.
  • CHANGELOG [Unreleased] entry, AGENT.md vocabulary bullet, docs/modules.md placeholder table + E150 section, garage README/module comment, and shared/contracts/http-service.yaml are all consistent.

All findings above are fixed in the follow-up commit on this branch; inline threads resolved. Verdict after fixes: tests, lint, and docs are green — mergeable from my side once CI (incl. SonarCloud) passes.

…les, add README row

Review findings on PR #1028, all fixed in this commit:

- Blocking: E147 collides with open PR #1022 (k8s serviceAccount/RBAC
  checks use E147-E149; main stops at E146). Renamed the new
  provided-contract host diagnostic to E150 everywhere (renderer,
  utils docstring, docs/modules.md, CHANGELOG, tests).
- Major: the single-service migration missed modules whose compose
  services include auxiliary job containers (postgres: postgres +
  postgres-backup/postgres-restore; superset: superset + superset-init),
  because module_dns_host only handled len(services)==1. Extended the
  rule: when a namesake compose service matches the module's own name,
  the dns placeholder resolves to its renderer compose name
  (<id>-<namesake> for a non-convention id). Migrated the postgres
  sql-database host/connectionUri and the superset http-service host to
  the dns placeholder; verified end-to-end that id: analytics-db now
  yields host analytics-db-postgres and renders with zero errors.
  Convention ids are unaffected (all checked-in profiles use
  id == module name).
- Minor: added the E150 row to the README troubleshooting table next
  to E136.

Tests: 21 tests in test_contract_host_resolution.py (namesake-rule
unit tests, postgres-style resolution test, module.yaml guard test
asserting checked contract kinds use the dns placeholder); full suite
1911 passed / 43 env-gated skips; ruff, yamllint, markdownlint clean.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
@sonarqubecloud

Copy link
Copy Markdown

@RonaldHensbergen

Copy link
Copy Markdown
Owner Author

Traceerbaarheid loose ends uit deze review (niet in scope van deze PR, als issues ingediend): #1035 (k8s-renderpad heeft geen provide-side host-check; E150 is compose-only), #1036 (E150 slaat scheme-less host:port endpoints over), #1037 (pre-existing marquez MARQUEZ_HOST bug, genoemd in de PR-body). #1010 heeft geen milestone, dus geen milestone-overerving van toepassing.

@RonaldHensbergen RonaldHensbergen left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Geen openstaande punten meer; PR is naar mijn oordeel merge-klaar. (Eigen PR kan niet via APPROVE — dit is de expliciete quality-gate Aussage.)

Kwaliteitsgate op head 714c367:

  • CI (run 41101049101) success: lint, test (ubuntu/macos/windows, incl. coverage gate), sonarcloud, package, cds-test, cds-runtime, security-scan — plus MVP CI Proof en Docker Runtime Smoke Test success.
  • Lokaal: 1911 tests passed (43 env-gated skips; de ene SQLAlchemy-error is ook op main reproduceerbaar), ruff/yamllint/markdownlint clean.
  • Review-substantie: blocking (E147-collisie met open PR #1022) en major (postgres/superset namesake-service migratie) findings opgelost in 714c367; inline thread resolved; E-code is nu E150, collision-vrij tegen main + open PRs (verse fetch).
  • Loose ends buiten scope als issues ingediend en gerefereerd in een PR-comment: #1035, #1036, #1037.
  • Docs in sync: CHANGELOG [Unreleased], AGENT.md, docs/modules.md, README troubleshooting-tabel (E150-row), garage README, shared/contracts/http-service.yaml.

@RonaldHensbergen
RonaldHensbergen merged commit 36ec66c into main Oct 30, 2026
14 checks passed
@RonaldHensbergen
RonaldHensbergen deleted the fix/1010-contract-hostname branch October 30, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contract hostnames silently break when a module instance id differs from the service name

1 participant