ADR 0099: a step that runs once names what it reads; issues 077 and 078 resolved; designs 08 and 20 amended

This commit is contained in:
2026-09-21 23:33:47 +02:00
parent c2a81cbb20
commit 0e0f0298c6
8 changed files with 146 additions and 7 deletions
@@ -0,0 +1,92 @@
---
topic: what runs on it
status: accepted
date: 2026-09-21
deciders: jochen
reconstructed: false
extends: 0052-a-step-that-runs-once-before-a-container.md
---
# 99. A step that runs once names what it reads, and runs again when it changed
## Context
[ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) has a consumer
fetch a fact its provider made at first start through a run-once step: the route proxy fetches
the certificate authority's root before it starts. A run-once step runs once per declaration
([ADR 0052](0052-a-step-that-runs-once-before-a-container.md)): its marker is the digest of its
own declaration, and a re-apply that finds the marker does nothing.
The provider can move. When the authority is assigned to another node it makes a new root there,
and the mesh rewrites the consumer's binding file with the new address — but the step's own
declaration has not changed, so the step does not run again, the proxy keeps the old root, and it
refuses every certificate the new authority issues
([issue 077](../04-ISSUES/077-a-fact-fetched-at-first-start-is-fetched-once/00-report.md)). A
restart trigger was the natural remedy and was refused on a run-once step, on the ground that a
step does not stay running to be restarted.
Two things the host already does point at the answer. What a container reads is part of what it
is: a container's digest includes the digest of every resource it names under `restart-on`, so a
rewritten file it reads is a changed container
([issue 045](../04-ISSUES/045-a-container-keeps-the-values-it-started-with/00-report.md)).
And a run-once step's marker *is* its digest. Nothing new is needed for the step to run again when
what it reads changed; only the refusal stands in the way.
## Decision
**A run-once step may name what it reads under `restart-on`. For a step the word means *run
again*: when a named resource changed in this apply, the step's digest has moved, its marker no
longer matches, and it runs again — gating what follows, as it did the first time.** Nothing
about the marker changes; the refusal of the pair is lifted, in the control plane and on the host.
**The container that consumes what a step made names the step.** A step that ran counts as a
change, so a service that names it under `restart-on` is recreated after it, holding what the step
fetched. Without this the step fetches a new root and the service keeps serving with the old one.
The route proxy's gate names the binding file it reads; the proxy's server names the gate and the
binding. When the authority moves, the binding is rewritten, the gate fetches the new root, and the
server is recreated with it — in one apply.
## Considered Options
1. **A provider epoch in the binding — the mesh raises a number when a provider is re-issued or
moved, and the consumer's file carries it.** Rejected: the binding already changes when the
provider moves (its address does), and a re-issue does not change what the authority serves —
its state persists. An epoch would be a second signal for a change the file already shows.
2. **The step runs before every start of the service, with no marker.** Rejected: every reconcile
would run it, and a step that runs on every apply reads as a change on every apply, so the
service naming it would be recreated every few minutes.
3. **The proxy fetches the root itself, at start.** Rejected as the general answer: it fixes the
proxy and leaves the next consumer of a fact made at first start to fix itself. The step is the
general shape ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)).
4. **Lift the refusal and read `restart-on` as *again* on a step.** Adopted: it is what the
digest already does, and it needs no new word.
## Consequences
A fact fetched at first start follows its provider when the provider moves. What is not covered:
a provider whose state is wiped behind the mesh's back, on the same node, makes a new fact that
nothing the mesh knows reflects. That is not a change the mesh can see, and it is not claimed.
The one contradiction the refusal named is real and is now a documented reading: on a running
container `restart-on` means recreate, on a step it means run again. Both are "this must reflect
what it reads".
## How it is checked
- mesh-host: a unit test declares a run-once step naming a file, records its marker against the
file's old content, applies with the new content and asserts the step ran; applies again with
nothing changed and asserts it did not. A second test declares a container naming a run-once
step and asserts the container is recreated after the step ran, with the step as the stated
reason.
- mesh-controller: the manifest parser accepts a run-once step with `restart-on`; the
catalogue-wide manifest test parses the route proxy's manifest, whose gate and server name what
they read.
- The route-forwarding bed still passes with the host that accepts the pair. No bed moves the
authority: the mechanism is proven by the unit tests, the declaration by the manifest test.
## References
- [issue 077](../04-ISSUES/077-a-fact-fetched-at-first-start-is-fetched-once/00-report.md)
- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md), [ADR 0053](0053-a-step-that-runs-on-a-schedule.md), [ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)
- [`03-DESIGN/01-to-be/08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md), [`03-DESIGN/01-to-be/20-writing-a-module.md`](../03-DESIGN/01-to-be/20-writing-a-module.md)
+1
View File
@@ -146,6 +146,7 @@ python3 00-META/checks/index.py fail if stale
- **0085** — [A secret is a provision, and the vault is the module that provides it](0085-a-secret-is-a-provision.md) - **0085** — [A secret is a provision, and the vault is the module that provides it](0085-a-secret-is-a-provision.md)
- **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md) - **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md)
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md) - **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
### How it is built ### How it is built
+4 -3
View File
@@ -9,6 +9,7 @@ code:
- mesh-host internal/apply (the service that reflects a rule set) - mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-21 updated: 2026-09-21
decisions: decisions:
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
- 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md - 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md
- 02-DECISIONS/0005-the-node-host.md - 02-DECISIONS/0005-the-node-host.md
- 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md
@@ -564,9 +565,9 @@ The mesh mints the authority's password and nothing else of its: a root certific
are things only the authority can make, and a served fact written in a manifest cannot carry what are things only the authority can make, and a served fact written in a manifest cannot carry what
does not exist until the authority has run. So the authority serves its root at a path beside its does not exist until the authority has run. So the authority serves its root at a path beside its
ACME directory, and the proxy that requires it fetches that root over the mesh network in a ACME directory, and the proxy that requires it fetches that root over the mesh network in a
run-once step before it starts. The step is run once per declaration: a root that changes run-once step before it starts. The step names the binding it reads and the proxy names the
after first start is fetched again only when the declaration changes step, so when the authority moves the root is fetched again and the proxy is recreated with it
([issue 077](../../04-ISSUES/077-a-fact-fetched-at-first-start-is-fetched-once/00-report.md)). ([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)).
*How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer *How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer
from the catalogue and asserts the routed name is served. from the catalogue and asserts the routed name is served.
@@ -7,6 +7,7 @@ code:
- mesh-sdk src - mesh-sdk src
updated: 2026-09-21 updated: 2026-09-21
decisions: decisions:
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
- 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md - 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
- 02-DECISIONS/0040-what-a-module-is.md - 02-DECISIONS/0040-what-a-module-is.md
@@ -198,3 +199,11 @@ one shape this does not protect, and should not be written.
*How it is checked:* the lab's coupled-pair spike declares exactly this pair, pushes a refused *How it is checked:* the lab's coupled-pair spike declares exactly this pair, pushes a refused
file, and asserts the file on disk is the new one, the service serves the old one, and the machine file, and asserts the file on disk is the new one, the service serves the old one, and the machine
reports the push failed. reports the push failed.
A `run-once` step may itself name what it reads under `restart-on`; for a step the word means
*run again* — a step that fetches a fact from a provider names the binding it reads, and runs
again when the provider moved. The service that consumes what the step made names the step, so it
is recreated with the new fact
([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)). *How it is
checked:* the host's unit tests run a step again when its named file changed and not otherwise,
and recreate a container naming a step after the step ran.
@@ -1,7 +1,9 @@
--- ---
status: open status: resolved
opened: 2026-09-21 opened: 2026-09-21
located-in: [mesh-host internal/apply (run-once marker), mesh-catalog modules/route-proxy] located-in: [mesh-host internal/declaration, mesh-controller internal/catalogue, mesh-catalog modules/route-proxy]
fixed-by: ADR 0099; mesh-host and mesh-controller multiple-fixes (a run-once step may name what it reads and runs again when it changed); mesh-catalog multiple-fixes (the proxy's gate names the binding, the server names the gate)
amended-design: 03-DESIGN/01-to-be/08-connectivity.md, 03-DESIGN/01-to-be/20-writing-a-module.md
--- ---
# 077 — A fact fetched at first start is fetched once per declaration # 077 — A fact fetched at first start is fetched once per declaration
@@ -0,0 +1,16 @@
# Diagnosis — 2026-09-21
1. The host's marker for a run-once step is the digest of its declaration, and that digest
already includes the digest of every resource the container names under `restart-on` — what a
container reads is part of what it is (issue 045). So a run-once step that named the binding
file it reads would run again the moment the mesh rewrote that file. Only the refusal of the
pair run-once + `restart-on`, in the manifest parser and on the host, stood in the way.
2. When the authority moves, the binding's address changes and the file is rewritten; a re-issue
changes nothing the authority serves, since its state persists. The file is the signal.
3. The service also had to follow: a step that ran counts as a change, so a service naming the
step under `restart-on` is recreated with what the step fetched.
**Located in:** the two refusals and the proxy's manifest. Decided in
[ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md); proven by unit
tests on the host (the step runs again when its file changed, and not when it did not; the
container naming the step is recreated after it ran) and the catalogue-wide manifest test.
@@ -1,7 +1,8 @@
--- ---
status: open status: resolved
opened: 2026-09-21 opened: 2026-09-21
located-in: [mesh-controller internal/inventory (secrets), mesh-controller cmd (secret accept)] located-in: [mesh-controller internal/inventory (secrets)]
fixed-by: mesh-controller multiple-fixes (a delivery is refused for a name the module does not declare as an own secret, a requirement it has not got, or a local it does not keep; the refusal names what it does declare); found one stale delivery in the whole-mesh bed on the spot
--- ---
# 078 — A delivered secret is accepted under any name # 078 — A delivered secret is accepted under any name
@@ -0,0 +1,17 @@
# Diagnosis — 2026-09-21
1. Acceptance sealed the value and wrote the row without reading the module's manifest, which the
mesh holds. Both delivery paths did: a module's own secret, and a pair credential for a
requirement kept in the vault.
2. Refused now, in the inventory, so every caller gets it: an own secret must be one the manifest
declares; a pair credential must name a requirement the module has, and where the module keeps
several secrets for it (ADR 0094) a local it keeps — and no local where it keeps one. Each
refusal names what the module does declare.
3. The refusal found a stale delivery at once: the whole-mesh bed delivered `smtp-pass` to a module
that declares `smtp-password`. Corrected in the bed.
**Located in:** the inventory's two accept paths. Not a decision: the manifest was already the
authority on what a module holds. Proven by unit tests against the store: a delivery under an
undeclared name is refused naming the declared ones; under a declared name it is kept; to an
unknown module it is refused with the remedy; a pair delivery for a requirement the module has not
got, or with no local where several are kept, or under a local it does not keep, is refused.