Rename mesh-control -> mesh-controller, substrate -> foundation

One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-16 18:40:40 +02:00
parent 49b80d8516
commit 5d6e8fbe7a
89 changed files with 785 additions and 785 deletions
+25 -25
View File
@@ -31,9 +31,9 @@
* MESH_LAB_INCUS='sudo -n incus'
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap
* MESH_LAB_BUNDLE=.../mesh-host/examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
* MESH_LAB_CATALOG=.../mesh-catalog/modules
* MESH_LAB_SOURCE=<forge>/mesh-control.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_SOURCE=<forge>/mesh-controller.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_KEEP=1 to leave it standing afterwards
*/
import { test, before, after } from "node:test";
@@ -45,7 +45,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec, push } from "../../src/lifecycle/operate.ts";
import { bootstrapBinaryPath, hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
import { genesis, type GenesisResult } from "./genesis.ts";
import { incus } from "../../src/incus/client.ts";
import { instanceNameOf } from "../../src/lifecycle/operate.ts";
@@ -69,7 +69,7 @@ const REGISTRY = `${ANCHOR}:5000`;
*/
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
/**
* What `amqp-ping` requires, and what the substrate does not supply.
* What `amqp-ping` requires, and what the foundation does not supply.
*
* The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a
* record of, so it provides nothing to anything. A module asking for `amqp` is asking for a
@@ -98,7 +98,7 @@ const FILTER_MODULE = "nftables";
const STORE = { module: "postgres", repo: "mesh-catalog", path: "modules/postgres", container: "mesh-postgres" };
const CATALOGUE = { module: "mesh-catalog", repo: "mesh-catalog", path: "modules/mesh-catalog", container: "mesh-catalog" };
/** The control plane, rebuilt from its own repository — the step that ends the installer's tenure. */
const CONTROL_PLANE = { module: "mesh-control", repo: "mesh-control", path: "", container: "mesh-control" };
const CONTROL_PLANE = { module: "mesh-controller", repo: "mesh-controller", path: "", container: "mesh-controller" };
/**
* What this mesh must hold when it is finished, and what must be RUNNING on the machine.
@@ -108,14 +108,14 @@ const CONTROL_PLANE = { module: "mesh-control", repo: "mesh-control", path: "",
* the registry and the builder — are here too: they are carried in, and a mesh missing any of them
* is not one.
*/
const MUST_HOLD = ["mesh-control", "distribution", "builder", "mesh-tools", "postgres",
const MUST_HOLD = ["mesh-controller", "distribution", "builder", "mesh-tools", "postgres",
"mesh-catalog", "lavinmq", "amqp-ping"];
const MUST_RUN = ["mesh-control", "mesh-registry", "mesh-broker", "mesh-store",
const MUST_RUN = ["mesh-controller", "mesh-registry", "mesh-broker", "mesh-store",
"mesh-postgres", "mesh-catalog", "mesh-lavinmq", "amqp-ping"];
/** Named once, because the step title is also how later steps say what they waited on. */
const NEEDS = "the mesh runs a broker for that module to talk to";
const GENESIS = "a bare machine becomes a mesh of one, raised by the installer";
const SUBSTRATE = "the substrate is up — a store and a broker of the mesh's own";
const FOUNDATION = "the foundation is up — a store and a broker of the mesh's own";
const BUILT_CP = "the control plane is one this mesh built, not one it was handed";
const PIVOTED = "the pivot finished — what raised the mesh is gone";
const HAS_REGISTRY = "the registry serves this mesh its own images";
@@ -191,7 +191,7 @@ const skip =
!installer ? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap" :
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" :
!sourceRef ? "MESH_LAB_SOURCE_REF is not set to the commit to build" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a substrate template" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation template" :
!catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
false;
@@ -220,19 +220,19 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
// The control plane is a live, mesh-MANAGED container: the mesh recreates it whenever its
// declaration changes — most visibly when the machine first gets its `.internal` name on the
// private network, which becomes the container's `--add-host` (containers are immutable, so a new
// spec is a new container). A `docker exec mesh-control` that lands in that brief recreate window
// spec is a new container). A `docker exec mesh-controller` that lands in that brief recreate window
// fails with "container ... is not running". That is not the mesh being wrong — it is a command
// racing a legitimate restart — so it is retried until the control plane answers again. A real
// command failure (anything else) still throws at once.
const deadline = Date.now() + (timeoutMs ?? 120_000);
for (;;) {
const { out, ok } = await on(CONTROL, `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
const { out, ok } = await on(CONTROL, `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs);
if (ok) return out;
if (/is not running|No such container/i.test(out) && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 2_000));
continue;
}
throw new Error(`${CONTROL}: docker exec mesh-control /mesh-control ${command}\n${out}`);
throw new Error(`${CONTROL}: docker exec mesh-controller /mesh-controller ${command}\n${out}`);
}
}
@@ -304,7 +304,7 @@ async function waitForContainer(node: string, container: string, seconds = 200):
*
* **The control plane runs in a container, so a file on the machine is not a file it can open.**
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
* inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is
* inside mesh-controller, which is correct and was briefly mistaken for a missing manifest. It is
* copied the last step of the way with `docker cp`.
*
* **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp`
@@ -315,7 +315,7 @@ async function registerModule(module: string, manifest: string): Promise<string>
const onMachine = `/tmp/${module}.json`;
const inContainer = `/${module}.json`;
await push(instanceId, CONTROL, manifest, onMachine);
await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`);
await must(CONTROL, `docker cp ${onMachine} mesh-controller:${inContainer}`);
return mesh(`module add ${inContainer}`);
}
function tokenFrom(said: string): string {
@@ -393,7 +393,7 @@ function report(name: string): string {
*/
const PLAN: { code: string; title: string }[] = [
{ code: "R1", title: GENESIS },
{ code: "R2", title: SUBSTRATE },
{ code: "R2", title: FOUNDATION },
{ code: "R3", title: BUILT_CP },
{ code: "R4", title: PIVOTED },
{ code: "R5", title: HAS_REGISTRY },
@@ -479,7 +479,7 @@ before(async () => {
// whose anchor is somewhere else every node, including this one, would enrol against an
// address nothing answers on. The installer refuses to guess it and says so, which is
// right: it does not know what this machine is called from outside.
bundleTemplate: substrateBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
bundleTemplate: foundationBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
registry: REGISTRY,
source,
sourceRef,
@@ -514,7 +514,7 @@ before(async () => {
// saying it published an image and the registry serving one are different facts, and it is the
// second that matters.
await step("R2", SUBSTRATE, GENESIS, async () => {
await step("R2", FOUNDATION, GENESIS, async () => {
await waitForContainer(CONTROL, "mesh-store", 120);
await waitForContainer(CONTROL, "mesh-broker", 120);
return (await on(CONTROL, `docker ps --format '{{.Names}}\t{{.Status}}'`)).out;
@@ -525,7 +525,7 @@ before(async () => {
// the image has to be one this mesh's own registry serves.
await step("R3", BUILT_CP, GENESIS, async () => {
const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-control 2>&1`)).out.trim();
`docker inspect -f '{{.Config.Image}}' mesh-controller 2>&1`)).out.trim();
assert.match(image, /@sha256:[0-9a-f]{64}/,
`the control plane names its image by tag, not by digest: ${image}`);
assert.ok(image.includes(":5000/"),
@@ -535,17 +535,17 @@ before(async () => {
await step("R4", PIVOTED, BUILT_CP, async () => {
const ps = (await on(CONTROL, `docker ps -a --format '{{.Names}}'`)).out;
assert.doesNotMatch(ps, /^temp-mesh-control$/m,
assert.doesNotMatch(ps, /^temp-mesh-controller$/m,
`the temporary control plane is still here, so the pivot did not finish:\n${ps}`);
return ps;
});
await step("R5", HAS_REGISTRY, SUBSTRATE, async () => {
await step("R5", HAS_REGISTRY, FOUNDATION, async () => {
await waitForContainer(CONTROL, "mesh-registry", 120);
const held = (await on(CONTROL,
`curl -sS --max-time 15 http://127.0.0.1:5000/v2/_catalog`)).out;
assert.match(held, /mesh-control/,
`the registry serves no mesh-control, so nothing was published into it:\n${held}`);
assert.match(held, /mesh-controller/,
`the registry serves no mesh-controller, so nothing was published into it:\n${held}`);
return held.trim();
});
@@ -597,7 +597,7 @@ before(async () => {
return built;
});
// A store of its own. **Not the substrate's.** The installer raises a store for the control
// A store of its own. **Not the foundation's.** The installer raises a store for the control
// plane to keep its own records in, the way it raises a broker — plumbing, not a module the mesh
// has any record of, so it provides nothing to anything. A module that wants a database wants a
// provider in the graph, and the catalogue below is the first thing to want one.
@@ -790,7 +790,7 @@ before(async () => {
// broker: a module's account is scoped to what it declares it emits and consumes, and calling
// a tool needs a temporary reply queue that scope does not cover. So a module can SERVE tools
// and cannot CALL them, and nothing issues an account to anyone who wants to ask (novox/hq
// issue 049). Until that is decided the caller is the substrate's bootstrap admin over the
// issue 049). Until that is decided the caller is the foundation's bootstrap admin over the
// broker's loopback, reached by joining its network namespace.
const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-catalog`)).out.trim();
@@ -1061,7 +1061,7 @@ after(async () => {
for (const name of [
GENESIS,
SUBSTRATE,
FOUNDATION,
BUILT_CP,
PIVOTED,
HAS_REGISTRY,