ADR 0104: a provision may be answered by an adapter to the predecessor; issue 093 located; connectivity says how the proxy hands over

This commit is contained in:
2026-09-22 23:57:38 +02:00
parent a2323ade2e
commit a23ede495e
4 changed files with 119 additions and 2 deletions
@@ -0,0 +1,93 @@
---
topic: the mesh
status: accepted
date: 2026-09-22
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
---
# 104. A provision may be answered by an adapter to the predecessor
## Context
[Issue 093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md),
found on the first adopted node hours after it was raised. Every module reachable by name requires
a **route**. The mesh has one provider of it, its own proxy, which binds the two public ports on
the machine's own network. The predecessor's proxy holds those ports and serves every public name
there. So:
- a web module cannot be taken until the mesh's proxy runs;
- the mesh's proxy cannot run until the predecessor's stops;
- and when it stops, every name the predecessor served goes dark, because the mesh's proxy serves
only what mesh modules have contributed.
Adoption ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)) keeps a *file* as it
was and a *container* as it was. It has nothing for a service whose successor is **different
software**: nothing shares a name to hold, so there is only replacement, of every name at once.
The certificates decide the rest. The predecessor's proxy holds them and renews them. A successor
taking over first would have to obtain a certificate for every public name in the same window in
which it took the ports — with nothing else migrated yet, and the only way back being to start the
predecessor again.
## Considered Options
1. **The successor proxy serves routes it did not derive**, given as the operator's configuration,
each disappearing as the module owning that name contributes its own. Rejected for the
migration: it puts the riskiest step first, and it puts configuration the mesh does not own
into the one component whose design is that the mesh derives its configuration.
2. **Take the proxy first and accept the window.** Rejected: every public name at once, before
anything else has moved, with certificates to obtain in the same window.
3. **Make the requirement optional**, so a module can be taken while nothing provides route.
Rejected: it is not optional. A module that says it must be reachable and is not is a module
reporting success while serving nobody.
4. **An adapter module provides the provision by writing into the predecessor's service.** Adopted.
## Decision
**A provision whose only provider owns a scarce machine-wide resource may be answered, during a
migration, by an adapter module that writes into the predecessor's own configuration.**
For the route: a module that **provides `route`**, receives every contribution as the proxy does,
and writes each one as a route file where the predecessor's proxy reads its configuration —
pointing at the machine port the contributing module now publishes. The predecessor's proxy keeps
serving every name it already serves, keeps its certificates and keeps renewing them; a name whose
module has migrated is served by the same proxy, pointing at the mesh's container instead of the
predecessor's.
**It is migration scaffolding and says so.** It is assigned only on an adopted node, it writes only
files it can name as its own, and it is removed when the predecessor's proxy retires — at which
point the mesh's own proxy takes the ports and already knows every route, because by then every
name belongs to a module that contributes it.
**This does not make the mesh the predecessor's manager.** The adapter writes route files and
nothing else, and only because the predecessor's control has been stopped by the operator, which
ADR 0100 requires before a node is adopted.
## Consequences
- The migration keeps its shape: one service at a time, each reversible on its own. The proxy is
the **last** cutover again, not the first.
- There is no certificate event until the end, and at the end there is one, for a proxy that
already has every route from the mesh.
- The mesh writes into a directory the predecessor owns. That is safe only while the predecessor's
control is stopped, and it is why the adapter is refused on a converged node.
- The adapter is throwaway code with a stated end. Something has to delete it; that something is
the operator, and the flip is the moment.
- A second provision with this shape — a resolver on 53, a mail relay on 25 — now has a pattern to
follow rather than a new argument to have.
## How it is checked
Unit tests hold the adapter to writing one route file per contribution, naming each file as its
own, removing a file when its contribution goes, and leaving every file it did not write. The
controller is held to resolving `route` from it exactly as from the proxy. The adoption lab bed
gains a step: with the predecessor's proxy serving a name, a module taken behind the adapter is
reachable under that name, and the predecessor's own names keep answering. Assigning the adapter
to a converged node is refused.
## References
- [Issue 093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md)
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0007](0007-connectivity.md)
+1
View File
@@ -92,6 +92,7 @@ python3 00-META/checks/index.py fail if stale
- **0101** — [A machine's own resolver does not make it in use](0101-a-machines-own-resolver-does-not-make-it-in-use.md) - **0101** — [A machine's own resolver does not make it in use](0101-a-machines-own-resolver-does-not-make-it-in-use.md)
- **0102** — [The mesh writes into a shared file, never over it](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) - **0102** — [The mesh writes into a shared file, never over it](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)
- **0103** — [What an adopted node holds, and what its guard refuses](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md) - **0103** — [What an adopted node holds, and what its guard refuses](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)
- **0104** — [A provision may be answered by an adapter to the predecessor](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)
### Its tiers, from the bottom up ### Its tiers, from the bottom up
+14
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-22 updated: 2026-09-22
decisions: decisions:
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
- 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md - 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md - 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
@@ -535,6 +536,19 @@ undeclared one does not — then the module is removed and the port closes with
rule. *A rule set that is written but never loaded passes every check that reads the file, which rule. *A rule set that is written but never loaded passes every check that reads the file, which
is why the check reads packets.* is why the check reads packets.*
### While a predecessor still serves the same thing
*2026-09-22, [ADR 0104](../../02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md).*
The mesh's proxy owns the two public ports and derives its routes from what modules contributed, so
it cannot stand beside a predecessor's proxy and cannot serve the predecessor's names. During a
migration a provision of this shape — one provider, a scarce machine-wide resource — may instead be
answered by an **adapter**: a module that provides `route` and writes each contribution into the
predecessor's own configuration, pointing at the machine port the contributing module publishes.
The predecessor's proxy goes on serving every name and renewing every certificate; the mesh's proxy
is taken last, when every route is one the mesh contributed. The adapter is refused on a converged
node, and removed when the predecessor's proxy retires. *How it is checked:* the adoption bed takes
a module behind the adapter and asserts the name answers and the predecessor's own names still do.
### On an adopted node ### On an adopted node
*2026-09-22, [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).* **The firewall found on the machine stays in force**, and the mesh *2026-09-22, [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).* **The firewall found on the machine stays in force**, and the mesh
@@ -1,7 +1,7 @@
--- ---
status: open status: located
opened: 2026-09-22 opened: 2026-09-22
located-in: [] located-in: [mesh-catalog, mesh-controller internal/catalogue]
fixed-by: fixed-by:
amended-design: amended-design:
--- ---
@@ -62,3 +62,12 @@ It has to be first, because everything reachable by name waits on it.
otherwise ask a public authority for every name at the moment of the cutover. otherwise ask a public authority for every name at the moment of the cutover.
- Is there a general rule here for "a requirement whose only provider owns a scarce port", or is - Is there a general rule here for "a requirement whose only provider owns a scarce port", or is
each such provision its own decision? each such provision its own decision?
## Decided
*2026-09-22.* [ADR 0104](../../02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md):
during a migration, a provision whose only provider owns a scarce machine-wide resource may be
answered by an **adapter** that writes into the predecessor's own configuration. For the route,
the predecessor's proxy keeps serving every name and keeps its certificates, while each migrated
module's name is pointed at the mesh's container. The proxy is the last cutover again, and by then
every route is one the mesh contributed.