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:
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user