A module is a repository and a path, and installing is described to its end #33
@@ -0,0 +1,89 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-09-12
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 0009-modules-and-the-graph.md
|
||||
---
|
||||
|
||||
# 69. A module is a repository and a path within it
|
||||
|
||||
## Context
|
||||
|
||||
**The builder clones one repository and reads `module.json` at its root.** The to-be design says
|
||||
so in as many words — *"one file at the root"* — and the code implements it: clone, read the root
|
||||
manifest, build what it declares.
|
||||
|
||||
**Nothing that exists is shaped that way.** The catalogue holds sixty-seven modules, each in its
|
||||
own directory, and has no manifest at its root. None of the five code repositories has one either.
|
||||
So today the builder cannot be asked to build any module that exists: pointed at the catalogue it
|
||||
finds no manifest, and pointed at a module's source it finds no manifest.
|
||||
|
||||
**The system being replaced already works the other way**, and has for years: a monorepo with one
|
||||
directory per piece of software, and the coordinator builds a module from a repository and a path
|
||||
inside it. The root-only assumption is not a simplification of that — it is a different model that
|
||||
was never reconciled with it.
|
||||
|
||||
**And it splits what a build needs into two places.** The control plane's manifest sits in the
|
||||
catalogue; the source it describes sits in the control plane's own repository. A build must read
|
||||
one tree, so under the root-only model neither location can be built from.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. One repository per module.** Rejected. Sixty-seven repositories for sixty-seven modules, most
|
||||
of which are a single manifest naming a public image, and every one needing its own creation,
|
||||
permissions and lifecycle. It also contradicts [ADR 0015](0015-applications-live-in-their-own-repository.md),
|
||||
which put *applications* in their own repositories precisely because modules do not need one.
|
||||
|
||||
**2. Keep manifests in the catalogue and source elsewhere, and have a build fetch both.** Rejected.
|
||||
A build would clone two trees whose versions can disagree, so "what commit is this module?" stops
|
||||
having one answer — and that question is the whole basis of knowing when to rebuild.
|
||||
|
||||
**3. A module is a repository and a path within it.** Chosen. It is what the current system does,
|
||||
what the catalogue already looks like, and it keeps a module's description beside the thing it
|
||||
describes.
|
||||
|
||||
## Decision
|
||||
|
||||
**A module is named by a repository and a path within it.** The path holds `module.json`, and
|
||||
everything that manifest declares is produced from that path. A module whose path is the root is
|
||||
the ordinary case of this, not a separate one.
|
||||
|
||||
**A module's manifest lives beside its source.** Where a module has code, its directory holds both,
|
||||
so one commit answers "what is this module, and what is it made of". Where a module has no source —
|
||||
a manifest naming a public image — the directory holds only the manifest, and there is nothing to
|
||||
build.
|
||||
|
||||
**This moves the core modules.** The control plane and the builder are built from the control
|
||||
plane's repository, so their manifests belong in that repository at their own paths, not in the
|
||||
catalogue. The catalogue keeps the modules whose source it holds, and the modules that are only a
|
||||
manifest.
|
||||
|
||||
**One commit, one module version.** Because a module is one path in one repository, the commit that
|
||||
built it identifies it exactly, and "the source has moved ahead of what the mesh holds" stays a
|
||||
question with a yes or no answer.
|
||||
|
||||
## Consequences
|
||||
|
||||
The builder gains a path alongside the repository and the ref. A build is `repository, path, ref`,
|
||||
and the manifest it returns is the module the mesh records.
|
||||
|
||||
The catalogue stops being the place every manifest lives, and becomes the place manifests live
|
||||
*when their module has no other home*. That is a smaller claim than it sounds: most of the
|
||||
sixty-seven stay exactly where they are.
|
||||
|
||||
Two repositories change shape — the control plane's gains manifests for the modules built from it.
|
||||
Nothing else moves.
|
||||
|
||||
A repository can hold modules that are built and modules that are not, and no rule distinguishes
|
||||
them beyond whether their manifest declares anything to build.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A module is buildable from its repository and path | The builder is asked for a module by repository and path, and returns a manifest whose artifacts are pinned to digests the mesh's registry assigned. |
|
||||
| A manifest sits beside what it describes | A module declaring something to build, whose path holds no source to build it from, is refused at build time rather than producing an empty result. |
|
||||
| One commit identifies one module | Two builds of the same repository, path and commit produce the same digests. |
|
||||
| The core modules are built like any other | The control plane is rebuilt from its own repository and path, and the running mesh is upgraded to it — the same path an ordinary module takes. |
|
||||
@@ -100,6 +100,8 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0036** — [Bootstrap ends at a usable mesh, and the first credential comes from a person](0036-bootstrap-ends-at-a-usable-mesh.md)
|
||||
- **0066** — [Public routing is name-agnostic, its names are resolved inside the mesh, and an internal authority can certify them](0066-public-routing-is-name-agnostic.md)
|
||||
- **0067** — [Genesis is a pivot: a temporary control plane installs the registry that makes it permanent](0067-genesis-is-a-pivot.md)
|
||||
- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(proposed)*
|
||||
- **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md)
|
||||
|
||||
### What runs on them, and how it gets there
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-catalog modules/builder
|
||||
- mesh-control internal/builder
|
||||
- mesh-control internal/catalogue/build.go
|
||||
- mesh-control internal/inventory/secrets.go
|
||||
- mesh-control cmd/mesh-builder
|
||||
updated: 2026-08-31
|
||||
updated: 2026-09-12
|
||||
decisions:
|
||||
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||
- 02-DECISIONS/0010-delivery.md
|
||||
- 02-DECISIONS/0005-the-node-host.md
|
||||
@@ -39,13 +41,25 @@ resolution, and assigning it brings both. So the module count does not return, b
|
||||
that made it return — *a module is expensive, so put several things in one* — is gone. A module
|
||||
here is cheap: a manifest and, usually, nothing else.
|
||||
|
||||
## One file at the root
|
||||
## One file, at the module's own path
|
||||
|
||||
`module.json`, and a convention somebody can look for beats a setting somebody has to find. It
|
||||
says what the module is, what it provides and requires, what it claims, what capabilities it
|
||||
needs, what it puts on a machine — and, if anything must be produced from the source, what to
|
||||
build.
|
||||
|
||||
**A module is a repository and a path within it** ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)).
|
||||
The manifest sits at that path, beside the source it describes, and everything it declares is
|
||||
produced from there. A module whose path is the repository root is the ordinary case of this and
|
||||
not a separate shape.
|
||||
|
||||
*Corrected 2026-09-12. This section previously said the manifest was at the root, full stop, which
|
||||
made a repository hold exactly one module. Nothing that exists is shaped that way: the catalogue
|
||||
holds sixty-seven modules in their own directories, the system being replaced has always built a
|
||||
module from a repository and a path, and the root-only reading left every existing module
|
||||
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
|
||||
it finds no manifest either.*
|
||||
|
||||
## The manifest in the repository is not the manifest the mesh holds
|
||||
|
||||
A resource names an artifact:
|
||||
@@ -345,3 +359,42 @@ first copy comes from outside, exactly once, and every copy after it is the mesh
|
||||
machine across the private network, because a mesh-scoped provision that only answers locally is
|
||||
not one. A container that is running is not a registry that replies, and this project has paid for
|
||||
that distinction once already.*
|
||||
|
||||
## The three the loop cannot build, and there are only three
|
||||
|
||||
*2026-09-12. Written down because it keeps being rediscovered as if it were new, once per
|
||||
component. It is one rule, it has three instances, and the list is closed.*
|
||||
|
||||
**Anything the build loop needs in order to run cannot be delivered by the build loop.** It arrives
|
||||
from outside exactly once, and from then on it is an ordinary module, upgraded like one. What
|
||||
"from outside" means is *not* the same for all three, and saying so matters, because two of them
|
||||
have a route and one does not:
|
||||
|
||||
| What | Why it cannot come through the loop | How it arrives |
|
||||
|---|---|---|
|
||||
| The control plane | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry ([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)) |
|
||||
| The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) |
|
||||
| The builder | It is what builds. Nothing builds it before it runs. | **Nothing yet.** Not carried, not public. See below. |
|
||||
|
||||
**The builder has no route, and this is the open one.** It is built from the control plane's
|
||||
repository, so it cannot be pulled from the public internet like the registry; and the installer
|
||||
carries one image only, the control plane's. So a mesh raised by the installer today has no builder
|
||||
and no way to obtain one, which means it cannot build the catalogue, which means every module
|
||||
waiting on a digest keeps waiting. Whatever answers this — the installer carrying a second image,
|
||||
the control plane's own build producing both, or the first builder being fetched some other way —
|
||||
is the last thing between a raised mesh and a self-upgrading one.
|
||||
|
||||
**There is no fourth.** Everything else the mesh runs is either upstream — a third-party image
|
||||
pulled by digest — or built by the builder from a repository and published to the registry. So the
|
||||
question "how does *this* one get here first?" has an answer for every module without asking it
|
||||
again: if it is not one of the three above, it comes through the loop.
|
||||
|
||||
**A carried artifact is not a differently-pinned artifact.** Once published it is named by a digest
|
||||
the mesh's registry assigned, exactly like everything the builder produces. A reader cannot tell
|
||||
from a running mesh which of its images were carried, and that is the point: carrying is how the
|
||||
first copy arrives, not what it permanently is.
|
||||
|
||||
*Checked by the thing already checked at genesis: after installing, the running control plane is
|
||||
pinned to a digest the mesh's own registry assigned, and not to the id of the image the installer
|
||||
carried. The same check applies to the builder and to the registry, and it is the same check —
|
||||
an image id where a registry digest belongs means the pivot did not finish.*
|
||||
|
||||
Reference in New Issue
Block a user