Skip to content

Promote dbt module out of modules-experimental/ to production-suitable - #594

Merged
RonaldHensbergen merged 12 commits into
mainfrom
feat/dbt-production-suitable
Sep 7, 2026
Merged

Promote dbt module out of modules-experimental/ to production-suitable#594
RonaldHensbergen merged 12 commits into
mainfrom
feat/dbt-production-suitable

Conversation

@RonaldHensbergen

Copy link
Copy Markdown
Owner

Summary

The dbt module's productionSuitable: false flag reflected maturity gaps — no README, no demo profile wiring, no bespoke tests — rather than any inherent runtime limitation. That's a different situation from modules/secrets/vault, whose productionSuitable: false is permanent (it only ever runs Vault in ephemeral dev mode). dbt-core itself, running against a real Postgres with the module's existing hardened container settings (read-only root, dropped capabilities, non-root user), has no such limitation, so this PR closes the actual gaps and promotes it to modules/.

Module relocation

  • modules-experimental/transformation/dbt/modules/transformation/dbt/: drops productionSuitable: false and the (experimental) displayName/description suffix.
  • modules/transformation/dbt/README.md (new): Purpose/Known limitations/Upstream docs/Configuration notes, matching the structure of other production module READMEs (e.g. modules/warehouse/postgres/README.md).

Demo profile

  • profiles/local-dagster-postgres-superset-dbt/ (new): the reference local-dagster-postgres-superset stack plus dbt wired to postgres via the sql-database contract. Added as a new profile rather than editing the base profile's composition, following the same pattern local-dagster-postgres-superset-vault established for optional add-ons.

Tests

  • tests/test_dbt_hardening.py (new, 9 tests): image hardening (digest pin, non-root user, pip uninstalled in both build stages), compose service hardening (read_only, cap_drop, tmpfs paths, one-shot restart: "no", the docs sidecar's service_completed_successfully dependency), the required targetDatabase.contractRef schema field, and the entrypoint's required connection env vars — closing the "zero bespoke test files" gap the module had while experimental.

Docs

  • docs/architecture.md: Transformation layer row now lists dbt instead of (planned).
  • docs/roadmap.md: dbt module and new profile moved from the experimental/near-term list into Stable Components.
  • images/dbt/README.md: updated module path references, dropped the "It is experimental" note (the image's build/publish/scan/sign workflow wiring was already identical to the production Dagster/Superset images — nothing there needed to change).

Testing

  • make check — 623 tests, ruff, yamllint, markdownlint all pass.
  • cds validate / cds test (plan+render stages) / cds render pass for the new local-dagster-postgres-superset-dbt profile; manually confirmed the rendered dbt-run service correctly resolves DBT_HOST/DBT_PORT/DBT_DBNAME/DBT_USER/DBT_PASSWORD from the postgres.sql-database contract binding.
  • python -m unittest tests.test_module_isolation -v passes for the relocated module (no cross-module service-name hardcoding), confirming it holds up under the stricter scrutiny modules/ (vs. modules-experimental/) modules get.

The dbt module's productionSuitable: false flag reflected maturity gaps
(no README, no demo profile wiring, no bespoke tests) rather than any
inherent runtime limitation — unlike modules/secrets/vault, whose false
flag is permanent because it only runs Vault in ephemeral dev mode.
dbt-core itself, running against a real Postgres with the module's
existing hardened container settings (read-only root, dropped
capabilities, non-root user), has no such limitation.

- modules-experimental/transformation/dbt/ -> modules/transformation/dbt/:
  drops productionSuitable: false and the "(experimental)" displayName/
  description suffix.
- Adds modules/transformation/dbt/README.md (Purpose/Known limitations/
  Upstream docs/Configuration notes), matching the structure of other
  production module READMEs (e.g. modules/warehouse/postgres/README.md).
- Adds profiles/local-dagster-postgres-superset-dbt/: the reference
  local-dagster-postgres-superset stack plus dbt wired to postgres via
  the sql-database contract, following the same "extend via a new
  profile" pattern used for local-dagster-postgres-superset-vault
  (rather than editing the base profile's composition).
- Adds tests/test_dbt_hardening.py (9 tests) covering image hardening
  (digest pin, non-root user, pip uninstalled in both build stages),
  compose service hardening (read_only, cap_drop, tmpfs paths,
  one-shot restart policy, docs sidecar's service_completed_successfully
  dependency), the required targetDatabase.contractRef schema field, and
  the entrypoint's required connection env vars — closing the "zero
  bespoke test files" gap the module had as an experimental module.
- Updates docs/architecture.md (Transformation layer row: dbt instead of
  "(planned)") and docs/roadmap.md (dbt module + new profile moved from
  the module list into Stable Components).
- images/dbt/README.md: updated module path references and dropped the
  "It is experimental" note (the image build/publish/scan/sign workflow
  wiring was already identical to the production dagster/superset
  images).

Testing:
- make check (623 tests, ruff, yamllint, markdownlint) passes.
- cds validate / cds test (plan+render stages) / cds render pass for the
  new local-dagster-postgres-superset-dbt profile; manually confirmed
  the rendered dbt-run service correctly resolves DBT_HOST/DBT_PORT/
  DBT_DBNAME/DBT_USER/DBT_PASSWORD from the postgres.sql-database
  contract binding.
- tests.test_module_isolation passes for the relocated module (no
  cross-module service-name hardcoding).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@RonaldHensbergen RonaldHensbergen added area:configuration Configuration, wiring, and settings behavior enhancement New feature or request labels Sep 2, 2026
RonaldHensbergen and others added 2 commits September 2, 2026 22:29
Reported when running the local-dagster-postgres-superset-dbt profile:
  dbt-run-1  | cp: cannot stat '/app/images/dbt/profiles.yml': Permission denied

Root cause: images/dbt/Dockerfile's
  COPY --chmod=0444 images/dbt/profiles.yml /app/images/dbt/profiles.yml
was the first COPY to reference the /app/images/dbt directory, so
BuildKit created that directory tree itself to satisfy the destination
path - and applied the requested --chmod (0444, no execute bit) to the
newly created directories too, not just the file. That leaves the
non-root dbt user unable to traverse into /app/images/dbt at all,
so `cp` fails at stat() before it even gets to read permissions on the
file itself.

Reproduced in isolation with a minimal Dockerfile (a plain COPY --chmod
into a not-yet-existing nested directory) and confirmed the identical
"Permission denied" traversal failure; images/dagster/base/Dockerfile
avoids this by design - it has an earlier plain COPY (without --chmod)
that creates /app/images/dagster/ first, so its own
`COPY --chmod=0444 .../workspace.yaml` only touches the file.

Fix: copy profiles.yml to a flat path directly under /app
(/app/profiles.yml.template) instead of a new nested directory, so no
directory creation is implicated by --chmod. /app already exists (via
WORKDIR /app) with normal permissions, so only the file's mode is
affected. Updated entrypoint.sh's `cp` source path to match.

Testing:
- make check (623 tests, ruff, yamllint, markdownlint) passes.
- docker build -f images/dbt/Dockerfile . succeeds locally; manually
  verified as the non-root uid 999 user that /app/profiles.yml.template
  is now readable and copyable (previously reproduced the exact
  "Permission denied" failure with the old nested path in an isolated
  minimal Dockerfile).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dbt-target named volume is shared between dbt-run (writes dbt
artifacts) and the dbt-docs nginx sidecar (serves them read-only).
Docker populates a first-referenced named volume from whichever
image has content at the mount path. Mounting the shared volume at
nginx's stock docroot (/usr/share/nginx/html) risked seeding the
volume with nginx's own root-owned default index.html/50x.html,
permanently blocking dbt-run's non-root (uid 999) writes if the
nginx container was ever created/started before dbt-run's.

Move the mount target to /usr/share/nginx/dbt-docs, a path with no
default content in the nginx:alpine image, so the volume always
populates empty regardless of container creation order. Updated
nginx.conf's root directive to match, and added a regression test
asserting the shared mount never targets nginx's stock docroot.

Verified with a real docker build + docker create/start reproduction:
the new mount path populates empty (no leftover nginx defaults) even
when the nginx container is created first, and a uid 999 write to the
shared volume is correctly visible from the nginx container.
RonaldHensbergen and others added 5 commits September 5, 2026 11:55
…uitable

# Conflicts:
#	docs/architecture.md
#	images/dbt/README.md
#	modules/transformation/dbt/module.yaml
…gate

Merging main brought in #599's warehouseType/duckdb support, which
replaced targetDatabase's hard JSON Schema `required` entry with a
requiredIf-gated consume entry (required only when
config.warehouseType == "postgres", enforced at compile time as E041).
test_target_database_contract_ref_is_required predated that change and
asserted the old schema-level `required` array; updated it to assert the
new requiredIf gate and mappedFrom instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@SemTiOne SemTiOne left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also add a CHANGELOG entry.

Comment thread profiles/local-dagster-postgres-superset-dbt/init-db.sql Outdated
Comment thread profiles/local-dagster-postgres-superset-dbt/profile.yaml
Comment thread modules/transformation/dbt/README.md
RonaldHopRegioGV and others added 4 commits September 7, 2026 09:02
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

@SemTiOne SemTiOne left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM.

@RonaldHensbergen
RonaldHensbergen merged commit ba44a62 into main Sep 7, 2026
19 checks passed
@RonaldHensbergen
RonaldHensbergen deleted the feat/dbt-production-suitable branch September 7, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:configuration Configuration, wiring, and settings behavior enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants