Files
hq/02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md

5.6 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-09-21 jochen false 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 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): 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). 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). 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).
  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". This record is about a run-once container, the step ADR 0052 defined. A run-once process is a different shape whose marker does not carry what it reads; it is not covered here.

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