From 346e6139957b06a16303fd7112723b8a5b58abcc Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 29 Sep 2026 00:51:42 +0200 Subject: [PATCH] ADR 0142: the mesh delivers its own components as binaries, not container images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured: the host is a binary somebody copied to four machines, owned by no package and built by nothing, while the controller, catalogue, builder and vault are container images publishing no ports at all. Same language, same project, same kind of work, delivered two ways — and the difference is not a judgement about either, it is that images are the only delivery that works. What it costs: genesis must raise a container runtime before the control plane can exist; updating the control plane goes through a registry the control plane runs; a host change cannot be rolled out at all; and compiling the language the mesh is written in is not a capability of the builder, so the controller is built from a hand-written Dockerfile — the incantation the bundle toolchain exists to abolish. Third-party software stays a container: the store, the registry, the broker are somebody else's build. The container runtime stays on the machine for modules. What changes is that the control plane no longer needs it to exist. The receiving half is already built and tested (ADR 0141). Staged: compile Go, an artifact names its target, deliver a binary, the host first, then the rest, genesis last. --- ...delivers-its-own-components-as-binaries.md | 158 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/07-the-foundation.md | 25 ++- 03-DESIGN/01-to-be/18-building-a-module.md | 27 ++- 4 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md diff --git a/02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md b/02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md new file mode 100644 index 0000000..fb01e3f --- /dev/null +++ b/02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md @@ -0,0 +1,158 @@ +--- +topic: the mesh +status: accepted +date: 2026-09-29 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0141-the-host-delivers-its-own-successor.md +--- + +# 142. The mesh delivers its own components as binaries, not as container images + +## Context + +Measured on the control-node, 2026-09-29: + +| what | how it runs | publishes | +|---|---|---| +| host | a binary on the machine | — | +| controller, catalogue, builder, vault | containers | nothing | +| store, registry, broker | containers | ports | + +**The mesh's own software is delivered two ways, and the difference is not a property of the +software.** The host and the controller are both written in the same language, both the mesh's own, +both doing the mesh's own work. One is an image fetched from a registry. The other is a file somebody +copied to four machines, owned by no package, built by nothing +([issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md)). + +**The reason is not a judgement about either, it is that images are the only delivery that works.** +There is no way to put a binary on a machine. The host is hand-copied because of that, and the +controller is an image because of that. Neither was chosen on its merits. + +What it costs, all of it measured rather than argued: + +- **Genesis must raise a container runtime before the control plane can exist.** The bundle carries + three images and one of them is the controller, *"in the bundle for the same reason they are: there + is nothing to fetch it with yet"* + ([design 07](../03-DESIGN/01-to-be/07-the-foundation.md)). So the hardest moment in the mesh's life + has a prerequisite that the thing being started does not need. +- **Updating the control plane depends on the control plane.** Its image is fetched from the registry, + which is a container the controller manages. +- **A change to the host cannot be rolled out at all.** Every machine here runs a byte-identical + hand-copied binary. A change merged yesterday reached none of them. +- **Compiling the language the mesh is written in is not a capability of the builder.** The bundle + toolchains are typescript — real, with a registered base module — and python, which is named in the + list and absent from the catalogue. The controller is built as an image from a Dockerfile, which is + the per-repository incantation the bundle toolchain exists to abolish + ([design 18](../03-DESIGN/01-to-be/18-building-a-module.md)). + +The half that *receives* a binary safely is already built and tested +([ADR 0141](0141-the-host-delivers-its-own-successor.md)): versions side by side in directories named +for them, the newest run, the running one standing aside between reconciles, retirement keeping the +predecessor, and a rollback that chooses a directory. What is missing is everything that puts one +there. + +## Considered Options + +1. **Leave it as it is.** Rejected: it is not a design, it is the reach of one mechanism. And it is + what makes a host change undeliverable. +2. **Containerise the host too**, so everything is delivered one way. Rejected: the host is what + starts the container runtime and what applies containers. A host in a container is the bootstrap + problem made total, and the machine would have no way back from a bad one. +3. **Deliver the mesh's components as operating-system packages.** Rejected for the reason + [ADR 0141](0141-the-host-delivers-its-own-successor.md) rejected it for the host: a package and a + trusted repository per operating system, three of each, and the `package` resource asserts presence + and deliberately never a version. +4. **Binaries for the mesh's own components, containers for third-party software.** Adopted. + +## Decision + +**The mesh's own components are delivered as binaries on the machine.** The host, the controller, the +catalogue, the builder, the vault — the software this project writes. They are delivered by the +mechanism [ADR 0141](0141-the-host-delivers-its-own-successor.md) built: an archive, fetched by +digest, unpacked into a directory named for its version, with the running one standing aside between +reconciles and a rollback that chooses the predecessor. + +**Third-party software stays a container.** The store, the registry, the broker. They are somebody +else's build, they are already adopted as modules +([ADR 0078](0078-the-store-and-broker-are-modules.md)), and an image is the right way to carry +somebody else's software. **The container runtime remains required** — modules use it — so this +removes a dependency from the control plane, not from the machine. + +**The builder compiles the languages the mesh is written in.** A toolchain for Go, with a base module +providing the compiler, exactly as typescript has. The obligation the toolchain list warns about — an +SDK carrying the broker client, the envelope and tool serving — attaches to a *module* written in a +language, not to the language being compilable. None of these components is a module in that sense; +the host is what applies modules. + +**An artifact says what it targets.** A compiled binary is per operating system, pinned at link time +([ADR 0005](0005-the-node-host.md)), and a toolchain deliberately takes nothing from the module, +because anything a module could override there it would be writing a Dockerfile to override. So the +target is a property of the artifact rather than of the recipe, and one artifact declared per target +is one build each. + +**A component's version comes from where it sits, not from its linker.** It is unpacked into a +directory named for its version, so it can read its own version from its path. The stamp goes, and +with it the need for a build to know what it will be called. + +**Genesis carries a binary reference where it carried an image reference.** The principle does not +change — the bundle names a thing by digest and the host fetches it, pinned because nothing can +resolve a version when no mesh exists — and the container runtime stops being a prerequisite for the +control plane. It stays a prerequisite for the store and the broker, which is where it belongs. + +**The order is staged, and each step stands alone.** Compiling Go; an artifact naming its target; +delivering a binary; the host as the first component delivered; the controller, catalogue, builder and +vault out of their containers; genesis last. Genesis is last for the reason it is always last: it +matters for a machine nobody has yet, and every earlier step is provable on a mesh that exists. + +## Consequences + +- **One delivery for the mesh's own software**, so a change to the host ships the way a change to the + controller does, and neither is copied by hand. +- **The control plane stops depending on a container runtime and on its own registry.** Both remain on + the machine for other reasons; neither gates the control plane's own life any more. +- **`Replaced()`, the known-good record and the launcher's rollback stop being dead code.** They were + written for this and have been called by nothing but their tests. +- **Four more components gain a rollback they do not have.** Today a bad controller image is recovered + by an operator; under this it is recovered the way a bad host is. +- **Two versions of each component occupy disk.** Around nine megabytes each. The predecessor is what a + rollback needs. +- **Genesis gets smaller, not larger.** One fewer image to carry and one fewer runtime to raise before + the control plane. +- **This does not make the components smaller or simpler.** They are the same programs; what changes is + how they arrive. A reader expecting the containers to have been hiding complexity will not find any. +- **What got harder:** the builder gains a language, artifacts gain a target, and the mesh gains a + second kind of thing it must deliver correctly — one where getting it wrong takes the control plane + down rather than a module. That is why the host is first: it is the component whose recovery is + already built and tested. + +## How it is checked + +- **A component is delivered and runs, with nothing copied by hand.** A bed builds the host from its + repository, delivers it to a machine running an older one, and the machine reports the new version. + This fails today at the first step, because nothing builds it. +- **Each target is built once and only the matching one is delivered.** Asserted by declaring an + artifact per operating system and checking that a machine is offered the one it can run — a host + built for another is what ADR 0005's link-time pin exists to refuse. +- **A component reads its version from its path**, asserted by unpacking the same bytes into two + differently named directories and seeing each report its own. +- **A bad component is rolled back without an operator**, for the host first: a version that will not + start is replaced by its predecessor once, and the second failure halts naming the machine. +- **The control plane comes up with no registry reachable**, which is the dependency this removes — + asserted by raising it with the registry stopped. +- **Genesis raises a control plane with no container runtime running**, and raises the store and the + broker afterwards. Last, and on a machine with nothing on it. +- **A published port count that does not change.** The mesh's own components publish nothing today, so + moving them out of containers must not open anything — asserted on the machine's reachable set before + and after, which the converge preview already reads. + +## References + +- [ADR 0141](0141-the-host-delivers-its-own-successor.md) — the receiving half, already built +- [ADR 0005](0005-the-node-host.md) — the host, its supervision, and one binary per operating system +- [ADR 0078](0078-the-store-and-broker-are-modules.md) — why third-party software stays a container +- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — what genesis must raise, and in what order +- [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) — the + measurement that started this +- [design 07](../03-DESIGN/01-to-be/07-the-foundation.md) — the bundle's three images, one of them the + controller diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index ccdcf79..4c2b005 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -142,6 +142,7 @@ python3 00-META/checks/index.py fail if stale - **0131** — [Everything on the mesh speaks to the broker seat, and AMQP is not a provision](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) - **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md) - **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md) +- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/07-the-foundation.md b/03-DESIGN/01-to-be/07-the-foundation.md index d80a0e6..82a05ed 100644 --- a/03-DESIGN/01-to-be/07-the-foundation.md +++ b/03-DESIGN/01-to-be/07-the-foundation.md @@ -11,8 +11,9 @@ code: - mesh-catalog modules/postgres - mesh-catalog modules/lavinmq - mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh) -updated: 2026-09-22 +updated: 2026-09-29 decisions: + - 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md @@ -314,4 +315,24 @@ of a database and pushed to over the broker. What arrived and what did not is th **One fault, and it was in the joining.** The token did not say what the mesh calls the machine, so enrolment needed a flag its own help said it did not — and failed at the broker with an empty -username. Recorded in ADR 0004 as the fifth thing a token carries. \ No newline at end of file +username. Recorded in ADR 0004 as the fifth thing a token carries. + +## The mesh's own components arrive as binaries + +*2026-09-29 — +[ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md).* + +The bundle carries three images and one of them is the controller, *because there is nothing to fetch +it with yet*. That reasoning holds and its conclusion changes: the controller is carried as a **binary** +reference rather than an image reference, pinned by digest exactly as before. Nothing about the bundle's +shape moves — it names a thing and the host fetches it — and the container runtime stops being something +genesis must raise before the control plane can exist. It still raises one, for the store and the broker, +which is where somebody else's software belongs. + +The mesh's own components — the host, the controller, the catalogue, the builder, the vault — are +delivered as binaries into directories named for their versions, by the mechanism +[ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) describes. Third-party +software stays a container. The split is not about isolation; it is about who built the thing. + +Measured before deciding it: the mesh's own components publish no ports at all, so this opens nothing. +Only the store, the registry and the broker publish, and they are staying as they are. diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index 5c77068..d9ce6f0 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -5,8 +5,9 @@ code: - mesh-controller cmd/mesh-builder - mesh-controller internal/builder - mesh-catalog modules/builder -updated: 2026-09-25 +updated: 2026-09-29 decisions: + - 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md - 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md - 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md - 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md @@ -263,3 +264,27 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr **Tools, hooks and consumers are not further modes**, which is the test of whether three is the right number: they are loaded by a tool host, and a tool host is a process that stays up. + +## The builder compiles the languages the mesh is written in + +*2026-09-29 — +[ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md).* + +The toolchain list was typescript and python, and only typescript had a base module in the catalogue. +Meanwhile the control plane — written in the language this project is mostly written in — was built as +an image from a hand-written Dockerfile, which is the per-repository incantation this whole mechanism +exists to abolish. + +So the list gains Go, with a base module providing the compiler exactly as typescript has one. The +obligation the list's own comment warns about — an SDK carrying the broker client, the event envelope +and tool serving — attaches to a **module** written in a language, not to the language being +compilable. The mesh's own components are not modules in that sense; the host is what applies modules. + +**And an artifact says what it targets.** A compiled binary is per operating system, pinned at link +time, and a toolchain deliberately accepts nothing from the module — anything a module could override +there it would be writing a Dockerfile to override. The target is therefore a property of the artifact, +not of the recipe: one artifact declared per target, one build each. + +A component's version stops being stamped in at link time. It is unpacked into a directory named for +its version, so it reads its version from its own path, and a build no longer has to know what it will +be called.