Compare commits

..
Author SHA1 Message Date
jschoubben c4151e6bc4 ADR 0163 built: the take digest, the minted-secret refusal, the networks setting, settings judged where stored, genesis raising the forge as declared; issues 096, 097, 126 resolved
The record gets its built note; designs 05 and 09 the revisions; 086, 098,
099, 100 and 101 stay located because every machine is converged and the
record's live row — a take read on an adopted machine — has not been run;
090 is built in part, its network difference left for the take to say.
2026-10-01 23:45:57 +02:00
14 changed files with 129 additions and 99 deletions
@@ -131,6 +131,32 @@ the plan says it too.
| Genesis raises the forge as its module declares it | a genesis test that raises, assigns, and finds the module holding rather than raising a second |
| Live | the next cutover on an adopted machine: `take` shows the comparison, refuses the downgrade if there is one, and the service keeps its configuration and its secret |
## Built, 2026-10-02
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
Built across mesh-host 63 and 64 and mesh-controller 201, 202 and the pull request that followed
them. Rule 1: `take` previews every held thing's comparison and ends with a digest; `take --yes
<digest>` acts on exactly that preview, and a changed preview or an account older than the flip
allows is refused, as the flip's are. A published port's reach is said as the machine reported it,
behind the found firewall whose rules are not read. Rule 2: an older image, a differing file and a
minted, unaccepted secret for found data refuse, overridden by `--downgrade`, `--replace <path>` and
`--mint <name>`; the secrets a module holds on a machine are read with where each came from. Rule 3:
`secret accept --provider` reaches a required secret. Rule 4: the per-machine setting is `networks`,
a container id to the found networks it keeps; judged for an adopted machine only, joined by the host
after the container runs, part of the container's spec, named in the preview. Rule 5: the host's
facts, former targets and strays. Rule 6: one judgement, run where a setting is stored and where a
machine is composed; a module whose stored setting its definition can no longer compose is left out
of the declaration, the envelope says so, the host keeps that module's things, and `plan` and `push`
say it by name. A key that reaches nothing is refused where stored and said by `plan`, and never
costs a module. Rule 7: genesis raises the forge under the module's container name, with its image
digest and its data directory; the network is the one difference left, said by the take, because the
bootstrap forge reaches the store on the machine's loopback.
**Not yet proven live.** Every machine of this mesh is converged, so the table's last row — a take
on an adopted machine — waits for the next adoption. What is live is what the rows above it check.
Issues 086, 098, 099, 100 and 101 stay located until that row is read.
## References
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
+14 -1
View File
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-host]
updated: 2026-10-01
updated: 2026-10-02
decisions:
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
- 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
@@ -163,6 +163,19 @@ a resource's former targets, removes a container or file it wrote under a name t
longer names, never removes what was found, and reports what runs on the machine that it neither
wrote nor holds. *How it is checked:* ADR 0163's table.
**What the host joins, keeps and raises for a take** — revision, 2026-10-02
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 4, 6 and 7). A
container may name networks it also joins once it runs — the found network a per-machine setting keeps
for a taken container while a neighbour still resolves it there; joined after the run, part of the
container's spec, refused when it cannot be joined. A declaration may name the modules the mesh left
out of it because a stored setting cannot compose with the module's definition: the host keeps what it
wrote and holds for a left-out module and says so, where absence used to read as removal. And genesis
raises the bootstrap forge under the forge module's container name, with the module's image digest and
its data directory, so the module holds it by the found rule; the network is the one difference a take
has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a
host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test
holds the installer's constants to the module's manifest where the catalogue is checked out beside it.
**Found reaches every kind that can touch what the machine has**
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record
keeps its mode and owner, a unit present with no record keeps its state and boot setting, a
+16 -1
View File
@@ -8,7 +8,7 @@ code:
- mesh-host packaging/nox-mesh-host-network.sh
- mesh-controller internal/token
- mesh-controller internal/inventory/nodes.go
updated: 2026-10-01
updated: 2026-10-02
decisions:
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
@@ -323,6 +323,21 @@ network are said. `take --yes <digest>` cuts over what was previewed, as the fli
container may keep a found network by a per-machine setting while its neighbours are not yet taken.
*How it is checked:* ADR 0163's table.
**A setting is judged where it is stored, and the take's words** — revision, 2026-10-02
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1, 2, 4 and 6).
The preview ends with a digest of what it said; `take --yes <digest>` acts on that preview and nothing
else, and a preview that has changed since, or an account of the machine older than the flip allows, is
refused as the flip's is. A module the machine holds nothing for has nothing to compare, and `--yes`
suffices. The overrides are `--downgrade`, `--replace <path>` and `--mint <name>`; the per-machine
setting that keeps a found network is `networks`, a container id to the networks it keeps, accepted
for an adopted machine only. Storing a setting composes it against the module's current definition and
refuses, naming node, module, layer and key, what cannot compose or reaches nothing. A definition that
later moves under a stored setting costs that module its place in the machine's declaration, said by
name in `plan`, `push` and the declaration itself, and the machine is told everything else; a stray
setting no longer refuses the machine where it is read. *How it is checked:* controller tests over the
one judgement — refused where stored, a module left out where composed, the envelope naming it — and
over a take's digest, staleness and secrets.
A candidate machine is not empty. It has a package manager, probably a container runtime,
configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
says the host never touches what it did not create — adoption is the deliberate act of taking
@@ -40,3 +40,9 @@ it changes before it changes it, and for taking a module this one does not.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the preview names a narrowing. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
mesh-controller 201 and the pull request after it: the preview names it, and `take --yes <digest>`
acts on the preview that was read. Stays located until a take is read on an adopted machine — every
machine of this mesh is converged today, so the record's live row has not been run.
@@ -53,3 +53,13 @@ network, or it is not a takeover.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 7: genesis raises as the module declares. Building follows,
host first, then the controller's `take`.
## Built in part, 2026-10-02
mesh-host 64: genesis raises the forge under the module's container name (`gitea`), pinned to the
module's image digest, with the module's data directory mounted at `/data` — so the module finds it,
holds it, and a take compares equal images and the same data. A test holds the installer's constants
to the module's manifest where the catalogue is checked out beside it. The network is the difference
left: the bootstrap forge runs on the machine's network to reach the store on its loopback, the module
runs bridged and publishes its ports, and the take says so. Closing waits for group 9's genesis test —
a mesh raised, the module assigned, and the module found holding rather than raising a second forge.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-23
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
fixed-by: mesh-controller (the pull request after 201: JudgeSettings, LeftOut), mesh-host 64 (left_out kept)
amended-design:
---
@@ -63,3 +63,12 @@ knowing the code.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 6: judged where stored; an impossible statement costs a module. Building follows,
host first, then the controller's `take`.
## Resolved, 2026-10-02
One judgement, in the catalogue, run where a setting is stored and where a machine is composed. Stored,
a setting that cannot compose with the module's current definition is refused naming the node, the
module, the layer and the key; a key that reaches nothing is refused there too. Composed, a definition
that moved under a stored setting leaves that module out of the machine's declaration — the envelope
names it, the host keeps what it holds and wrote for it, `plan` and `push` say it — and the machine is
told everything else. A stray setting no longer refuses the whole machine where it is read.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-23
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
fixed-by: mesh-host 63 (former targets removed, strays reported), mesh-controller 201/202 (strays shown)
amended-design:
---
@@ -80,3 +80,11 @@ found, and so would be kept for ever on purpose.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 5: former targets are removed and strays reported. Building follows,
host first, then the controller's `take`.
## Resolved, 2026-10-02
mesh-host 63: the host's record keeps a resource's former targets, removes a container or file it
wrote under a name the declaration no longer names, never what was found, and reports strays — what
runs on the machine that the mesh neither wrote nor holds. mesh-controller 201 and 202 show strays
on `node show` for an adopted and a converged machine alike; the live mesh reported four on the
control node the evening it rolled.
@@ -68,3 +68,9 @@ written.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the difference is shown and a differing file refuses. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
mesh-host 63 reports the difference between the kept original and the declared content; mesh-controller
201 shows it in the preview and refuses a differing file unless `--replace <path>` names it, or the
module declares the file partially. Stays located until a take is read on an adopted machine.
@@ -65,3 +65,9 @@ expected rate.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the images are compared by age and a downgrade refuses. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
mesh-host 63 reports the found image and both images' creation dates; mesh-controller 201 says
DOWNGRADE and refuses unless `--downgrade` is said. Stays located until a take is read on an adopted
machine.
@@ -70,3 +70,11 @@ the module can only be installed fresh.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 2 and 3: a minted secret for found data refuses; secret accept reaches required secrets. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
`secret accept <node> <module> <name> --provider <node>` reaches a required secret (mesh-controller 201).
The pull request after it reads every secret a module holds on a machine with its origin, and a take
of a module whose data was found refuses a minted, unaccepted one — naming the accept that carries
the existing value in, or `--mint <name>` to let the service take the new one. Stays located until
a take is read on an adopted machine.
@@ -66,3 +66,10 @@ exercise.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 4: the neighbours are named; a found network may be kept by a setting. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
The preview names every neighbour on a found network (mesh-controller 201). The pull request after it
adds the per-machine setting `networks` — a container id to the found networks it keeps — judged for an
adopted machine only, and mesh-host 64 has the taken container join each once it runs. Stays located
until a take is read on an adopted machine.
@@ -1,7 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-26
located-in: [mesh-host internal/apply]
fixed-by: mesh-host 63 (every written field compared), mesh-controller 201 (build says the policy)
amended-design:
---
# 126 — a volume path is not in the spec comparison, and a roll-out raced a data move
@@ -50,3 +52,9 @@ the install-page junk was discarded twice.
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 5 and 7: every field compared; build says the policy. Building follows,
host first, then the controller's `take`.
## Resolved, 2026-10-02
mesh-host 63: every field the host writes is compared before a container is called current, volumes
and paths included. mesh-controller 201: `build` and the take-in say when a module's policy rolls a
result out at once; under ADR 0162 the plan says it too.
@@ -1,61 +0,0 @@
---
status: located
opened: 2026-10-01
located-in: [mesh-controller examples/route-proxy/main.go (routesFrom requires a route's public `name` and treats `internal-name` only as an alias of it)]
fixed-by:
amended-design: []
---
# 191 — A route with only an internal name is dropped as naming nothing
## What was observed
A module whose endpoint reaches only the private network could not be reached by its internal name.
The module ran and answered on its own port. Its route's internal name resolved to the serving node.
The request failed during the TLS handshake:
```
http: TLS handshake error from …: no public route for "unifi.home-server.internal" in this mesh,
so no certificate is asked for
```
The proxy's own log said why, every time it re-read its routes:
```
unifi on home-server asked for a route and named nothing; skipped
```
The route it skipped was not empty. The mesh had given it an endpoint, a port, a scheme and an internal
name, and no public name:
| route | `name` | `internal-name` | served |
|---|---|---|---|
| home-assistant | a public name | `home-assistant.home-server.internal` | under both |
| unifi | — | `unifi.home-server.internal` | under neither |
Three other modules on the same node were skipped with the same line on the same pass.
## Why it matters
**Reach is decided in one place, and the proxy reads the old shape of the decision.**
[ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)
made an endpoint's reach decide which names exist. The controller composes the public name, the
internal name, or both, and composes no name that nobody asked for
([issue 140](../140-an-endpoints-reach-is-not-declared/01-resolution.md)). An endpoint that reaches
only the private network is the ordinary case for anything that should not face the internet. It is
exactly the case the proxy drops.
**The failure is quiet and points the wrong way.** Nothing marks the module unhealthy. The handshake
error says *no public route*, which reads as a certificate fault on the reader's side. The line that
gives the real cause is one of four identical lines repeated every few seconds in a log nobody reads
until they already suspect the proxy.
**The opposite move is not a workaround.** Giving the endpoint public reach makes the proxy serve it.
It also publishes an administration interface to the internet to get a name on the private network.
## Open questions
- Should the proxy refuse a route it cannot serve in a way the controller or an operator sees, not
only in its own log? The same silent skip covers a route with no usable port or an unknown scheme.
- What checks that what the controller composes and what the proxy serves stay the same shape? ADR
0138 changed one side and nothing failed on the other.
@@ -1,31 +0,0 @@
# Diagnosis
*2026-10-01.*
**Ruled out first: the module itself.** Its container was up and had not restarted. The controller's
status endpoint answered on its own port with `"up": true`. The tool wrapper beside it was serving
all its tools.
**Ruled out: name resolution.** The internal name resolved to the serving node's private-network
address, which is where the proxy listens. Plain HTTP to the name reached the proxy and got a 404.
HTTPS failed in the handshake, and the proxy logged that it had no route for the name.
**The route as the proxy received it.** The mesh-written route file held a complete contribution
for the module: endpoint `web`, port, scheme `https`, `insecure`, a label, and `internal-name`. It had
no `name`. That is what the controller composes for an endpoint whose reach stops at the private
network (ADR 0138, `composeName`). The contribution was correct.
**Located: `routesFrom` in the proxy.** It reads `name` first and skips the contribution if `name`
is empty. It reads `internal-name` only at the end, as a second host for a rule that already has a
public one. So the proxy can serve an internal name only next to a public one. That matched the
mesh before ADR 0138, when both names were always composed. It has been wrong since then.
The other half of the proxy already handles the case. Certificates for a host are split by whether it
is in the public set: hosts outside it go to the internal authority, and only hosts inside it are
eligible for ACME. A host that is only ever an internal name falls on the correct side of both checks
without change. Only reading the route was wrong.
**The fix.** `routesFrom` takes a route that names either host, serves each name it carries, and
marks only the public one as public. It still skips a route that names neither, with the same log line.
A test proves an internal-only route is served, certified by the internal authority, and refused by
the public one. That test fails against the code before the change.