242 lines
16 KiB
Markdown
242 lines
16 KiB
Markdown
---
|
|
layer: to-be
|
|
status: in-progress
|
|
code:
|
|
- mesh-host cmd/mesh-bootstrap
|
|
- mesh-host internal/bootstrap
|
|
- mesh-lab test/integration/whole-mesh-full.test.ts
|
|
updated: 2026-09-22
|
|
decisions:
|
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
|
- 02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md
|
|
- 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
|
|
- 02-DECISIONS/0067-genesis-is-a-pivot.md
|
|
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
|
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
|
|
- 02-DECISIONS/0005-the-node-host.md
|
|
- 02-DECISIONS/0010-delivery.md
|
|
- 02-DECISIONS/0036-bootstrap-ends-at-a-usable-mesh.md
|
|
---
|
|
|
|
# Raising a mesh
|
|
|
|
How a mesh comes into existence on machines that have none, and how a machine joins one that
|
|
already exists. This is the procedure an operator runs. It is not a description of the lab, and
|
|
the lab does not have one of its own.
|
|
|
|
## Two moments, and only two
|
|
|
|
A mesh is raised once and joined many times, and the two are not variations of each other.
|
|
|
|
**Genesis** happens on one machine, when there is no mesh. Nothing can be asked, nothing can be
|
|
granted, and nothing has been published anywhere. It is the only moment at which the ordinary
|
|
rules cannot all hold at once, and it is resolved by a pivot
|
|
([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)).
|
|
|
|
**Joining** happens on every machine after the first, and it is ordinary. A mesh exists, so it can
|
|
be asked for a token and told what the machine should be. Joining installs the host and nothing
|
|
else: no temporary anything, no foundation raised by hand, no registry.
|
|
|
|
Confusing the two is what produced a procedure that only ever worked in a fixture. A bed that
|
|
raises four machines the same way has not tested genesis at all — it has tested joining, four
|
|
times, with the first one hand-fed.
|
|
|
|
## What changed, and what did not
|
|
|
|
*2026-09-13.* The installer carries a builder now, and builds the controller it raises. Three
|
|
records settle it: [ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md) that
|
|
genesis builds rather than carries, [ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)
|
|
where it clones from, and [ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md) how
|
|
the builder arrives — which also records an argument that failed. It was put that a produced image
|
|
must be published before anything can fetch it, so the registry would have to come up before the
|
|
controller. It does not: the machine that builds the image is the machine that runs it, and a
|
|
local image is named by the digest of its own configuration exactly as a carried one is. **Building
|
|
changes where the bytes came from, not where they are.**
|
|
|
|
So the pivot is unchanged, the registry is where it was, and one step was added before the bundle is
|
|
written. What follows describes the program that exists.
|
|
|
|
## Genesis
|
|
|
|
The installer is a single program carrying **the builder** inside it — not the controller
|
|
([ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md)). What cannot be fetched is
|
|
the thing that does the fetching, so that is what is carried; everything else is made here.
|
|
|
|
It proceeds in one direction, and every step is safe to run again.
|
|
|
|
**First it refuses to start if the machine is not ready.** A container runtime, the ability to
|
|
write where it must write, the host binary where it expects it — and a repository and a commit to
|
|
build from, because an installer told nothing would raise a store and a broker and then have
|
|
nothing to raise a controller from. A machine that is not ready is told what is missing rather
|
|
than half-changed.
|
|
|
|
**Then it loads the carried builder and builds the controller with it**, from a repository on a
|
|
mesh that already exists and a named commit ([ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)).
|
|
This is the same repository and path every later rebuild of the controller will use, so what
|
|
raises the mesh is the same thing that will maintain it.
|
|
|
|
**Then it describes what the machine will become.** The image it just made is named by the digest
|
|
of its own configuration — content-addressed and unforgeable, and requiring nothing to have served
|
|
it. That is legal precisely where nothing could have served one, and it is why building here needs
|
|
no registry: the machine that made the image is the machine that will run it.
|
|
|
|
**Then it raises the foundation and a temporary controller, and waits for that controller to
|
|
answer.** At this point the machine is a mesh of one node with nothing joined to it.
|
|
|
|
**Then the machine joins the mesh it is itself running.** It enrols, and an agent runs on it. Being
|
|
heard from once is not the same as an agent running, and the installer checks both, because
|
|
enrolling is itself the thing that makes a mesh hear from a machine.
|
|
|
|
**Then it installs a registry**, so the mesh has somewhere to keep its own images.
|
|
|
|
**Then it publishes the controller's image to that registry**, which is the moment the image
|
|
first receives a digest assigned by something other than itself. This is the carrying step, and it
|
|
is the same step for all three things the build loop cannot produce for itself — the controller,
|
|
the registry, and the builder. The rule and its closed list are in
|
|
[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three).
|
|
|
|
**Then it installs the controller again, as an ordinary module pinned to that digest, and drops
|
|
the temporary one.** The pivot is complete: what raised the mesh is gone, and what runs is a module
|
|
like any other. From here the mesh can build and roll out its own upgrades, including to the thing
|
|
that runs it.
|
|
|
|
### Genesis on a machine in use
|
|
|
|
*2026-09-22, [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).* A control-node that is already running a predecessor mesh is raised
|
|
**adopted**, said so by the operator. The operator stops the predecessor's control on it first —
|
|
the daemons that write its configuration — and its services keep running. **Every port the
|
|
foundation binds is an input to genesis**, checked free before anything is raised, refused with the
|
|
name of what holds it; the ports given become the node's settings for the foundation's modules, so
|
|
adopting the foundation as modules keeps them, and every reader of them — the filter, the base
|
|
ruleset, the private network's endpoint, the addresses consumers are given — reads them there. On
|
|
the control-node measured, the store's and the bus's usual ports were free and the registry's, the
|
|
broker's management port and, as the private network's hub, its port were held. Genesis also
|
|
checks the private network's range does not overlap a tunnel the predecessor runs. Genesis adopted
|
|
**does not load the base ruleset**: the machine's own firewall already filters; the mesh opens its
|
|
foundation's ports through it and keeps the store from outside with a table of its own that only
|
|
refuses ([08-connectivity](08-connectivity.md)). **A converged genesis refuses a machine in use** —
|
|
a container running, or a port listening on an address other than loopback that is neither ssh's
|
|
nor held by one of the operating system's own network daemons
|
|
([ADR 0101](../../02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md)) — and
|
|
names every one it counted, so a forgotten flag cannot close a working machine. **A machine
|
|
raised adopted stays adopted when genesis is run again**
|
|
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)): the installer reads the mode from what the machine records,
|
|
refuses a run without the flag on an adopted machine, and refuses the flag on a converged one. *How it is checked:* the adoption bed raises genesis converged on a machine in use and
|
|
asserts the refusal, then adopted with the registry's port held and asserts the refusal names its
|
|
holder, then with another port given asserts the foundation comes up, stays on that port once
|
|
adopted as modules, and the machine's service is still reachable.
|
|
|
|
## After the pivot, and still part of installing
|
|
|
|
Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it
|
|
has is a controller, a store, a queue and a registry. What it cannot yet do is **produce
|
|
anything** — and almost every module in the catalogue is waiting to be produced, because a manifest
|
|
names what its artifacts are and nothing has made them.
|
|
|
|
So installing continues:
|
|
|
|
**The builder arrives, and installing is what brings it.** It is a module like any other and is
|
|
assigned to a machine like any other, but it cannot be built by the thing it is — see
|
|
[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three).
|
|
So it is carried, and it is already here: it is what built the controller. The last step of
|
|
installing publishes it into the mesh's own registry, installs it as an ordinary module pinned to
|
|
that digest, and issues it a broker account — the same two acts the controller went through,
|
|
plus the one thing only a builder needs. The account is issued before the machine is sent anything,
|
|
because a builder that arrives without its credential starts, finds nothing it may read, and waits,
|
|
which looks exactly like a builder with no work.
|
|
|
|
**The core modules are built.** Each is named by a repository and a path
|
|
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), and the builder is
|
|
asked for each in turn: it clones, reads the manifest at that path, produces what it declares,
|
|
publishes each artifact into the mesh's own registry, and hands back the module with its artifacts
|
|
pinned and the commit recorded. The mesh records that, and from then on the module is described by
|
|
something it made rather than by a placeholder.
|
|
|
|
**The controller is built like the rest.** It was carried in and published once, which got the
|
|
mesh running; building it from its own repository and path is what makes it upgradeable. The first
|
|
time that happens is the moment the mesh stops depending on the installer for anything.
|
|
|
|
**And then the catalogue.** Every module with source of its own is built the same way. Until this
|
|
has happened a mesh can install only what is public or carried, which is the foundation and little
|
|
else.
|
|
|
|
Only after all of that is the ordinary loop available: change a module's source, the mesh notices
|
|
its own copy is older than the source, rebuilds it, and rolls it out. That loop is what makes
|
|
moving services across one at a time possible, so it is part of installing rather than something
|
|
that comes later.
|
|
|
|
What remains after *that* belongs to somebody else: adding machines, and deciding what they run.
|
|
|
|
## Joining
|
|
|
|
A machine joins with the host binary and a token. It does not raise a foundation, does not install a
|
|
registry, and is never enrolled twice. The mesh already knows how to tell a machine what to be;
|
|
joining is the point at which a machine starts listening. A machine in use joins **adopted**: the
|
|
token says so, the operator has stopped the predecessor's control on it first, and from then on it
|
|
keeps what it has until each module is taken
|
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
|
|
|
## Where the line falls
|
|
|
|
The installer owns everything that is the mesh's own. The lab owns only what is the lab's: raising
|
|
virtual machines, giving them addresses that resolve nowhere, and injecting faults.
|
|
|
|
**The lab runs the installer. It does not describe installation.** This is the whole point. A
|
|
second description kept in step with the first is the arrangement that already failed — the fixture
|
|
invented a registry that exists in no production, and hid two separate faults for as long as it
|
|
existed. Anything the lab must do that the installer does not is either a lab concern or a hole in
|
|
the installer, and saying which is a decision, not a convenience.
|
|
|
|
## What is not yet true
|
|
|
|
Stated plainly, because a document that implies otherwise is worse than none.
|
|
|
|
**The installer is not packaged.** It builds from source. An operator raising a first machine still
|
|
needs a toolchain and a working tree, which is most of the burden the installer exists to remove.
|
|
|
|
**A mesh cannot say how it was raised.** Nothing afterwards can contradict a claim that a machine
|
|
was brought up the supported way, so the rule that it must be is, today, unenforced.
|
|
|
|
**Nothing asks for the core modules yet.** Installing ends with a builder that works — a raised
|
|
machine will build the shared base and a module on top of it when asked — and nothing asks. The
|
|
paragraphs above describing the catalogue being built are a thing somebody now types, rather than a
|
|
thing that cannot happen.
|
|
|
|
**A module's declaration still has to be copied onto the machine by hand.** The installer reads the
|
|
registry's and the builder's manifests from a checkout somebody put there. The controller's is no
|
|
longer among them: it lives in the controller's own repository
|
|
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), the build the
|
|
installer runs reports it with the artifact resolved, and genesis registers that
|
|
([issue 072](../../04-ISSUES/072-the-controllers-manifest-exists-twice/00-report.md)). The
|
|
builder's manifest belongs in the same repository and is still read from the catalogue; that is
|
|
the last of this.
|
|
|
|
**A machine has no account for a registry that asks for one.** The mesh grants a consumer a
|
|
credential for a database; it does not yet do so for the store its own images live in. Genesis
|
|
avoids the question by carrying the image it needs, which makes this a joining problem and a
|
|
pulling problem, not a genesis one.
|
|
|
|
**The SDK still comes from a git URL, and the ordering that fixes it is decided but not built.**
|
|
[ADR 0076](../../02-DECISIONS/0076-the-sdk-is-a-published-package.md) settles that the package
|
|
registry (gitea) comes up and the SDK is published into it *before* the base toolchain is built, so
|
|
the toolchain resolves the SDK by version rather than cloning it — closing
|
|
[issue 053](../../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md). The
|
|
builder already knows how to be handed a package-registry credential and inject it into a build; what
|
|
is not yet wired is the genesis step that raises gitea and publishes the SDK ahead of the base, and
|
|
the toolchain's own manifest still names the SDK by a git URL. Until both land, the base build clones
|
|
the SDK inside `docker build`, which is slow and names a branch head rather than a version.
|
|
|
|
## How these rules are checked
|
|
|
|
| Rule | Checked by |
|
|
|---|---|
|
|
| Genesis works on a machine that is not the lab | The bed raises its first machine by running the installer, not around it. A bed that stops doing so fails its own acceptance check. |
|
|
| The other machines join, and are not re-raised | The bed gives them the host binary and a token only. A second enrolment of the first machine is a failure, not a no-op. |
|
|
| Every step may be run again | The installer is re-run against a raised machine and must change nothing and report why. |
|
|
| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. |
|
|
| The installer is what installed this | **Nothing.** See above. |
|
|
| The builder can arrive on a fresh mesh | The installer carries it and installs it as its last step, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. |
|
|
| The controller a mesh runs is one it built | The genesis bed asserts the running controller is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
|
|
| A core module is built rather than only carried | The controller is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
|
|
| Installing produced a mesh that can produce | A module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. |
|