diff --git a/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md b/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md new file mode 100644 index 0000000..7c43917 --- /dev/null +++ b/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md @@ -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. | diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 28b6721..eaf273f 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -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 diff --git a/03-DESIGN/01-to-be/12-a-module-repository.md b/03-DESIGN/01-to-be/12-a-module-repository.md index 127c231..e5ca4a8 100644 --- a/03-DESIGN/01-to-be/12-a-module-repository.md +++ b/03-DESIGN/01-to-be/12-a-module-repository.md @@ -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.*