Skip to content

feat(deploy): inject WATCHTOWER_* variables directly via a compose override (ADR-0012) - #22

Merged
swimmesberger merged 1 commit into
wt/tenant-switcherfrom
wt/env-injection
Aug 10, 2026
Merged

feat(deploy): inject WATCHTOWER_* variables directly via a compose override (ADR-0012)#22
swimmesberger merged 1 commit into
wt/tenant-switcherfrom
wt/env-injection

Conversation

@swimmesberger

Copy link
Copy Markdown
Owner

Stacked on #21 (which stacks on #19) — merge in order; base is the tenant-switcher branch so the diff stays scoped.

What

The deploy engine now generates a compose override file and merges it as a second -f into pull/up, placing the reserved WATCHTOWER_* variables directly into service environment:. Template authors need zero compose changes for the App API / tenant switcher — the silent-401 interpolation footgun (--env-file vars interpolate but never enter containers unless passed through) is gone. Classic passthrough keeps working identically.

Who gets what

  • WATCHTOWER_STACK_ID — every service, always. WATCHTOWER_URL — every service, when PublicBaseUrl is set.
  • WATCHTOWER_APP_TOKEN — scoped, it's a credential: services labeled watchtower.inject-token: "true" ("false" opts out, parsing lenient); absent labels → the tenant template's target service, or the sole service of a single-service stack; multi-service plain stacks get nothing by default.

The policy is an engine-neutral component per ADR-0010's dual-engine rule (the future Kube engine applies the same plan as pod env); only the override rendering is Docker-private.

Reviewer notes

  • Security: values never logged (deploy output names variables per service only); token-carrying records have redacting ToString(); override is a sibling temp file, 0600, deleted on every exit path; $$$ escaping because compose interpolates the override too. ADR-0012 is explicit that the exposure bound is on Watchtower's defaults — the repo author controls service names/labels and can direct the token, which is in-threat-model (the repo is the code the operator chose, and already got the token under passthrough).
  • Merge semantics verified for map- and list-form environment: and env_file precedence (override wins per key) — recorded in the docs.
  • Test seams reworked: ComposeCliService verbs route through protected RunAsync/RunCapturedAsync, so every verb's argument assembly executes under test — the review caught that override forwarding was previously untestable and the fix is mutation-verified. Teardown stubs now throw on any verb but down, so no test can shell out to a real Docker CLI.
  • Caveat documented: services behind inactive profiles: may flip the single-service default across compose versions — the label is the escape hatch.

Verification

dotnet build Release 0 warnings · 415 tests green (284 + 131) · no rpc-schema.json drift · npm run build clean.

…erride (ADR-0012)

The deploy engine now generates a compose override file and merges it as a
second -f into pull/up, placing the reserved variables straight into service
environment: — the repo's compose file no longer needs passthrough lines,
killing the silent-401 interpolation footgun.

- Policy (engine-neutral, ADR-0010 seam discipline): WATCHTOWER_STACK_ID to
  all services; WATCHTOWER_URL to all when PublicBaseUrl is set; the token
  scoped — watchtower.inject-token label ('true' opts in, 'false' opts out,
  lenient parsing), else the tenant template's target service, else the sole
  service of a single-service stack; multi-service plain stacks get nothing
  by default.
- Mechanics (Docker-private): service/label discovery via docker compose
  config --format json with the deploy's own args; override YAML rendered
  with escaping for compose's own interpolation ($ -> $$); sibling temp
  file, 0600, deleted on every exit path; deploy output names which
  variables went to which services — values never logged, and the plan/
  config records carry redacting ToString() overrides.
- Classic passthrough keeps working unchanged (override merges last, wins
  per key; verified for both map- and list-form environment and env_file
  precedence, recorded in the docs).
- Test seams reworked: ComposeCliService verbs route through protected
  RunAsync/RunCapturedAsync, so every verb's argument assembly executes
  under test (override forwarding mutation-verified); teardown stubs now
  throw on any verb but down.
- ADR-0012 + rewritten injection docs (who-gets-what table, label, honest
  exposure/threat-model paragraphs, profiles caveat).
@swimmesberger
swimmesberger merged commit 5cecb4e into main Aug 10, 2026
1 of 3 checks passed
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.

1 participant