ADR 0099; issues 077, 078 and 074 resolved; designs 08 and 20 amended #70

Merged
jschoubben merged 5 commits from multiple-fixes into main 2026-09-21 21:58:51 +00:00
10 changed files with 181 additions and 10 deletions
@@ -0,0 +1,94 @@
---
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". 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
- [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)
- **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)
- **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
+4 -3
View File
@@ -9,6 +9,7 @@ code:
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-21
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/0005-the-node-host.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
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
run-once step before it starts. The step is run once per declaration: a root that changes
after first start is fetched again only when the declaration changes
([issue 077](../../04-ISSUES/077-a-fact-fetched-at-first-start-is-fetched-once/00-report.md)).
run-once step before it starts. The step names the binding it reads and the proxy names the
step, so when the authority moves the root is fetched again and the proxy is recreated with it
([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
from the catalogue and asserts the routed name is served.
@@ -7,6 +7,7 @@ code:
- mesh-sdk src
updated: 2026-09-21
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/0074-the-wire-is-specified-not-the-types.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
file, and asserts the file on disk is the new one, the service serves the old one, and the machine
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,9 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-21
located-in: [mesh-lab test/integration]
fixed-by:
amended-design:
fixed-by: mesh-lab, over three multiple-fixes rounds — nine beds read the catalogue or were retired as duplicate coverage; the three mechanism fixtures wear names that are no catalogue module's; the declared list in test/beds-read-the-catalogue.test.ts is empty of WEARING
---
# A mesh test wears a catalogue module's name
@@ -33,3 +33,17 @@ route-forwarding, which needs the certificate authority beside the proxy, and th
*Later the same day.* Route-forwarding converted, with the authority beside the proxy
(ADR 0098). One bed remains declared: the large mesh test, with its three fixtures.
*2026-09-21, late.* The last three `WEARING` declarations are gone. The large mesh test's fixtures
are `a-store` (a provider with no resources) and `adopted-analytics`, its builder fixture retired below; the
runtime-restart bed's fixture is `a-runtime`. The credential-mechanism bed could not be renamed:
its runtime image is the catalogue's, and a runtime names its queues by the module compiled into
it, so the mesh's account for a module of another name is refused at the broker. It was retired —
the grant end-to-end bed proves the same login against the catalogue's redis, with the mesh writing
the contributions the retired bed wrote by hand — and its one assertion nothing else made (the
provider needs no seal key) moved there. Running the large test's renamed tests found two things
its long idleness had hidden: the consumer fixture's login exceeded a backend's twenty characters
(ADR 0049; it has a slug now), and the builder-as-module test declared the builder's image as an
upstream artifact by bare image ID, which a registry copy cannot fetch since ADR 0096 — retired,
since genesis installs the catalogue's builder and proves the same. The large test as a whole has
not been run end to end; its renamed tests and the tests around them have.
@@ -1,7 +1,9 @@
---
status: open
status: resolved
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
@@ -0,0 +1,25 @@
# 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.
**What closed it, and what did not.** The report named two ways the fact changes: the provider
moved, or its state wiped. The move is closed: it rewrites the consumer's binding, the step names
that binding, and the host's unit test runs the step again on exactly that rewrite — the bed that
would re-key a live authority was not written, because a move *is* a rewritten file and the unit
test covers the file. A state wiped behind the mesh's back on the same node is not closed and is
not claimed (ADR 0099, consequences): nothing the mesh knows changes, so nothing it declares can
follow it. On the mesh's own path it does not arise — the authority's state directory survives
unassign and reassign, and a re-issue does not touch it.
@@ -1,7 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-21
located-in: [mesh-controller internal/inventory (secrets), mesh-controller cmd (secret accept)]
located-in: [mesh-controller internal/inventory (secrets), mesh-controller cmd (module issue)]
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); module issue refuses a module with no broker secret before making the account; found one stale delivery in the whole-mesh bed and one orphan account the mesh itself made
---
# 078 — A delivered secret is accepted under any name
@@ -0,0 +1,25 @@
# 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.
*Later the same day.* The refusal caught the mesh itself: `module issue` made a broker account
for any module and delivered it as the own secret named `broker`, whether or not the module
declared one — a bed issuing the certificate authority, which speaks on no bus, left an account
on the broker that nothing would ever read. `module issue` now refuses a module with no `broker`
own secret before the account is made; a unit test holds it to that. A pair delivery for a
requirement the module keeps no secret for is refused too, on review. An own secret the mesh mints (the authority's password)
never needed an issue: it is minted when the node is resolved.