ADR 0142: the mesh delivers its own components as binaries #175
@@ -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
|
||||||
@@ -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)
|
- **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)
|
- **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)
|
- **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
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ code:
|
|||||||
- mesh-catalog modules/postgres
|
- mesh-catalog modules/postgres
|
||||||
- mesh-catalog modules/lavinmq
|
- mesh-catalog modules/lavinmq
|
||||||
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
||||||
updated: 2026-09-22
|
updated: 2026-09-29
|
||||||
decisions:
|
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/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
- 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
|
- 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
|
||||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.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,
|
**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
|
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.
|
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.
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-catalog modules/builder
|
- mesh-catalog modules/builder
|
||||||
updated: 2026-09-25
|
updated: 2026-09-29
|
||||||
decisions:
|
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/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/0097-a-vendor-image-is-a-declared-build-input.md
|
||||||
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.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
|
**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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user