A module is a repository and a path within it
The design said a module's manifest sits at a repository's root, full stop, which means one repository per module. Nothing that exists is shaped that way: the catalogue holds sixty-seven modules one to a directory, no code repository has a manifest at its root, and the system being replaced has always built a module from a repository and a path. So the builder could be asked to build nothing that exists — pointed at the catalogue it finds no manifest, pointed at a module's source it finds none either. Recorded as a decision because it moves the core modules' manifests beside their source, and corrects the design that said otherwise. Also corrects, in the same document, how the three things the build loop cannot produce actually arrive. They were written as though all three were carried in. Only the control plane is: the registry is pulled from the public internet, and the builder has no route at all — which is now stated as the open one rather than implied to be solved. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -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