From a23ede495e180b7aa56f628a24455e3bcecf4c38 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 23:57:38 +0200 Subject: [PATCH] ADR 0104: a provision may be answered by an adapter to the predecessor; issue 093 located; connectivity says how the proxy hands over --- ...swered-by-an-adapter-to-the-predecessor.md | 93 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/08-connectivity.md | 14 +++ .../00-report.md | 13 ++- 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md diff --git a/02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md b/02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md new file mode 100644 index 0000000..35675c9 --- /dev/null +++ b/02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md @@ -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) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index c94167e..62260b6 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -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) - **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) +- **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 diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 2d3c85d..5cccb35 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -9,6 +9,7 @@ code: - mesh-host internal/apply (the service that reflects a rule set) updated: 2026-09-22 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/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 @@ -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 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 *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 diff --git a/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md b/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md index babf6bb..9ea59e8 100644 --- a/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md +++ b/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-22 -located-in: [] +located-in: [mesh-catalog, mesh-controller internal/catalogue] fixed-by: 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. - Is there a general rule here for "a requirement whose only provider owns a scarce port", or is 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.