Files
hq/04-ISSUES/037-a-module-cannot-run-code-at-a-lifecycle-phase/00-report.md
T
jschoubben 2405d72fb0 ADR 0052 (proposed) — an init step is a container run once to completion
A module can declare state but not a step that runs. mosquitto must seed its
dynsec admin into dynamic-security.json before the broker starts, or the plugin
aborts; the database providers need the same for first-boot migrations and
health gates (04-ISSUES/037). The old event-hook engine that did this was
powerful and flaky; this is the narrowest sound mechanism instead.

A run-once step is an ordinary container marked `run-once: true`: the host runs
it to completion, requires exit 0, and gates the apply on it — so what the
declaration places after it (the broker) starts only once it has finished.
Gating is by declaration order, not a resolved dependency (ADR 0005); the
completion marker is the recorded declaration digest (ADR 0018), so a re-apply
does not re-run it unless the declaration changed. No new host shape and no
arbitrary host command: strictly less powerful than an `action`.

Points 04-ISSUES/037 fixed-by/amended-design at the record; index regenerated;
records.py and index.py pass.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 23:52:26 +02:00

3.5 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
located 2026-09-05
mesh-control
mesh-host
mesh-catalog
02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md

037 — A module cannot run its own code at a lifecycle phase

The symptom, as observed

Found while converting the catalogue (2026-09-05), across several modules at once. A module can declare things that exist — a directory, a file with fixed content, a network, a container — but it cannot declare a step that runs at a defined point in its own lifecycle. Three converted modules need exactly that and have nowhere to put it:

  • mosquitto. Its Dynamic Security plugin will not start unless dynamic-security.json already contains an admin client before the broker's first start — the broker loads the plugin at boot. Seeding it is a run-once step that must happen after the file resource exists and before the container starts. The vocabulary has no "before first start."
  • The database providers (postgres/mongodb/mssql). First-boot seeding works today only because the image happens to do it from an env var. Anything the mesh itself must run once against the server — a schema migration, an extension enable, a health gate before the module is announced ready — has no home.
  • The seed-then-mutate family already recorded in 035 is the same shape seen from the content side; this is it seen from the timing side.

Why it matters beyond the instance

This is not a defect in a module — it is a capability the module system does not yet offer. A real class of modules needs to run their own code at points in the build/install/run lifecycle: seed-before-start, migrate, post-start health-gate, pre-remove drain. The declarative resource model deliberately describes state, not steps, and that is right for what it covers; the gap is that some modules genuinely have a step.

Prior art, and its warning. An earlier mesh had exactly this as a feature: event-driven hooks that ran custom code at phases of the build/publish/deploy pipeline. It was powerful and it was complex to set up and flaky — which is the real content of this record. The need is not in question; the cost of the obvious answer is. Whatever shape this takes must not reproduce that fragility, or it will be worse than the gap.

Open questions

  • Is the right unit narrow — a run-once / init resource ("run this once, here, in the lifecycle") — or general — a per-phase lifecycle hook on a module, and if so which phases (build / publish / install / pre-start / post-start / pre-remove)?
  • Where does a hook's code run — in the module's own runtime container under its scoped account (ADR 0043/0047), so it inherits the same isolation as its tools and events? Or is some of it the host's, before a container exists?
  • How is a step made idempotent and reconcilable so a re-apply does not re-run it destructively — the same discipline the resource model gets for free and a step does not?
  • What is the smallest version that unblocks the three modules above without rebuilding the old flaky hook engine? Is "seed-before-first-start" alone enough for now, with the general case deferred?
  • A rule states how it is checked: whatever shape is chosen, what lab scenario proves a hook runs exactly once, at the right phase, and converges on re-apply?