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
58 lines
3.5 KiB
Markdown
58 lines
3.5 KiB
Markdown
---
|
|
status: located
|
|
opened: 2026-09-05
|
|
located-in: [mesh-control, mesh-host, mesh-catalog]
|
|
fixed-by: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
|
|
amended-design: 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](../035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md)
|
|
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?
|