diff --git a/README.md b/README.md index d0b878f..e69ad53 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ test. | | **Bootstrap** | **Full** | |---|---|---| -| Contains | virtual machines, the node host, a pinned substrate bundle | a complete mesh: forge, control plane, delivery, modules | +| Contains | virtual machines, the node host, a pinned foundation bundle | a complete mesh: forge, control plane, delivery, modules | | Verdict from | what the host reports about the state it reconciled | a pipeline result ending in verify | | Exercises | tiers 0 and 1 | tier 2 and above, and modules | | Exists to | **develop the mesh** | **test what runs on it** | @@ -140,23 +140,23 @@ is written down here rather than reconstructed a third time. ```sh export MESH_LAB_HOST_BINARY=/mesh-host -export MESH_LAB_BUNDLE=/examples/substrate-first-node.lock -export MESH_LAB_MODULES=/examples/modules -export MESH_LAB_BUILDER=/build/mesh-builder # build/, which is git-ignored +export MESH_LAB_BUNDLE=/examples/foundation-first-node.lock +export MESH_LAB_MODULES=/examples/modules +export MESH_LAB_BUILDER=/build/mesh-builder # build/, which is git-ignored # The installer. `suite` builds it with mesh-host's `make bootstrap`, which embeds a `docker save` # of the control-plane image — so it is built AFTER that image, in the same run, or it carries a # stale one sealed inside a binary where nothing would ever notice. The whole-mesh bed raises its -# anchor by RUNNING this, rather than by applying a substrate bundle itself (novox/hq ADR 0067). +# anchor by RUNNING this, rather than by applying a foundation bundle itself (novox/hq ADR 0067). export MESH_LAB_BOOTSTRAP_BINARY=/mesh-bootstrap -export MESH_LAB_CONTROL_IMAGE=mesh-control:development # optional; what it carries +export MESH_LAB_CONTROL_IMAGE=mesh-controller:development # optional; what it carries # A checkout of the mesh's catalogue. The installer reads the registry's and the control plane's # manifests from a copy of it ON THE MACHINE, because at genesis there is no forge, no build # machine and — until the registry is up — nothing serving anything. export MESH_LAB_CATALOG=/modules -# Built with `go build -o ./examples/` in mesh-control. +# Built with `go build -o ./examples/` in mesh-controller. export MESH_LAB_PROVISIONER=/postgres-provisioner export MESH_LAB_OBJECTSTORE_PROVISIONER=/objectstore-provisioner export MESH_LAB_ROUTE_PROXY=/route-proxy diff --git a/one-node-mesh-report.json b/one-node-mesh-report.json new file mode 100644 index 0000000..512c53c --- /dev/null +++ b/one-node-mesh-report.json @@ -0,0 +1,161 @@ +{ + "scenario": "one-node-mesh", + "established": 22, + "of": 22, + "steps": [ + { + "code": "R1", + "title": "a bare machine becomes a mesh of one, raised by the installer", + "status": "pass", + "seconds": 316, + "why": "" + }, + { + "code": "R2", + "title": "the foundation is up — a store and a broker of the mesh's own", + "status": "pass", + "seconds": 1, + "why": "" + }, + { + "code": "R3", + "title": "the control plane is one this mesh built, not one it was handed", + "status": "pass", + "seconds": 0, + "why": "" + }, + { + "code": "R4", + "title": "the pivot finished — what raised the mesh is gone", + "status": "pass", + "seconds": 0, + "why": "" + }, + { + "code": "R5", + "title": "the registry serves this mesh its own images", + "status": "pass", + "seconds": 0, + "why": "" + }, + { + "code": "R6", + "title": "the machine is enrolled, and an agent is running on it", + "status": "pass", + "seconds": 0, + "why": "" + }, + { + "code": "R7", + "title": "the builder is installed as a module, with an account", + "status": "pass", + "seconds": 0, + "why": "" + }, + { + "code": "P1", + "title": "the mesh builds the shared base from source", + "status": "pass", + "seconds": 3, + "why": "" + }, + { + "code": "P2", + "title": "the mesh builds and runs a store of its own", + "status": "pass", + "seconds": 7, + "why": "" + }, + { + "code": "P3", + "title": "the mesh builds and runs its own catalogue", + "status": "pass", + "seconds": 4, + "why": "" + }, + { + "code": "P4", + "title": "the mesh rebuilds its own control plane from source", + "status": "pass", + "seconds": 30, + "why": "" + }, + { + "code": "N1", + "title": "the mesh puts itself on a private network, and its machine has a name", + "status": "pass", + "seconds": 4, + "why": "" + }, + { + "code": "N2", + "title": "the machine has a packet filter, loaded from what modules declared", + "status": "pass", + "seconds": 2, + "why": "" + }, + { + "code": "U1", + "title": "the mesh builds a module standing on that base", + "status": "pass", + "seconds": 14, + "why": "" + }, + { + "code": "U2", + "title": "the mesh runs a broker for that module to talk to", + "status": "pass", + "seconds": 26, + "why": "" + }, + { + "code": "U3", + "title": "the anchor runs the module the mesh built", + "status": "pass", + "seconds": 7, + "why": "" + }, + { + "code": "V1", + "title": "the control plane can describe the mesh, and what it says is true", + "status": "pass", + "seconds": 1, + "why": "" + }, + { + "code": "V2", + "title": "the catalogue holds every module this mesh built", + "status": "pass", + "seconds": 3, + "why": "" + }, + { + "code": "V3", + "title": "the machine's networking is what the modules asked for", + "status": "pass", + "seconds": 1, + "why": "" + }, + { + "code": "V4", + "title": "every resource the mesh declared is true on the machine", + "status": "pass", + "seconds": 13, + "why": "" + }, + { + "code": "E1", + "title": "a change to a module's source reaches the machine on its own", + "status": "pass", + "seconds": 12, + "why": "" + }, + { + "code": "E2", + "title": "the mesh comes back after the machine reboots", + "status": "pass", + "seconds": 33, + "why": "" + } + ] +} \ No newline at end of file diff --git a/provisioners/README.md b/provisioners/README.md index 13f5acc..1b97057 100644 --- a/provisioners/README.md +++ b/provisioners/README.md @@ -39,7 +39,7 @@ must reach the same state from wherever it starts. That means, in order: Step 2 is the half usually missing, and it is the same rule the host follows about removing what it declared and no longer declares. -The reference implementation lives in `mesh-control/examples/postgres-provisioner`, because that +The reference implementation lives in `mesh-controller/examples/postgres-provisioner`, because that is where the contract is defined and where the language is already set up to read it. The lab's job is the other half: raising a real PostgreSQL and proving that what the mesh delivered becomes a login that works, a rotation that takes effect, and a revocation that bites. diff --git a/scenarios/anthropic-bed.yml b/scenarios/anthropic-bed.yml index 02199c6..c2529a0 100644 --- a/scenarios/anthropic-bed.yml +++ b/scenarios/anthropic-bed.yml @@ -8,10 +8,10 @@ # The flow the test drives (OAuth stubbed, so it is the FLOW that is proven, not the vendor): # the manager module seals the refresh token to the node's PUBLIC key -> the host unseals it and # mounts the cleartext at the manager's bound path -> the manager calls the stub token endpoint -> -# submits back only { access token, re-sealed box } -> mesh-control seals the access token per +# submits back only { access token, re-sealed box } -> mesh-controller seals the access token per # consumer holder -> the consumer runtime writes ~/.claude/.credentials.json, access-token-only. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # Build BOTH runtime images into the local daemon first (the scenario stocks and serves them by # digest, which is where the host pulls them from): # scripts/build-module-runtime.sh anthropic-manager /tmp/anthropic-manager.tar @@ -35,7 +35,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # The two model-access runtimes, built by scripts/build-module-runtime.sh into the local daemon and # loaded onto the machine, which holds them by their own image IDs. - mesh-runtime-anthropic-manager:development diff --git a/scenarios/audit-node.yml b/scenarios/audit-node.yml index 8107155..1eba0e2 100644 --- a/scenarios/audit-node.yml +++ b/scenarios/audit-node.yml @@ -1,6 +1,6 @@ # One machine that becomes a mesh and then assigns itself the audit logger. # -# The substrate is first-node's — a store, a broker, the control plane — and one module image on +# The foundation is first-node's — a store, a broker, the control plane — and one module image on # top: the tool runtime carrying the audit-logger (mesh-catalog). The node enrols itself and the # mesh assigns it the audit logger, so its events account is one the mesh delivered, not the # broker's own (novox/hq ADR 0048). @@ -20,7 +20,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # The tool runtime with the audit-logger, built by scripts/build-runtime-image.sh into the local # daemon and loaded onto the machine, which holds it by its own image ID. - mesh-runtime-audit:development diff --git a/scenarios/catalogue-apps.yml b/scenarios/catalogue-apps.yml index 9a46c1d..5c92fdc 100644 --- a/scenarios/catalogue-apps.yml +++ b/scenarios/catalogue-apps.yml @@ -1,6 +1,6 @@ # One machine that becomes a mesh and is then assigned a SECOND wave of modules at once — the ones # converted this session (novox/hq ADR 0039/0048/0052): mongodb, unifi and marrytts, with postgres -# carried along as a known-good control. This is catalogue-small's sibling: same first-node substrate, +# carried along as a known-good control. This is catalogue-small's sibling: same first-node foundation, # same one-push co-residence, a different (and heavier) set of modules. # # The four exercise the three converted shapes: @@ -14,7 +14,7 @@ # None of the four share a directory, so the shared-workspace refusal (novox/hq 04-ISSUES/012) does # not bite; that class stays for a later bed. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-module-runtime.sh {mongodb,unifi,postgres} build the runtime images into the local # daemon (mongodb carries mongosh; unifi and postgres carry their CLIs). The service images # (mongo, unifi-controller, marytts, postgres) must be in the local daemon to be stocked. @@ -31,13 +31,13 @@ machines: egress: true inbound: allow # Sized up past catalogue-small's 6GiB: this wave carries two heavy JVM/embedded-DB service - # containers (the UniFi controller and MaryTTS) on top of the substrate, mongodb, postgres and + # containers (the UniFi controller and MaryTTS) on top of the foundation, mongodb, postgres and # three node runtimes — a dozen containers, two of them memory-hungry at startup. memory: 8GiB cpus: 4 images: - - mesh-control:development + - mesh-controller:development # The runtimes built by scripts/build-module-runtime.sh and stocked here. marrytts needs none. - mesh-runtime-mongodb:development - mesh-runtime-unifi:development diff --git a/scenarios/catalogue-media.yml b/scenarios/catalogue-media.yml index 9dc1534..1921ec9 100644 --- a/scenarios/catalogue-media.yml +++ b/scenarios/catalogue-media.yml @@ -16,7 +16,7 @@ # after enrol and before the push. Each module additionally OWNS its own config directory # (/services/{sonarr,radarr}/config), which the mesh does create. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-module-runtime.sh {sonarr,radarr} build the two runtime images into the local daemon # (they speak HTTP and need no CLI added). The service images lscr.io/linuxserver/{sonarr,radarr} # must be in the local daemon to be stocked. @@ -32,14 +32,14 @@ machines: at: { segment: hosting, address: [192.0.2.10] } egress: true inbound: allow - # Two *arr apps (server + runtime each) on top of the first-node substrate — seven containers. + # Two *arr apps (server + runtime each) on top of the first-node foundation — seven containers. # The Servarr images are lighter than catalogue-apps' JVM pair, so catalogue-small's 6GiB is # ample headroom. memory: 6GiB cpus: 4 images: - - mesh-control:development + - mesh-controller:development # The two runtimes built by scripts/build-module-runtime.sh and stocked here. - mesh-runtime-sonarr:development - mesh-runtime-radarr:development diff --git a/scenarios/catalogue-mqtt.yml b/scenarios/catalogue-mqtt.yml index 866b7c3..6da3e98 100644 --- a/scenarios/catalogue-mqtt.yml +++ b/scenarios/catalogue-mqtt.yml @@ -11,7 +11,7 @@ # the broker — so "the broker came up" is itself the proof the seed ran, because an unseeded store # crash-loops the broker. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-module-runtime.sh mosquitto builds mesh-runtime-mosquitto:development (carrying # mosquitto_ctrl and the compiled bootstrap entrypoint) into the local daemon, which this scenario # pulls from the internet over its uplink. eclipse-mosquitto:2 must be in the local @@ -32,7 +32,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development - mesh-runtime-mosquitto:development place: diff --git a/scenarios/catalogue-small.yml b/scenarios/catalogue-small.yml index f45c5f1..555be7a 100644 --- a/scenarios/catalogue-small.yml +++ b/scenarios/catalogue-small.yml @@ -3,14 +3,14 @@ # # The per-backend beds each assign one module: postgres (a database provider), minio (an object-store # provider), redis (a cache provider + tools), plex (a tools module). This raises the same first-node -# substrate and then assigns all four to the one anchor in a single push, so the proof is that they +# foundation and then assigns all four to the one anchor in a single push, so the proof is that they # resolve and come up TOGETHER on one node — nothing new about any single module, everything new about # their co-residence. # # The four are chosen because none of them share a directory, so the shared-workspace refusal # (novox/hq 04-ISSUES/012 — the media stack) does not bite here; that class stays for a later bed. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-module-runtime.sh {postgres,redis,minio,plex} build the four runtime images into the # local daemon (postgres carries psql, minio carries mc), which this scenario stocks and serves by # the internet over its uplink. Only the mesh's own images come from the local daemon. @@ -26,14 +26,14 @@ machines: at: { segment: hosting, address: [192.0.2.10] } egress: true inbound: allow - # Sized up: this anchor runs the substrate (store, broker, control) plus four modules — three of + # Sized up: this anchor runs the foundation (store, broker, control) plus four modules — three of # which are a server container and a runtime container each — so a dozen containers at once. The # single-module beds run at 3GiB; co-residence needs the headroom. memory: 6GiB cpus: 4 images: - - mesh-control:development + - mesh-controller:development # The four per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. plex # needs no server image in the lab — its runtime serves tools with no Plex to reach. - mesh-runtime-postgres:development diff --git a/scenarios/first-node.yml b/scenarios/first-node.yml index 4065e23..56f7a07 100644 --- a/scenarios/first-node.yml +++ b/scenarios/first-node.yml @@ -1,4 +1,4 @@ -# One machine, raising a substrate from the bundle its host carries. +# One machine, raising a foundation from the bundle its host carries. # # This is the bootstrap class (novox/hq ADR 0009): no forge, no control plane to talk to, no # delivery. It exists to develop the steps of raising a mesh on a machine that has nothing but a @@ -24,7 +24,7 @@ machines: # is what pinning asks for (novox/hq ADR 0006). The store and the broker are ordinary third-party # images, and the machine pulls them from the internet like anything else. images: - - mesh-control:development + - mesh-controller:development place: all: [host, runtime] diff --git a/scenarios/fresh-mesh.yml b/scenarios/fresh-mesh.yml new file mode 100644 index 0000000..e5c0d85 --- /dev/null +++ b/scenarios/fresh-mesh.yml @@ -0,0 +1,106 @@ +# FOUR FRESH MACHINES AND THE INSTALLER. Nothing is handed to them. +# +# This is `whole-mesh-full`'s topology with `genesis-single`'s honesty. The four-machine bed loads +# thirty-four of the mesh's own images onto its machines from the workstation, because it does not +# build them — they are produced by hand beside the bed and copied in. That is a shape no real +# installation has, and it is the same class of fiction the lab already removed once: it used to +# raise a registry inside the scenario, and a bootstrap that only worked against it went green here +# and would have failed on any real machine. +# +# So this bed hands over NOTHING. There is no `images:` list. Every machine gets a container +# runtime and the host binary, which are prerequisites of the machine rather than parts of the +# mesh, and after that the mesh is on its own: +# +# - the foundation, the registry and the builder's own dependencies are PULLED from the internet, +# which is where a bare machine gets them; +# - the control plane is BUILT, by the builder the installer carries, from a repository and a +# commit it is told to use; +# - every module after that is BUILT by the mesh's own builder and published into the mesh's own +# registry, and a machine that runs one PULLS it from there. +# +# That last clause is the thing no bed has ever checked, and it is why this one has four machines +# rather than one. Genesis puts the builder, the registry and everything they make on ONE machine. +# A second machine running a mesh-built module has to fetch it from a registry that asks who it is, +# and nothing yet gives a joined node an account for it. The bed asks anyway, in its own test, so +# the gap is a named failure rather than an absence. +# +# hosting (public, routable) home (private, behind the access point) +# anchor 192.0.2.20 ── anchor home-server 10.99.1.10 home server +# foundation, registry, workstation 10.99.1.20 workstation +# builder, control plane laptop 10.99.1.30 workstation +# +# EGRESS IS NOT OPTIONAL HERE. With nothing loaded, a sealed machine stops at the installer's first +# pull. Every machine has a way out, and it is a SECOND path: each still reaches the rest of the +# scenario through its declared gateway, and the uplink carries only what leaves the scenario — +# the public images, and the forge the control plane is cloned from. +# +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap +# MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock +# MESH_LAB_CATALOG=.../mesh-catalog/modules +# MESH_LAB_SOURCE= MESH_LAB_SOURCE_REF= +scenario: fresh-mesh + +segments: + # The routable segment. anchor lives here; its public address is the broker endpoint every token + # carries and the overlay hub the home nodes dial. + hosting: + kind: public + cidr: [192.0.2.0/24] + + # The household segment behind an ordinary home router: masquerades v4 outbound, forwards + # inbound, expires idle mappings after two minutes. + home: + kind: private + cidr: [10.99.1.0/24] + gateway: + to: hosting + address: [192.0.2.50] + nat: [v4] + forwardable: true + mapping_ttl: 120s + +machines: + # The anchor. Raised by the installer into a mesh of one, and then asked to build. + # + # Sized for what it actually does here: the store, the broker, the registry, TWO control planes + # during the pivot, the builder, and a build worksphome-server holding a Node toolchain image and an npm + # cache. It is NOT sized for the whole anchor service set, because this bed does not run one — it + # proves the machinery that would produce it. + anchor: + at: { segment: hosting, address: [192.0.2.20] } + egress: true + inbound: allow + memory: 12GiB + cpus: 6 + disk: 60GiB + + # Three machines that JOIN. Host binary and a token, nothing else — no bootstrap, no foundation, + # no registry. They are deliberately small: what they are here to prove is that a joined machine + # can be given a module the mesh built, which is a question about credentials and not about load. + home-server: + at: { segment: home, address: [10.99.1.10] } + egress: true + inbound: allow + memory: 4GiB + cpus: 2 + disk: 25GiB + workstation: + at: { segment: home, address: [10.99.1.20] } + egress: true + inbound: allow + memory: 3GiB + cpus: 2 + disk: 20GiB + laptop: + at: { segment: home, address: [10.99.1.30] } + egress: true + inbound: allow + memory: 3GiB + cpus: 2 + disk: 20GiB + +# **No `images:` key, and that is the whole point of this file.** Anything a machine holds here, it +# pulled or the mesh built. See the header. + +plhome-server: + all: [host, runtime] diff --git a/scenarios/genesis-single.yml b/scenarios/genesis-single.yml index da12b61..898a281 100644 --- a/scenarios/genesis-single.yml +++ b/scenarios/genesis-single.yml @@ -2,10 +2,10 @@ # # The smallest thing that proves a mesh can be raised. One machine on a public segment with a way # out to the internet, the host placed, and nothing else — the installer carries the control -# plane's image and the substrate's own images are pulled over the uplink, exactly as they are on +# plane's image and the foundation's own images are pulled over the uplink, exactly as they are on # a bare machine. # -# The address matters: the substrate template names the broker at 192.0.2.10, and a token carries +# The address matters: the foundation template names the broker at 192.0.2.10, and a token carries # that address verbatim as the endpoint an enrolling node dials. With one machine, that machine # must BE it, or the mesh would hand out an endpoint nothing answers on. scenario: genesis-single @@ -23,7 +23,7 @@ machines: # bare machine. A scenario that needs no images can omit this; genesis cannot. egress: true inbound: allow - # Enough for the substrate (store, broker), the registry, and two control planes during the + # Enough for the foundation (store, broker), the registry, and two control planes during the # pivot. Smaller than the four-node bed's anchor, which also carries a whole service set. memory: 8GiB cpus: 4 @@ -33,8 +33,8 @@ machines: # # The runtime is not a mesh tier — it is a prerequisite of the machine, and the installer's first # step refuses to go on without one. Placing it here is the lab preparing a machine, not the lab -# describing an installation. Everything above tier 0 — the substrate, the registry, the control +# describing an installation. Everything above tier 0 — the foundation, the registry, the control # plane — is the installer's, and the lab places none of it. That is the whole point of this bed: -# if the lab placed the substrate, it would be describing installing all over again. +# if the lab placed the foundation, it would be describing installing all over again. place: all: [host, runtime] diff --git a/scenarios/grafana-node.yml b/scenarios/grafana-node.yml index e58814c..f3260f3 100644 --- a/scenarios/grafana-node.yml +++ b/scenarios/grafana-node.yml @@ -20,7 +20,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development - mesh-runtime-grafana:development place: diff --git a/scenarios/growing-mesh.yml b/scenarios/growing-mesh.yml index 0ab0f04..542b8fd 100644 --- a/scenarios/growing-mesh.yml +++ b/scenarios/growing-mesh.yml @@ -28,7 +28,7 @@ machines: inbound: allow images: - - mesh-control:development + - mesh-controller:development place: all: [host, runtime] diff --git a/scenarios/lavinmq-bed.yml b/scenarios/lavinmq-bed.yml index f20fcdc..a7f7745 100644 --- a/scenarios/lavinmq-bed.yml +++ b/scenarios/lavinmq-bed.yml @@ -1,28 +1,28 @@ -# Two machines: one is the substrate, the other carries a lavinmq PROVIDER and a consumer of it. +# Two machines: one is the foundation, the other carries a lavinmq PROVIDER and a consumer of it. # # lavinmq is the mesh's own control-plane broker (mesh-broker), and it is ALSO offered as a # user-facing capability: a module that needs a message queue gets its OWN broker — a scoped vhost and # user on a lavinmq PROVIDER — not an account on the control broker. That makes lavinmq the sharp -# two-node case, for the same reason two-node-db is: the substrate's broker publishes 5672 on the node +# two-node case, for the same reason two-node-db is: the foundation's broker publishes 5672 on the node # it runs on, and a lavinmq provider must publish 5672 too for its consumers to reach it — so the two # cannot share a machine. The moment a real amqp provider must own a node's 5672, it collides with the # control broker already there, and the chain is blocked single-node. # -# This is the split that unblocks it. `anchor` runs the substrate (store, broker, control) and NOTHING +# This is the split that unblocks it. `anchor` runs the foundation (store, broker, control) and NOTHING # else. `laptop` runs the whole chain: the lavinmq PROVIDER and the amqp-ping CONSUMER that requires # it. Provider and consumer are co-located on laptop, so the grant never crosses a node boundary; only -# enrolment crosses to anchor, over the underlay both machines share. And because the substrate broker +# enrolment crosses to anchor, over the underlay both machines share. And because the foundation broker # is on the OTHER node, the provider owns laptop's 5672 uncontested. # -# It needs a host binary and the substrate bundle: +# It needs a host binary and the foundation bundle: # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # # HELPER — stock the two runtimes into the local daemon before the run (some may already be there): # scripts/build-module-runtime.sh lavinmq /tmp/lavinmq.tar # scripts/build-module-runtime.sh amqp-ping /tmp/amqp-ping.tar # The service image cloudamqp/lavinmq:latest must be in the local daemon too — it is already stocked as -# the substrate's own broker image; each node pulls what it runs from the internet, over its own +# the foundation's own broker image; each node pulls what it runs from the internet, over its own # uplink. scenario: lavinmq-bed @@ -46,7 +46,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # The lavinmq provider's runtime (reused for its run-once bootstrap and its provisioner) and the # amqp-ping consumer's runtime, both built by scripts/build-module-runtime.sh into the local daemon # and loaded onto the machines, which hold them by their own image IDs. diff --git a/scenarios/local-model-bed.yml b/scenarios/local-model-bed.yml index b76b292..b1ef7f5 100644 --- a/scenarios/local-model-bed.yml +++ b/scenarios/local-model-bed.yml @@ -11,9 +11,9 @@ # openai.env carries OPENAI_BASE_URL=http://:/v1. The test asserts that URL and that a # request to it reaches the running model server. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # Both modules are pure declaration (a server container + a templated file) — no module runtime image -# is built; the bed only stocks the substrate and the ollama server image. +# is built; the bed only stocks the foundation and the ollama server image. scenario: local-model-bed segments: @@ -31,7 +31,7 @@ machines: disk: 40GiB images: - - mesh-control:development + - mesh-controller:development place: all: [host, runtime] diff --git a/scenarios/minio-node.yml b/scenarios/minio-node.yml index 6347a7c..7163cf1 100644 --- a/scenarios/minio-node.yml +++ b/scenarios/minio-node.yml @@ -21,7 +21,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # minio's runtime, built by scripts/build-module-runtime.sh minio (it carries mc), loaded onto # the machine. - mesh-runtime-minio:development diff --git a/scenarios/model-usage-bed.yml b/scenarios/model-usage-bed.yml index bd067b0..0e4790d 100644 --- a/scenarios/model-usage-bed.yml +++ b/scenarios/model-usage-bed.yml @@ -1,7 +1,7 @@ # The usage context store, proved end to end (novox/hq ADR 0054). A postgres PROVIDER and the -# model-usage CONSUMER ride one node; the substrate (store, broker, control) owns the other. As in -# two-node-db, the app-postgres provider and the mesh's own substrate store both want host port 5432, -# so they cannot share a machine — the substrate lives on `anchor` and NOTHING else, and `laptop` +# model-usage CONSUMER ride one node; the foundation (store, broker, control) owns the other. As in +# two-node-db, the app-postgres provider and the mesh's own foundation store both want host port 5432, +# so they cannot share a machine — the foundation lives on `anchor` and NOTHING else, and `laptop` # runs postgres plus model-usage. Provider and consumer are co-located on laptop, so only enrolment # crosses to anchor, over the underlay both machines already share. # @@ -9,7 +9,7 @@ # provisioned postgres store, at BOTH grains (licence and session, differing only in `consumer`), # LATEST-per-key, and IN THE CLEAR — an ordinary select returns the numeric value and its raw payload. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # Build BOTH runtime images into the local daemon first (the scenario stocks and serves them by # digest, which is where the host pulls them from): # scripts/build-module-runtime.sh postgres /tmp/postgres.tar @@ -22,7 +22,7 @@ segments: cidr: [192.0.2.0/24] machines: - # The substrate ONLY: store, broker, control — three containers. + # The foundation ONLY: store, broker, control — three containers. anchor: at: { segment: hosting, address: [192.0.2.10] } egress: true @@ -30,8 +30,8 @@ machines: memory: 4GiB cpus: 4 # The postgres PROVIDER (server + broker-bound runtime) and the model-usage CONSUMER (its run-once - # migrate and its long-lived event runtime). The 5432-vs-substrate conflict is gone because the - # substrate store is on the OTHER node. The runtime images plus postgres:17-alpine are pulled from + # migrate and its long-lived event runtime). The 5432-vs-foundation conflict is gone because the + # foundation store is on the OTHER node. The runtime images plus postgres:17-alpine are pulled from # the internet over the uplink; forty gigabytes holds them with room to spare. laptop: at: { segment: hosting, address: [192.0.2.20] } @@ -42,7 +42,7 @@ machines: disk: 40GiB images: - - mesh-control:development + - mesh-controller:development # The per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. Each carries # its module's code — postgres its provisioner, model-usage its consumer, tools and run-once migrate. - mesh-runtime-postgres:development diff --git a/scenarios/one-node-mesh.yml b/scenarios/one-node-mesh.yml new file mode 100644 index 0000000..6568191 --- /dev/null +++ b/scenarios/one-node-mesh.yml @@ -0,0 +1,56 @@ +# ONE MACHINE, AND EVERYTHING A MESH HAS TO BE. Nothing is handed to it. +# +# The common case, and the one worth getting right first: a person with a single machine runs the +# installer and ends up with a mesh that works. Not a mesh that *runs* — `17-raising-a-mesh` is +# careful about that difference, and so is this scenario. Genesis ends with a foundation, a registry, +# a built control plane and a builder, and a mesh in that state cannot produce anything and holds no +# record of what it has. Calling that "up" is how the catalogue came to be missing from a test for +# weeks without anything complaining. +# +# So the test driving this asks the harder question: can this machine, given nothing but a container +# runtime and the host binary, end up holding +# +# - a foundation and a registry it pulled from the internet, +# - a control plane it BUILT, and then rebuilt from its own repository through the module path, +# - a builder that takes work over the broker, +# - the shared base every module with code of its own stands on, +# - a store of its own — the foundation's is the control plane's own plumbing, not a provider, +# - a catalogue, so it can say what it has and what a change reaches, +# - and a module of its own, built, provisioned and running. +# +# **There is no `images:` key, and that is the whole point of this file.** The four-machine scenario +# next door loads thirty-four of the mesh's own images from the workstation because it does not +# build them — a shape no real installation has. Here nothing is loaded. What the machine holds it +# either pulled from the internet or made. +# +# EGRESS IS NOT OPTIONAL. With nothing loaded, a sealed machine stops at the installer's first pull. +# +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap +# MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock +# MESH_LAB_CATALOG=.../mesh-catalog/modules +# MESH_LAB_SOURCE= MESH_LAB_SOURCE_REF= +scenario: one-node-mesh + +segments: + hosting: + kind: public + cidr: [192.0.2.0/24] + +machines: + # The address matters: the foundation template names the broker at a fixed address, and a token + # carries that verbatim as the endpoint an enrolling node dials. With one machine, that machine + # must BE it, or the mesh hands out an endpoint nothing answers on. + # + # Sized for what it actually does: the store, the broker, the registry, two control planes during + # the pivot, the builder, a Node toolchain and an npm cache in the build workspace, and then every + # core module it builds and runs on top of that. + anchor: + at: { segment: hosting, address: [192.0.2.10] } + egress: true + inbound: allow + memory: 12GiB + cpus: 6 + disk: 60GiB + +place: + all: [host, runtime] diff --git a/scenarios/openai-bed.yml b/scenarios/openai-bed.yml index ffac75a..16023ff 100644 --- a/scenarios/openai-bed.yml +++ b/scenarios/openai-bed.yml @@ -11,7 +11,7 @@ # OPENAI_API_KEY (env file + the Codex auth.json). The test asserts the written key equals the one # the operator set — through the sealed delivery path, unchanged. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # Build the consumer runtime image into the local daemon first (raise() stocks it from there): # scripts/build-module-runtime.sh openai-consumer /tmp/openai-consumer.tar scenario: openai-bed @@ -30,7 +30,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # The static-key consumer runtime, built by scripts/build-module-runtime.sh into the local daemon and # loaded onto the machine, which holds it by its own image ID. - mesh-runtime-openai-consumer:development diff --git a/scenarios/plex-node.yml b/scenarios/plex-node.yml index dc72b8d..e2ffee2 100644 --- a/scenarios/plex-node.yml +++ b/scenarios/plex-node.yml @@ -1,7 +1,7 @@ # One machine that becomes a mesh and then assigns itself plex's tool runtime. # # The audit-node bed proved an assigned *consumer* (novox/hq ADR 0048). This proves an assigned -# module that *serves tools* (ADR 0052): the same first-node substrate, plus plex's tool runtime on +# module that *serves tools* (ADR 0052): the same first-node foundation, plus plex's tool runtime on # top. The node enrols itself, the mesh issues plex a broker account scoped to serve.plex.* and # assigns it, the host runs the runtime container, and a caller invokes plex.plex_reachable over the # mesh — proof the module runs its own code as its own process under its own scoped account. @@ -21,7 +21,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # Plex's tool runtime, built by scripts/build-module-runtime.sh plex into the local daemon and # loaded onto the machine, which holds it by its own image ID. - mesh-runtime-plex:development diff --git a/scenarios/postgres-node.yml b/scenarios/postgres-node.yml index 913fa3f..3429b3e 100644 --- a/scenarios/postgres-node.yml +++ b/scenarios/postgres-node.yml @@ -20,7 +20,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # postgres's runtime, built by scripts/build-module-runtime.sh postgres (it carries psql), loaded # onto the machine. - mesh-runtime-postgres:development diff --git a/scenarios/redis-node.yml b/scenarios/redis-node.yml index d075371..b8b4441 100644 --- a/scenarios/redis-node.yml +++ b/scenarios/redis-node.yml @@ -20,7 +20,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # Redis's tool+provisioner runtime, built by scripts/build-module-runtime.sh redis into the local # daemon and loaded onto the machine, which holds it by its own image ID. - mesh-runtime-redis:development diff --git a/scenarios/route-forwarding.yml b/scenarios/route-forwarding.yml index 75da3b8..af84f47 100644 --- a/scenarios/route-forwarding.yml +++ b/scenarios/route-forwarding.yml @@ -14,9 +14,9 @@ # publicly-trusted certificate and answering an HTTP-01 challenge at the name — is proven separately # by certificates.test.ts against a real ACME server (Pebble), driving the same proxy binary. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-route-proxy-image.sh builds mesh-route-proxy:development into the local daemon -# (from mesh-control/examples/route-proxy, via mesh-catalog/modules/route-proxy/Dockerfile). +# (from mesh-controller/examples/route-proxy, via mesh-catalog/modules/route-proxy/Dockerfile). # alpine:latest must be in the local daemon — hello-web's backend is a bare alpine that serves a # fixed page over a busybox nc loop. Both images are stocked and served by digest. scenario: route-forwarding @@ -35,8 +35,8 @@ machines: cpus: 2 images: - - mesh-control:development - # The route-proxy's image, built from the canonical Go proxy in mesh-control by + - mesh-controller:development + # The route-proxy's image, built from the canonical Go proxy in mesh-controller by # scripts/build-route-proxy-image.sh, and hello-web's backend, a bare alpine nc loop. - mesh-route-proxy:development diff --git a/scenarios/schedule-tick.yml b/scenarios/schedule-tick.yml index e345da4..627e7d3 100644 --- a/scenarios/schedule-tick.yml +++ b/scenarios/schedule-tick.yml @@ -14,7 +14,7 @@ # - the container fires when the cron is due (top of the next minute); # - it fires AGAIN on the following minute — recurrence, not a one-shot. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_MODULES=.../mesh-control/examples/modules +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_MODULES=.../mesh-controller/examples/modules # alpine:latest must be in the local daemon; the machine pulls it from the internet over its # uplink, and the scheduled container declares it exactly as the catalogue writes it. # There is no runtime image: schedtest carries no code of its own — the scheduled container is a @@ -35,7 +35,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development place: # Only the host — schedtest has no mesh-runtime to place. The tick image is pulled from the diff --git a/scenarios/sonarr-node.yml b/scenarios/sonarr-node.yml index 8313f6e..70dcbeb 100644 --- a/scenarios/sonarr-node.yml +++ b/scenarios/sonarr-node.yml @@ -20,7 +20,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development - mesh-runtime-sonarr:development place: diff --git a/scenarios/tools-confluence.yml b/scenarios/tools-confluence.yml index 7fcc150..83b5d43 100644 --- a/scenarios/tools-confluence.yml +++ b/scenarios/tools-confluence.yml @@ -9,7 +9,7 @@ # built lazily and never throws at registration, so the runtime logs `[mesh-tools] serving 3 tool(s)` # and binds its serve queues regardless; a tool would only fail if it were actually invoked. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-module-runtime.sh confluence builds mesh-runtime-confluence:development into the # local daemon, which the machine pulls from the internet over its uplink. confluence # needs no service image — it is tools-only. @@ -29,7 +29,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # confluence's runtime, built by scripts/build-module-runtime.sh confluence into the local daemon # and loaded onto the machine, which holds it by its own image ID. There is no # service image: confluence is tools-only and outbound-only. diff --git a/scenarios/tools-gitlab.yml b/scenarios/tools-gitlab.yml index e797999..ad5d92c 100644 --- a/scenarios/tools-gitlab.yml +++ b/scenarios/tools-gitlab.yml @@ -10,7 +10,7 @@ # throws at registration, so the runtime logs `[mesh-tools] serving 23 tool(s)` and binds its serve # queues regardless; a tool would only fail if it were actually invoked without real creds. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # scripts/build-module-runtime.sh gitlab builds mesh-runtime-gitlab:development into the local # daemon, which the machine pulls from the internet over its uplink. gitlab needs no # service image — it is tools-only. @@ -30,7 +30,7 @@ machines: cpus: 2 images: - - mesh-control:development + - mesh-controller:development # gitlab's runtime, built by scripts/build-module-runtime.sh gitlab into the local daemon and # loaded onto the machine, which holds it by its own image ID. There is no # service image: gitlab is tools-only and outbound-only. diff --git a/scenarios/two-node-db.yml b/scenarios/two-node-db.yml index 84b1313..dc923b6 100644 --- a/scenarios/two-node-db.yml +++ b/scenarios/two-node-db.yml @@ -1,13 +1,13 @@ # The DB-consumer chain a single node cannot host, proved across two machines. # -# The app-postgres provider and the mesh's own substrate store both want host port 5432, so they -# cannot share a machine — the collision that blocked this chain single-node. Here the substrate +# The app-postgres provider and the mesh's own foundation store both want host port 5432, so they +# cannot share a machine — the collision that blocked this chain single-node. Here the foundation # (store, broker, control) lives on `anchor` and NOTHING else; `laptop` runs the whole chain — # postgres and redis PROVIDERS plus the baserow and letta CONSUMERS that require them. Both # machines sit on one shared segment and enrol into the one mesh; only enrolment crosses to anchor, # over the underlay both machines already share. Provider and consumers are co-located on laptop, so -# no cross-node module comms and no overlay are needed — and the 5432-vs-substrate conflict is gone -# because the substrate store is on the OTHER node. +# no cross-node module comms and no overlay are needed — and the 5432-vs-foundation conflict is gone +# because the foundation store is on the OTHER node. scenario: two-node-db segments: @@ -16,7 +16,7 @@ segments: cidr: [192.0.2.0/24] machines: - # The substrate ONLY: store, broker, control — three containers. Four gigabytes is plenty for a + # The foundation ONLY: store, broker, control — three containers. Four gigabytes is plenty for a # node that hosts no modules; the thrash the two-nodes bed warns of comes from stacking eleven # containers on a node, which this one never does. anchor: @@ -43,7 +43,7 @@ machines: disk: 60GiB images: - - mesh-control:development + - mesh-controller:development # The per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. Each carries # its module's provisioner, so no separate mesh-provision-* image is listed — the runtime is the # provisioner (ADR 0048). diff --git a/scenarios/two-nodes.yml b/scenarios/two-nodes.yml index 563ea5f..c046332 100644 --- a/scenarios/two-nodes.yml +++ b/scenarios/two-nodes.yml @@ -17,7 +17,7 @@ machines: at: { segment: hosting, address: [192.0.2.10] } egress: true inbound: allow - # The whole substrate, the registry, the builder, an adopted workload and the modules under + # The whole foundation, the registry, the builder, an adopted workload and the modules under # test all land here — eleven containers before the forge arrives. At the 1GiB default this # machine thrashes, and it presents as "the mesh hangs": every exec slows from 15s to 105s # and the forge test fails on a status poll that is merely queued behind page-outs. @@ -30,7 +30,7 @@ machines: memory: 2GiB images: - - mesh-control:development + - mesh-controller:development # And the builder, because it is a module the mesh assigns rather than a program somebody # starts by hand — which is the only way its credential can be one the mesh delivered. - mesh-builder:development diff --git a/scenarios/whole-mesh-ace.yml b/scenarios/whole-mesh-ace.yml index 6be3329..3ecb8f1 100644 --- a/scenarios/whole-mesh-ace.yml +++ b/scenarios/whole-mesh-ace.yml @@ -1,15 +1,15 @@ # The whole `ace` server's converted service set, installed together on ONE node behind the mesh -# substrate — the media/home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of +# foundation — the media/home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of # scenarios/whole-mesh-novox.yml; same topology, a different (larger, media-heavy) module set. # -# Substrate (store, broker, control) rides `anchor` and NOTHING else; ALL of ace's services ride the +# Foundation (store, broker, control) rides `anchor` and NOTHING else; ALL of ace's services ride the # `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres/redis # providers co-located with them. The media stack (sonarr/radarr/lidarr/plex/bazarr/nzbget/ # qbittorrent/bookshelf) shares the operator-owned library directories under /services/media (ADR # 0051 `accesses`); the test pre-creates them on the node, as the operator would, before the push — # the mesh confirms the paths exist and mounts them, but creates and chowns none of it. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # The runtimes are built by scripts/build-module-runtime.sh (one per module) and must be in the local # daemon, because nothing serves them and nothing can. Every media/app image is pulled from the # internet by the node itself, over its uplink, by the digest its module.json already pins. The test @@ -30,9 +30,9 @@ machines: memory: 4GiB cpus: 4 disk: 20GiB - # The substrate only, so the control plane's image only. The runtimes belong on the node that + # The foundation only, so the control plane's image only. The runtimes belong on the node that # runs the modules, and a 20GiB disk has no room for them anyway. - images: [mesh-control:development] + images: [mesh-controller:development] # The whole ace service set — 24 modules, ~50 containers, several heavy (Plex, Home Assistant, # Letta ~1.8GiB, Baserow ~1.5GiB, the UniFi controller's JVM, mssql ~2GiB). Sized past novox. ace: @@ -42,7 +42,7 @@ machines: memory: 18GiB cpus: 8 disk: 120GiB - # Every runtime. Not mesh-control: the control plane runs on the anchor. + # Every runtime. Not mesh-controller: the control plane runs on the anchor. images: - mesh-runtime-postgres:development - mesh-runtime-redis:development @@ -70,7 +70,7 @@ machines: - mesh-runtime-unifi:development images: - - mesh-control:development + - mesh-controller:development # The per-module runtimes (built by scripts/build-module-runtime.sh). - mesh-runtime-postgres:development - mesh-runtime-redis:development diff --git a/scenarios/whole-mesh-full.yml b/scenarios/whole-mesh-full.yml index 1c9df35..5ea45bd 100644 --- a/scenarios/whole-mesh-full.yml +++ b/scenarios/whole-mesh-full.yml @@ -1,19 +1,19 @@ # The FULL mesh in its REAL production shape: two segments, one access point, one overlay. # # This is the first multi-segment whole-mesh bed. The earlier flat whole-mesh-full sat every node -# on one public segment with a SEPARATE `anchor` carrying the substrate. Production is not flat, and +# on one public segment with a SEPARATE `anchor` carrying the foundation. Production is not flat, and # there is no separate anchor: `novox` IS the anchor. It sits on the routable `hosting` segment, -# runs the substrate (store, broker, control) AND its own service set AND is the overlay hub and the +# runs the foundation (store, broker, control) AND its own service set AND is the overlay hub and the # public ingress. `ace`, `shanks` and `g14` sit on the household `home` segment BEHIND a NAT gateway # — the access point — reachable from the outside only through what they dial out to. # # hosting (public, routable) home (private, behind the access point) # novox 192.0.2.20 ── anchor ace 10.99.1.10 home server, media/IoT set -# substrate + novox set shanks 10.99.1.20 workstation (light) +# foundation + novox set shanks 10.99.1.20 workstation (light) # overlay hub, ingress g14 10.99.1.30 workstation (light) # # The `home` gateway masquerades v4 outbound and forwards inbound (an ordinary household router). -# Home nodes reach novox's public 192.0.2.20 by dialling OUT through it: the substrate broker (5671), +# Home nodes reach novox's public 192.0.2.20 by dialling OUT through it: the foundation broker (5671), # the mesh's own artifact store, and — the thing this bed exists to prove — the WireGuard overlay hub # (51820/udp). The hub keepalive holds the NAT hole open so the tunnel, once formed, stays up. novox # cannot initiate to a home node at all; every home↔novox path is either the overlay or a forwarded @@ -35,20 +35,20 @@ # the gateway's masquerade? The driving test verifies the WireGuard handshake and cross-segment # reachability over the overlay explicitly, and reports form-vs-break as its headline. # -# Substrate-on-novox collides on two host ports the separate-anchor beds never hit: the substrate -# store binds 127.0.0.1:5432 and novox's postgres provider publishes 5432; the substrate broker binds +# Foundation-on-novox collides on two host ports the separate-anchor beds never hit: the foundation +# store binds 127.0.0.1:5432 and novox's postgres provider publishes 5432; the foundation broker binds # 5671 + 127.0.0.1:5672 and novox's lavinmq provider publishes 5672. The driving test REMAPS those two -# provider host publishes off the substrate's ports (consumers reach the providers over the mesh +# provider host publishes off the foundation's ports (consumers reach the providers over the mesh # network on the container port, so the host side is free to move). Reported as a topology finding. # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap MESH_LAB_CATALOG=.../mesh-catalog/modules # # GENESIS AND JOINING ARE TWO DIFFERENT ACTS, and this bed distinguishes them. novox is brought # into existence by `mesh-bootstrap` — the same program a bare machine runs — and is afterwards a # working mesh of one, with a registry and a control plane that is an ordinary module pinned to an # image that registry serves. ace, shanks and g14 then JOIN it: host binary, token, enrol, run. No -# bootstrap, no substrate, no registry. novox is never enrolled twice, because the installer +# bootstrap, no foundation, no registry. novox is never enrolled twice, because the installer # already did it. # # The images: are the UNION of the novox set (feat/novox-conversions @ 431310f: the slug + roundcube @@ -76,8 +76,8 @@ segments: mapping_ttl: 120s machines: - # The anchor: substrate (store, broker, control) + the whole novox service set + overlay hub + - # public ingress. Bigger than the flat bed's novox, because it now carries the substrate too. + # The anchor: foundation (store, broker, control) + the whole novox service set + overlay hub + + # public ingress. Bigger than the flat bed's novox, because it now carries the foundation too. novox: at: { segment: hosting, address: [192.0.2.20] } egress: true @@ -90,7 +90,7 @@ machines: # because they carry no runtime image of their own — what they run is third-party or is the # node itself. # - # **mesh-control is NOT here, and its absence is the point** (novox/hq ADR 0067). The anchor is + # **mesh-controller is NOT here, and its absence is the point** (novox/hq ADR 0067). The anchor is # brought into existence by the installer, and the installer carries the control plane's image # inside itself — that is the whole reason a machine that can reach no registry can still raise # a mesh. Handing it over from the workstation as well would mean the bed never found out diff --git a/scenarios/whole-mesh-novox.yml b/scenarios/whole-mesh-novox.yml index 45ef3d5..8662ed1 100644 --- a/scenarios/whole-mesh-novox.yml +++ b/scenarios/whole-mesh-novox.yml @@ -1,10 +1,10 @@ # The whole `novox` server's converted service set, installed together on ONE node behind the mesh -# substrate — the whole-catalogue install the rebuild has never actually run. First stage of a +# foundation — the whole-catalogue install the rebuild has never actually run. First stage of a # whole-mesh rehearsal (novox/hq). # -# Topology, proven by test/integration/assigned-two-node-db.test.ts: the substrate (store, broker, +# Topology, proven by test/integration/assigned-two-node-db.test.ts: the foundation (store, broker, # control) rides `anchor` and NOTHING else; ALL of novox's services ride the `novox` node — its own -# postgres provider owns 5432 there, so it cannot co-locate with the substrate store on 5432. Both +# postgres provider owns 5432 there, so it cannot co-locate with the foundation store on 5432. Both # machines sit on one public segment and enrol into the one mesh; an overlay is placed so a # consumer's binding `at` resolves to novox's private address and every consumer reaches the # providers co-located with it. @@ -15,7 +15,7 @@ # apps portainer verdaccio registry route-proxy mailu # node-level firewall fail2ban # -# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock +# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock # The runtimes are built by scripts/build-module-runtime.sh (one per module that has code) and the # route-proxy image by scripts/build-route-proxy-image.sh; those must be in the local daemon, # because nothing serves them and nothing can. Every third-party image is pulled from the internet @@ -29,7 +29,7 @@ segments: cidr: [192.0.2.0/24] machines: - # The substrate ONLY: store, broker, control. Nothing else lands here. + # The foundation ONLY: store, broker, control. Nothing else lands here. anchor: at: { segment: hosting, address: [192.0.2.10] } egress: true @@ -37,9 +37,9 @@ machines: memory: 4GiB cpus: 4 disk: 20GiB - # The substrate only, so the control plane's image only. Handing this machine the whole set of + # The foundation only, so the control plane's image only. Handing this machine the whole set of # runtimes would fill a 20GiB disk with images nothing on it will ever start. - images: [mesh-control:development] + images: [mesh-controller:development] # The whole novox service set — ~38 containers (five providers with runtimes, six consumers with # runtimes, portainer/verdaccio/registry/route-proxy, the nine-container Mailu stack and its # runtime) plus two node-level modules. mssql alone wants ~2GiB; Mailu, Nextcloud and Keycloak are @@ -55,7 +55,7 @@ machines: # layers and the runtimes. A hundred gigabytes holds the whole set without exhausting the disk # mid-apply. disk: 100GiB - # Every runtime, and the proxy. Not mesh-control: the control plane runs on the anchor. + # Every runtime, and the proxy. Not mesh-controller: the control plane runs on the anchor. images: - mesh-runtime-postgres:development - mesh-runtime-redis:development @@ -73,7 +73,7 @@ machines: - mesh-route-proxy:development images: - - mesh-control:development + - mesh-controller:development # The per-module runtimes (built by scripts/build-module-runtime.sh). registry, route-proxy, # invoicing, firewall and fail2ban carry no mesh-runtime image; route-proxy ships its own. - mesh-runtime-postgres:development diff --git a/scripts/build-route-proxy-image.sh b/scripts/build-route-proxy-image.sh index ef5985e..f660349 100755 --- a/scripts/build-route-proxy-image.sh +++ b/scripts/build-route-proxy-image.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # Build the route-proxy module's runtime image: the reference reverse proxy (novox/hq -# 08-connectivity §3), compiled from its canonical Go source in mesh-control into a container the +# 08-connectivity §3), compiled from its canonical Go source in mesh-controller into a container the # lab can stock and serve by digest. # # Unlike the TypeScript modules (built by build-module-runtime.sh into a node tool runtime), the # proxy is a Go program. The module ships only the packaging — a Dockerfile in mesh-catalog whose -# build context is the mesh-control repository root — and this script runs that build into the local +# build context is the mesh-controller repository root — and this script runs that build into the local # docker daemon, which a scenario's registry then stocks and serves by digest. # # build-route-proxy-image.sh @@ -13,7 +13,7 @@ set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)"; ROOT="$(cd "$HERE/.." && pwd)" -MESH_CONTROL="${MESH_CONTROL:-$ROOT/mesh-control}" +MESH_CONTROL="${MESH_CONTROL:-$ROOT/mesh-controller}" MESH_CATALOG="${MESH_CATALOG:-$ROOT/mesh-catalog}" TAG="${ROUTE_PROXY_TAG:-mesh-route-proxy:development}" DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile" @@ -22,7 +22,7 @@ DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile" [ -f "$MESH_CONTROL/examples/route-proxy/main.go" ] || { echo "no proxy source at $MESH_CONTROL/examples/route-proxy" >&2; exit 1; } -# Context is the mesh-control repository root: the proxy compiles against that module's go.mod and +# Context is the mesh-controller repository root: the proxy compiles against that module's go.mod and # its examples/route-proxy package. docker build -f "$DOCKERFILE" -t "$TAG" "$MESH_CONTROL" echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)" diff --git a/src/declaration/types.ts b/src/declaration/types.ts index ae71d9f..96c5a7b 100644 --- a/src/declaration/types.ts +++ b/src/declaration/types.ts @@ -103,7 +103,7 @@ export interface Machine { * and a handful of containers. * * Declared, because it is a fact about the machine the scenario describes — the node that runs - * the whole substrate is bigger than the laptop that joins it, and a test that starves its + * the whole foundation is bigger than the laptop that joins it, and a test that starves its * anchor at the default answers questions about memory pressure, not about the mesh. The forge * test failed three times as "status hangs" before anyone counted the containers in 1GiB * (novox/hq 04-ISSUES/024 is the same lesson about a different resource). @@ -158,7 +158,7 @@ export interface Scenario { * **The mesh's own images** — the ones that exist in no registry and are put onto a machine by * whoever built them. * - * mesh-control, mesh-builder, mesh-route-proxy, the per-module runtimes and the provisioners are + * mesh-controller, mesh-builder, mesh-route-proxy, the per-module runtimes and the provisioners are * built from source and published nowhere. A machine gets them the way an operator's machine * does: they are built on the workstation, loaded onto the machine, and named by the digest of * their own image configuration. Written as tags, because a tag is what `docker save` can diff --git a/src/lifecycle/egress.ts b/src/lifecycle/egress.ts index e2b4995..256ab41 100644 --- a/src/lifecycle/egress.ts +++ b/src/lifecycle/egress.ts @@ -7,7 +7,7 @@ * from the internet, and that is now the thing worth proving before a raise says it is finished. * * The failure it exists to stop is the same one, in the same shape: `raise` returns, the caller - * applies a substrate, the first pull fails, no node enrols, and the instance is left a bare + * applies a foundation, the first pull fails, no node enrols, and the instance is left a bare * shell — with the cause several steps back and looking like a mesh fault rather than a lab one. * * Two things are checked, in this order, because they fail differently and the difference is the @@ -89,9 +89,40 @@ export async function confirmEgress( // the retry is tightened so a silent server costs seconds rather than the step. A broken uplink // still fails the check above, before any of this. await resilientResolver(name); + + // **And then prove it is STEADY, because one success proved nothing.** + // + // The check above is satisfied by a single answer, and that is how a machine resolving one + // query in three passed it and then killed two installs twenty minutes later. What a run needs + // is not "a name resolved once" but "names resolve reliably", and those differ by exactly the + // failure that has cost the most time here. Consecutive, because alternating success and + // timeout is the observed shape — a total count would pass on the same machine. + const steady = await steadilyResolves(name, 5); + if (steady < 5) { + throw new EgressError( + `${machine} resolves ${UPSTREAM} only ${steady} time(s) in five consecutive tries.\n` + + ` It has egress and an unreliable resolver, which does not fail here — it fails later, ` + + `inside a pull or a clone, as "could not resolve host" with the cause long out of view.\n` + + ` The uplink's own resolver is the usual culprit and is deliberately last in the list; ` + + `a machine still failing this has something wrong upstream of the lab.`, + ); + } + log(` ${machine} resolves steadily (5/5)`); } } +/** How many of `tries` consecutive lookups answered. Stops at the first failure. */ +async function steadilyResolves(name: string, tries: number): Promise { + for (let i = 0; i < tries; i++) { + const said = await incusOk( + ["exec", name, "--", "sh", "-c", + `timeout 8 getent hosts ${UPSTREAM} >/dev/null && echo yes`], 20_000, + ); + if (said?.trim() !== "yes") return i; + } + return tries; +} + async function resolves(name: string, waitSeconds: number): Promise { const deadline = Date.now() + waitSeconds * 1_000; while (Date.now() < deadline) { @@ -140,11 +171,22 @@ async function reaches(name: string, waitSeconds: number): Promise { await incus([ @@ -152,7 +194,8 @@ async function resilientResolver(name: string): Promise { `link=$(ip -4 route show default | awk '{print $5}' | head -n1); ` + `via=$(ip -4 route show default | awk '{print $3}' | head -n1); ` + `if [ -n "$link" ] && command -v resolvectl >/dev/null 2>&1; then ` + - `resolvectl dns "$link" $via 1.1.1.1 8.8.8.8 >/dev/null 2>&1 || true; fi; true`, + `resolvectl dns "$link" 1.1.1.1 8.8.8.8 9.9.9.9 $via >/dev/null 2>&1 || true; ` + + `resolvectl flush-caches >/dev/null 2>&1 || true; fi; true`, ], 30_000); } diff --git a/src/lifecycle/place.ts b/src/lifecycle/place.ts index 8f2a785..1002a9a 100644 --- a/src/lifecycle/place.ts +++ b/src/lifecycle/place.ts @@ -5,7 +5,7 @@ * the thing it exists to test did not exist (novox/hq 03-DESIGN/00-as-is/11-the-lab.md). Tier * 0 now does, so this is the seam where the lab acquires a consumer. * - * Only `host` is placeable. Everything else in the placement vocabulary — the substrate, a + * Only `host` is placeable. Everything else in the placement vocabulary — the foundation, a * control plane, a forge — is still refused by name rather than ignored, because a scenario * that declares something and raises without it is the fault this lab was built to catch * (novox/hq 04-ISSUES/003). @@ -515,7 +515,7 @@ export async function loadHeldImages( throw new Error( `${requested} is not on this workstation, so there is nothing to hand the machines.\n` + ` It is one of the mesh's own images and exists in no registry — nothing can pull it.\n` + - ` Build it first (mesh-control's \`make image …\`, or scripts/build-module-runtime.sh).`, + ` Build it first (mesh-controller's \`make image …\`, or scripts/build-module-runtime.sh).`, ); } } @@ -626,3 +626,26 @@ export async function loadHeldImages( return held; } + +/** + * Install the host's systemd packaging, so the installer supervises it as a service rather than a + * background process — the path a real machine takes, and the only one that survives a reboot. The + * lab places the binary at HOST_PATH; the shipped launcher runs $MESH_HOST_BIN (default + * /usr/bin/nox-mesh-host), so a symlink points that at the binary rather than editing the packaged + * unit. The installer's --host-service then starts AND enables it (novox/hq ADR 0005). + */ +export async function installHostService( + instanceName: string, + machine: string, + packagingDir: string, + logMessage: (message: string) => void = () => {}, +): Promise { + const launcher = join(packagingDir, "nox-mesh-host-launch"); + const unit = join(packagingDir, "nox-mesh-host.service"); + await incus(["exec", instanceName, "--", "mkdir", "-p", "/usr/lib/nox-mesh-host"], 60_000); + await incus(["file", "push", launcher, `${instanceName}/usr/lib/nox-mesh-host/launch`, "--mode", "0755"], 120_000); + await incus(["file", "push", unit, `${instanceName}/etc/systemd/system/nox-mesh-host.service`, "--mode", "0644"], 120_000); + await incus(["exec", instanceName, "--", "ln", "-sf", HOST_PATH, "/usr/bin/nox-mesh-host"], 60_000); + await incus(["exec", instanceName, "--", "systemctl", "daemon-reload"], 60_000); + logMessage(`installed nox-mesh-host.service on ${machine}`); +} diff --git a/src/lifecycle/raise.ts b/src/lifecycle/raise.ts index 0c1aa9c..908237f 100644 --- a/src/lifecycle/raise.ts +++ b/src/lifecycle/raise.ts @@ -307,9 +307,18 @@ export async function raise( enter("waiting for machines to become usable"); await waitUntilAllUsable(created, readyTimeout, log); + // **Before the uplink lease is asked for, make sure each machine asks as itself.** + enter("giving each machine its own identity"); + await distinguishMachines(created, log); + enter("applying declared addresses"); await applyAddresses(scenario, instanceId, byMachine, log); + // The positive control for the step above: if two machines share an uplink address, say so + // HERE, where it is one obvious sentence, rather than letting it surface an hour later as + // intermittent name resolution on some machines and not others. + await noTwoMachinesShareAnAddress(scenario, byMachine, log); + // Transit first: a gateway's default route points at it, so it has to exist. enter("wiring the public networks together"); const transit = await raiseTransit(scenario, instanceId, log); @@ -329,7 +338,7 @@ export async function raise( // **Only now is "this machine can reach the outside" a true statement.** The route, the // gateway and the machine's own filtering are all in place, so this is the path a pull takes. // A raise that returned without checking would hand the next step a fact it depends on and - // has no way to test — which is how a substrate apply used to die on its first pull. + // has no way to test — which is how a foundation apply used to die on its first pull. enter("confirming egress reaches the internet"); await confirmEgress(scenario, byMachine, log); @@ -356,3 +365,85 @@ export async function raise( throw new RaiseError(instanceId, step, cause); } } + +/** + * Give every machine a machine-id of its own, before any of them asks for an uplink lease. + * + * **Every machine in a bed is a clone of one base image, so they all boot with the SAME + * `/etc/machine-id`.** systemd's DHCP client derives its client identifier from that file, and the + * uplink's dnsmasq keys leases on the client identifier rather than on the MAC — so four machines + * with four distinct MACs were all handed *the same address*, and the host kept one ARP entry for + * it. Whichever machine last answered an ARP request got everybody's replies. + * + * This is the fault behind every "the lab's DNS is flaky" run. It does not present as an address + * conflict; it presents as name resolution that works two times in three, as pulls that succeed on + * a retry, and as one machine out of four being fine — because that machine happened to be holding + * the address. It survived an earlier diagnosis that blamed resolver ordering, because reordering + * resolvers on a machine that has just won the ARP race does appear to fix it. + * + * **Ordering is the whole of it, and the first version got it wrong in the other direction.** That + * version also restarted networkd and waited for a new lease, which is what you would do to repair + * a machine already holding a shared address. Here there is nothing to repair yet: the uplink is + * still down at this point and `applyAddresses` is what asks for the lease. So this writes the + * identity and stops, and the request that follows is made as somebody. + * + * Machines without `systemd-machine-id-setup` are left alone; the step is advisory. + */ +async function distinguishMachines(names: string[], log: (m: string) => void): Promise { + for (const name of names) { + const said = await incusOk([ + "exec", name, "--", "sh", "-c", + // Regenerated rather than written: `systemd-machine-id-setup` owns the format, and a + // hand-made value that is not 32 hex characters is rejected by systemd at next boot. + `command -v systemd-machine-id-setup >/dev/null 2>&1 || { echo unchanged; exit 0; }; ` + + `rm -f /etc/machine-id && systemd-machine-id-setup >/dev/null 2>&1; ` + + `cat /etc/machine-id`, + ], 60_000); + log(` ${name} is ${said?.trim().slice(0, 12) || "unchanged"}`); + } +} + +/** + * Refuse to go on if two machines took the same uplink address. + * + * A check rather than a comment, because the failure it guards is invisible where it happens and + * unrecognisable where it surfaces. Every machine that declares egress is asked what address it + * holds; two the same is a stop, named as what it is. + */ +async function noTwoMachinesShareAnAddress( + scenario: Scenario, + byMachine: Map, + log: (m: string) => void, +): Promise { + const held = new Map(); + for (const [machine, spec] of Object.entries(scenario.machines)) { + if (!spec.egress || spec.at === "detached") continue; + const name = byMachine.get(machine); + if (!name) continue; + const said = (await incusOk([ + "exec", name, "--", "sh", "-c", + // The `src` of the default route, NOT its last field — the first version of this took + // `$NF` and compared four machines' route METRIC, which is identical by construction and + // made the guard fire on every bed. A check that cannot be wrong is worth less than one + // that is read carefully once. + `ip -4 -o route show default 2>/dev/null | ` + + `awk '{for (i = 1; i <= NF; i++) if ($i == "src") { print $(i + 1); exit }}' | head -n1`, + ], 30_000))?.trim(); + if (!said) continue; + held.set(said, [...(held.get(said) ?? []), machine]); + } + const shared = [...held.entries()].filter(([, who]) => who.length > 1); + if (shared.length === 0) { + if (held.size > 0) log(` every machine with egress took an address of its own`); + return; + } + throw new Error( + `two machines took the SAME uplink address: ` + + shared.map(([a, who]) => `${a} held by ${who.join(" and ")}`).join("; ") + `.\n` + + ` They are clones of one image, so they present one DHCP client identity unless each is ` + + `given its own machine-id before the lease is asked for.\n` + + ` This does not fail as an address conflict. It fails later, as name resolution that works ` + + `about two times in three and as pulls that succeed on a retry, because the host holds one ` + + `ARP entry and whichever machine answered last receives the replies.`, + ); +} diff --git a/src/lifecycle/supported.ts b/src/lifecycle/supported.ts index 55b07d8..545d922 100644 --- a/src/lifecycle/supported.ts +++ b/src/lifecycle/supported.ts @@ -36,7 +36,7 @@ export function assertSupported(scenario: Scenario): void { // `host`, `runtime` and `image:` work. Everything else in the vocabulary is named // individually rather than refused as a whole, so a scenario that places a host and a - // substrate is told exactly which half the lab cannot do. + // foundation is told exactly which half the lab cannot do. const unplaceable = new Set(); for (const { artifacts } of planPlacements(scenario)) { for (const artifact of artifacts) { diff --git a/src/pinning.ts b/src/pinning.ts index 3197cfd..2f63f1a 100644 --- a/src/pinning.ts +++ b/src/pinning.ts @@ -3,7 +3,7 @@ * * **A digest is not knowable until something is built** (novox/hq 04-ISSUES/025). A manifest in a * repository can pin a third-party image, because somebody can ask a registry what a tag points - * at. It cannot pin an image the mesh builds itself: mesh-control, mesh-builder, mesh-route-proxy, + * at. It cannot pin an image the mesh builds itself: mesh-controller, mesh-builder, mesh-route-proxy, * the per-module runtimes and the provisioners exist in no registry, so there is no manifest * digest to write down. The catalogue ships sixty-four zeros for them, which parses, resolves, * composes — and stops on the machine. @@ -54,7 +54,7 @@ export function repositoryOf(reference: string): string { * * **Derived from the shape the build produces, not from a list of names.** `make image * builder-image provisioner-image objectstore-image redis-provisioner-image proxy-image` in - * mesh-control and `scripts/build-module-runtime.sh` here both tag their output `mesh-` + * mesh-controller and `scripts/build-module-runtime.sh` here both tag their output `mesh-` * with no registry host and no upstream organisation — that is what "built here, published * nowhere" looks like, and a hardcoded list would go stale the first time a module is added. * diff --git a/src/rebuild.ts b/src/rebuild.ts index 13e5baf..ea160f2 100644 --- a/src/rebuild.ts +++ b/src/rebuild.ts @@ -45,7 +45,7 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] { env: { CGO_ENABLED: "0" }, }); } - const control = where["mesh-control"]; + const control = where["mesh-controller"]; if (control) { // **Every image the lab runs, not only the control plane's.** // @@ -84,7 +84,7 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] { // // It is built here at all because the bed now bootstraps THROUGH it (novox/hq ADR 0067): the // anchor is brought into existence by running the same program a bare machine runs, rather than - // by the bed applying a substrate bundle by hand and calling that an install. An installer that + // by the bed applying a foundation bundle by hand and calling that an install. An installer that // was stale would be a bed proving something about last week's procedure. const installer = env["MESH_LAB_BOOTSTRAP_BINARY"]; if (installer && where["mesh-host"]) { @@ -104,7 +104,7 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] { * thing it was going to run and now carries the thing that makes it, so a raised mesh holds a * control plane it built from a repository and a commit rather than one it was handed. * - * `mesh-builder:development` is what mesh-control's `make builder-image` tags — one tag, said in + * `mesh-builder:development` is what mesh-controller's `make builder-image` tags — one tag, said in * one place. Overridable because a release installer carries a release image, and nothing about * that is the lab's business. */ diff --git a/src/repos.ts b/src/repos.ts index dadabc9..c0330e0 100644 --- a/src/repos.ts +++ b/src/repos.ts @@ -23,6 +23,6 @@ export function repositories(env: NodeJS.ProcessEnv = process.env): Repositories const host = env["MESH_LAB_HOST_BINARY"]; if (host) found["mesh-host"] = dirname(host); const modules = env["MESH_LAB_MODULES"]; - if (modules) found["mesh-control"] = dirname(dirname(modules)); + if (modules) found["mesh-controller"] = dirname(dirname(modules)); return found; } diff --git a/test/integration/anthropic-bed.test.ts b/test/integration/anthropic-bed.test.ts index 85a4c9a..c9d3409 100644 --- a/test/integration/anthropic-bed.test.ts +++ b/test/integration/anthropic-bed.test.ts @@ -10,7 +10,7 @@ * **What changed from the earlier cut, and why this is simpler.** The refresh token no longer rides a * bespoke at-rest envelope the module opens with a node private key the mesh must somehow place — a * module is never given a node's private key, so that path could not exist. It rides the ORDINARY - * sealed-delivery path instead: mesh-control (via the manager module at adoption) seals it to the + * sealed-delivery path instead: mesh-controller (via the manager module at adoption) seals it to the * manager node's PUBLIC key, and the HOST unseals it with that node's real private key and mounts the * cleartext at the manager module's bound secret path — exactly as a consumer's db password arrives. * So there is no fake node key pair mounted here any more; the host's own real sealing key does the @@ -32,11 +32,11 @@ * ~/.claude/.credentials.json, access-token-only. * * STUBBED, and flagged in the report: (a) the vendor OAuth endpoint (a node stub); (b) the submit - * transport (the test invokes `mesh-control licence submit-refresh` on the manager's output, standing + * transport (the test invokes `mesh-controller licence submit-refresh` on the manager's output, standing * in for the authenticated cross-node call a manager node would make). The node-private-key stub of * the earlier cut is GONE — the host uses its own real key. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * Build both runtime images into the local daemon first: * scripts/build-module-runtime.sh anthropic-manager /tmp/anthropic-manager.tar * scripts/build-module-runtime.sh anthropic-consumer /tmp/anthropic-consumer.tar @@ -49,7 +49,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -61,7 +61,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "anthropic-bed"; @@ -96,22 +96,22 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } // The manager's adopt/refresh runtime writes its outputs as root, mode 0600 (secret files). To hand -// one to `mesh-control` — whose process runs as a non-root user — the test relaxes the mode on the +// one to `mesh-controller` — whose process runs as a non-root user — the test relaxes the mode on the // anchor host (where `must` is root) and then copies it in: `docker cp` preserves the source mode, so -// the file lands 0644 and mesh-control (a distroless image with no `chmod` of its own) can read it. +// the file lands 0644 and mesh-controller (a distroless image with no `chmod` of its own) can read it. // What is staged this way is a sealed box or the access token, never a cleartext refresh token, so a // world-readable copy discloses nothing the control plane does not already hold. In production the // operator who ran adopt owns the file and this does not arise. async function stageIntoControl(hostPath: string, dest: string): Promise { - await must(`chmod 0644 ${hostPath} && docker cp ${hostPath} mesh-control:${dest}`); + await must(`chmod 0644 ${hostPath} && docker cp ${hostPath} mesh-controller:${dest}`); } async function meshTry(command: string): Promise<{ out: string; ok: boolean }> { - return on(`docker exec mesh-control /mesh-control ${command}`); + return on(`docker exec mesh-controller /mesh-controller ${command}`); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -120,7 +120,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -173,11 +173,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -239,7 +239,7 @@ test("model access refreshes on the manager node and delivers only the access to }, ], }); - await must(`printf %s ${quote(managerManifest)} > /tmp/anthropic-manager.json && docker cp /tmp/anthropic-manager.json mesh-control:/anthropic-manager.json`); + await must(`printf %s ${quote(managerManifest)} > /tmp/anthropic-manager.json && docker cp /tmp/anthropic-manager.json mesh-controller:/anthropic-manager.json`); await mesh(`module add /anthropic-manager.json`); await mesh(`module issue anthropic-manager --node ${MACHINE}`); await mesh(`assign ${MACHINE} anthropic-manager`); @@ -355,7 +355,7 @@ test("model access refreshes on the manager node and delivers only the access to }, ], }); - await must(`printf %s ${quote(consumerManifest)} > /tmp/anthropic-consumer.json && docker cp /tmp/anthropic-consumer.json mesh-control:/anthropic-consumer.json`); + await must(`printf %s ${quote(consumerManifest)} > /tmp/anthropic-consumer.json && docker cp /tmp/anthropic-consumer.json mesh-controller:/anthropic-consumer.json`); await mesh(`module add /anthropic-consumer.json`); await mesh(`module issue anthropic-consumer --node ${MACHINE}`); await mesh(`assign ${MACHINE} anthropic-consumer`); diff --git a/test/integration/assigned-audit.test.ts b/test/integration/assigned-audit.test.ts index 9c4a412..22603fb 100644 --- a/test/integration/assigned-audit.test.ts +++ b/test/integration/assigned-audit.test.ts @@ -7,10 +7,10 @@ * container that connects over amqps with that account — never the broker's own. The trail filling * is the proof the delivered, scoped credential authenticated and the subscription bound. * - * It needs the host binary, the substrate bundle, and the runtime image the scenario loads: + * It needs the host binary, the foundation bundle, and the runtime image the scenario loads: * * MESH_LAB_HOST_BINARY=.../mesh-host - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-runtime-image.sh builds mesh-runtime-audit:development into the local daemon, * which scenarios/audit-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it. */ @@ -22,7 +22,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -34,7 +34,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "audit-node"; @@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -74,9 +74,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -89,7 +89,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -121,12 +121,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -165,7 +165,7 @@ test("the mesh assigns the audit logger, and it consumes over the account the me }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/audit.json && docker cp /tmp/audit.json mesh-control:/audit.json`); + await must(`printf %s ${quote(manifest)} > /tmp/audit.json && docker cp /tmp/audit.json mesh-controller:/audit.json`); await mesh("module add /audit.json"); // The mesh issues its scoped account and seals it to this machine, then assigns and pushes it. @@ -209,7 +209,7 @@ test("the mesh assigns the audit logger, and it consumes over the account the me // And the account the mesh made for it is a real one on the broker — the trail above already // proved it authenticated and read its queue. That it reaches no further than its own queue is - // the scope CreateModuleAccount applies, checked as patterns in mesh-control's own tests. + // the scope CreateModuleAccount applies, checked as patterns in mesh-controller's own tests. const users = await must(`docker exec mesh-broker lavinmqctl list_users 2>&1`); assert.match(users, /anchor-audit-logger/, `the scoped account is not on the broker:\n${users}`); }); diff --git a/test/integration/assigned-catalogue-apps.test.ts b/test/integration/assigned-catalogue-apps.test.ts index aa583c6..d163449 100644 --- a/test/integration/assigned-catalogue-apps.test.ts +++ b/test/integration/assigned-catalogue-apps.test.ts @@ -17,7 +17,7 @@ * * All are assigned to the one anchor, pushed ONCE, and the node converges ONCE with every one up. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh {mongodb,unifi,postgres} build the runtime images into the local * daemon; scenarios/catalogue-apps.yml stocks them. mongo:7, lscr.io/linuxserver/unifi-controller * and synesthesiam/marytts must be in the local daemon to be stocked. @@ -30,7 +30,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -42,7 +42,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "catalogue-apps"; @@ -73,7 +73,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -82,9 +82,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -97,7 +97,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -129,12 +129,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -310,7 +310,7 @@ test("the mesh assigns mongodb, unifi, marrytts and postgres to one node in one // --- add, issue (those that serve/emit), assign, then ONE push ---------------------------------- async function add(name: string, manifest: string): Promise { - await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); } diff --git a/test/integration/assigned-catalogue-media.test.ts b/test/integration/assigned-catalogue-media.test.ts index c75fad6..3830201 100644 --- a/test/integration/assigned-catalogue-media.test.ts +++ b/test/integration/assigned-catalogue-media.test.ts @@ -23,7 +23,7 @@ * * All is assigned to the one anchor, pushed ONCE, and the node converges ONCE with both modules up. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh {sonarr,radarr} build the runtime images into the local daemon; * scenarios/catalogue-media.yml stocks them. lscr.io/linuxserver/{sonarr,radarr} must be in the * local daemon; the service images are pulled from the internet. Each *arr runtime is given a lab @@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -50,7 +50,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "catalogue-media"; @@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -90,9 +90,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -105,7 +105,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -137,12 +137,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -248,7 +248,7 @@ test("sonarr and radarr, both accessing one operator-owned directory, co-resolve // --- add, issue (both emit events → each gets a scoped broker account), assign ------------------ async function add(name: string, manifest: string): Promise { - await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); } @@ -274,7 +274,7 @@ test("sonarr and radarr, both accessing one operator-owned directory, co-resolve // read as two modules owning one path. Now each ACCESSES it, so the pair co-resolves and both are // sent in a single declaration. A refusal here (nonzero, or "could not be resolved") is the // regression this bed exists to catch. - const pushed = await on(`docker exec mesh-control /mesh-control push ${MACHINE}`); + const pushed = await on(`docker exec mesh-controller /mesh-controller push ${MACHINE}`); assert.ok(pushed.ok, `the co-resident push was REFUSED — the shared-access collision ADR 0051 removed is back:\n${pushed.out}`); assert.doesNotMatch(pushed.out, /could not be resolved|both declare the path|shared data is the operator/, diff --git a/test/integration/assigned-catalogue-mqtt.test.ts b/test/integration/assigned-catalogue-mqtt.test.ts index 5bd307d..762738c 100644 --- a/test/integration/assigned-catalogue-mqtt.test.ts +++ b/test/integration/assigned-catalogue-mqtt.test.ts @@ -22,7 +22,7 @@ * the seed), and mosquitto's provisioner — running in the assigned runtime — creates a scoped client * for a contribution the mesh delivered, which then authenticates with the password the mesh minted. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh mosquitto builds mesh-runtime-mosquitto:development (carrying * mosquitto_ctrl and the compiled bootstrap entrypoint) into the local daemon, which * scenarios/catalogue-mqtt.yml stocks. eclipse-mosquitto:2 must be in the local daemon to be @@ -36,7 +36,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -48,7 +48,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "catalogue-mqtt"; @@ -78,7 +78,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -87,9 +87,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -102,7 +102,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -134,12 +134,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh and starts the host so it applies what it is pushed. @@ -187,7 +187,7 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker serves: { "mqtt-topic": {} }, emits: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"], // The events entrypoint subscribes to its own lifecycle events (an audit log), so it consumes - // them too — declared, or the substrate never makes the queue the runtime binds (ADR 0046). + // them too — declared, or the foundation never makes the queue the runtime binds (ADR 0046). consumes: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"], receives: { "mqtt-topic": "/var/lib/mosquitto-module/grants/mesh.json" }, grants: { "mqtt-topic": "/var/lib/mosquitto-module/grants" }, @@ -254,7 +254,7 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker ], }); - await must(`printf %s ${quote(manifest)} > /tmp/mosquitto.json && docker cp /tmp/mosquitto.json mesh-control:/mosquitto.json`); + await must(`printf %s ${quote(manifest)} > /tmp/mosquitto.json && docker cp /tmp/mosquitto.json mesh-controller:/mosquitto.json`); await mesh("module add /mosquitto.json"); const issued = await mesh(`module issue mosquitto --node ${MACHINE}`); assert.match(issued, /scoped to what it emits and consumes/, issued); @@ -324,7 +324,7 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker // each consumer it reads the login the mesh derived and the mesh-minted password the host unsealed, // and creates exactly that dynsec client, scoped to its own topic subtree (ADR 0048). A // hand-written contributions file + secret stand in for the control plane's write; their SHAPE is - // what mesh-control produces. The proof is authentication as the consumer with the mesh's password + // what mesh-controller produces. The proof is authentication as the consumer with the mesh's password // — a provisioner that invented its own would refuse the connection. const consumerPw = "mesh-minted-mqtt-7b2e1a"; await must(`printf %s ${quote(consumerPw)} > /var/lib/mosquitto-module/grants/app.secret`); diff --git a/test/integration/assigned-catalogue-small.test.ts b/test/integration/assigned-catalogue-small.test.ts index e32c950..819b711 100644 --- a/test/integration/assigned-catalogue-small.test.ts +++ b/test/integration/assigned-catalogue-small.test.ts @@ -18,7 +18,7 @@ * path is fulfilled by creating the consumer's login with the mesh-minted password — it seals nothing * and needs no seal key (novox/hq ADR 0048, issue 032-provider-runtime-has-no-seal-key). * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh {postgres,redis,minio,plex} build the four runtime images into the * local daemon; scenarios/catalogue-small.yml stocks them. postgres:17-alpine, redis:7-alpine and * minio/minio:latest is pulled from the internet by the node itself. @@ -31,7 +31,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -43,7 +43,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "catalogue-small"; @@ -74,7 +74,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -83,9 +83,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -104,7 +104,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -136,12 +136,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -340,7 +340,7 @@ test("the mesh assigns postgres, redis, minio and plex to one node in one push, // --- add, issue (the four that serve/emit), assign, then ONE push ------------------------------- async function add(name: string, manifest: string): Promise { - await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); } @@ -455,7 +455,7 @@ test("the mesh assigns postgres, redis, minio and plex to one node in one push, // issue 032): for each consumer it reads the login the mesh derived and the mesh-minted password the // host unsealed, and creates the ACL user under exactly that login and password — sealing nothing // and writing no credential file. A hand-written contributions file and secret stand in for the - // control plane's write; their SHAPE is what mesh-control produces. The proof is authentication as + // control plane's write; their SHAPE is what mesh-controller produces. The proof is authentication as // the consumer with the mesh's password (PONG) — a provisioner that invented its own would answer // WRONGPASS. const redisPassword = "mesh-minted-9f3c2a"; diff --git a/test/integration/assigned-grafana.test.ts b/test/integration/assigned-grafana.test.ts index bea54a3..1d92897 100644 --- a/test/integration/assigned-grafana.test.ts +++ b/test/integration/assigned-grafana.test.ts @@ -10,7 +10,7 @@ * Grafana — that the serve queue is bound is the proof the settings reached the runtime and its * tools loaded from them. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh grafana builds mesh-runtime-grafana:development into the local * daemon, which scenarios/grafana-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it. */ @@ -22,7 +22,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -34,7 +34,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "grafana-node"; @@ -63,7 +63,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -72,7 +72,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -85,7 +85,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -117,11 +117,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -163,13 +163,13 @@ test("the mesh assigns grafana's runtime, configured by settings, and it serves }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-control:/grafana.json`); + await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-controller:/grafana.json`); await mesh("module add /grafana.json"); // The operator states grafana's URL and API token as settings for this node — the config the // runtime will read. Nothing about them is in the manifest. const settings = JSON.stringify({ url: "http://127.0.0.1:3000", token: "lab-grafana-token" }); - await must(`printf %s ${quote(settings)} > /tmp/grafana-settings.json && docker cp /tmp/grafana-settings.json mesh-control:/grafana-settings.json`); + await must(`printf %s ${quote(settings)} > /tmp/grafana-settings.json && docker cp /tmp/grafana-settings.json mesh-controller:/grafana-settings.json`); await mesh(`settings set grafana /grafana-settings.json --node ${MACHINE}`); const issued = await mesh(`module issue grafana --node ${MACHINE}`); diff --git a/test/integration/assigned-model-usage.test.ts b/test/integration/assigned-model-usage.test.ts index abdd1cf..c809e63 100644 --- a/test/integration/assigned-model-usage.test.ts +++ b/test/integration/assigned-model-usage.test.ts @@ -1,7 +1,7 @@ /** * The usage context store, proved end to end (novox/hq ADR 0054). * - * model-usage is a MODULE, not a control-plane feature, because mesh-control is a CLI and cannot + * model-usage is a MODULE, not a control-plane feature, because mesh-controller is a CLI and cannot * consume events: the store that keeps the latest usage reading has to be something that subscribes * to `module.*.usage.*` and upserts. This bed raises a real mesh, provisions model-usage its own * postgres store, emits usage events into the mesh, and reads the store back to prove the three @@ -14,15 +14,15 @@ * 3. IN THE CLEAR — the value and its raw payload are ordinary columns an ordinary select returns; * nothing about usage is sealed. * - * The topology mirrors two-node-db: the app-postgres provider and the mesh's own substrate store both - * want host port 5432, so the substrate (store, broker, control) owns `anchor` and NOTHING else, and + * The topology mirrors two-node-db: the app-postgres provider and the mesh's own foundation store both + * want host port 5432, so the foundation (store, broker, control) owns `anchor` and NOTHING else, and * `laptop` runs the postgres PROVIDER and the model-usage CONSUMER co-located. Only enrolment crosses * to anchor, over the underlay both machines share. * - * It needs a host binary and the substrate bundle: + * It needs a host binary and the foundation bundle: * * MESH_LAB_HOST_BINARY=.../mesh-host - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * * HELPER — stock the two runtimes into the local daemon before the run (some may already be there): * scripts/build-module-runtime.sh postgres /tmp/postgres.tar @@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -50,12 +50,12 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "model-usage-bed"; /** The node that carries the postgres provider and the model-usage consumer. anchor carries only the - * substrate. */ + * foundation. */ const NODE = "laptop"; let instanceId = ""; @@ -83,7 +83,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane, a container on the first node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -92,9 +92,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -120,7 +120,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise { } | undefined; let said = ""; try { - const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`); + const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`); said = asked.out; if (asked.ok) state = JSON.parse(said); } catch (err) { @@ -160,11 +160,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) { @@ -269,7 +269,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot }); async function addIssueAssign(name: string, manifest: string): Promise { - await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); await mesh(`module issue ${name} --node ${NODE}`); await mesh(`assign ${NODE} ${name}`); @@ -289,7 +289,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot await mesh(`push ${NODE}`); await settled(NODE); - // The provider owns 5432 on laptop; the substrate store owns it on anchor. + // The provider owns 5432 on laptop; the foundation store owns it on anchor. const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`); for (const name of ["postgres", "mesh-postgres", "mesh-model-usage"]) { assert.match(onLaptop, new RegExp(`(^|\\n)${name}(\\n|$)`), @@ -331,7 +331,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot assert.match(tableReady, /^t$/m, `the usage table was never created (the consumer did not migrate):\n${tableReady}`); // Inject a usage event into the mesh. model-usage is a PURE CONSUMER, so its own broker account has - // no publish right (mesh-control grants write to mesh.events only to a module that declares `emits`). + // no publish right (mesh-controller grants write to mesh.events only to a module that declares `emits`). // So publish from the postgres provider's container — a publisher already on the node — overriding // the source header to the real producer the key names, exactly as events.test.ts injects with // `-e MESH_MODULE=...`. Write permission is per-exchange, not per-key, so postgres may carry any diff --git a/test/integration/assigned-plex.test.ts b/test/integration/assigned-plex.test.ts index 33c4b5c..14e6877 100644 --- a/test/integration/assigned-plex.test.ts +++ b/test/integration/assigned-plex.test.ts @@ -10,10 +10,10 @@ * proof the invocation routed to the assigned runtime, ran plex's real code, and replied, all under * the scoped account and never the broker's own. * - * It needs the host binary, the substrate bundle, and the runtime image the scenario loads: + * It needs the host binary, the foundation bundle, and the runtime image the scenario loads: * * MESH_LAB_HOST_BINARY=.../mesh-host - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh plex builds mesh-runtime-plex:development into the local daemon, * which scenarios/plex-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it. */ @@ -25,7 +25,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -37,7 +37,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "plex-node"; @@ -68,7 +68,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -77,9 +77,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -92,7 +92,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -124,12 +124,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -176,7 +176,7 @@ test("the mesh assigns plex's runtime, and it serves plex's tools over the accou }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/plex.json && docker cp /tmp/plex.json mesh-control:/plex.json`); + await must(`printf %s ${quote(manifest)} > /tmp/plex.json && docker cp /tmp/plex.json mesh-controller:/plex.json`); await mesh("module add /plex.json"); // The mesh issues plex's scoped account and seals it to this machine, then assigns and pushes it. @@ -210,7 +210,7 @@ test("the mesh assigns plex's runtime, and it serves plex's tools over the accou `plex's runtime never bound its serve queue:\n${(await on(`docker logs mesh-plex 2>&1 | tail -20`)).out}\n---\n${served}`); // A caller invokes plex.plex_reachable over the mesh, from the bootstrap account (a caller, like - // mesh-control's command API — plex's own account serves, it does not call). The reply is the + // mesh-controller's command API — plex's own account serves, it does not call). The reply is the // tool's own answer: it ran in the assigned runtime and reported the Plex server is unreachable // (there is none in the lab). A reply at all — not a timeout — is the proof the invocation routed // to the assigned runtime and ran plex's real code under its scoped account. diff --git a/test/integration/assigned-redis.test.ts b/test/integration/assigned-redis.test.ts index 647d889..2e8cea0 100644 --- a/test/integration/assigned-redis.test.ts +++ b/test/integration/assigned-redis.test.ts @@ -12,9 +12,9 @@ * has no way yet to deliver one to a provider's runtime (04-ISSUES). The manifest here sets a * lab-local key so the mechanism can be proven; the delivery is a separate, open design question. * - * It needs the host binary, the substrate bundle, and the runtime image the scenario loads: + * It needs the host binary, the foundation bundle, and the runtime image the scenario loads: * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development into the local * daemon, which scenarios/redis-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it. */ @@ -26,7 +26,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -38,7 +38,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "redis-node"; @@ -67,7 +67,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -76,7 +76,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -89,7 +89,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -121,11 +121,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -151,7 +151,7 @@ test("the mesh assigns redis, and its runtime serves tools and provisions grants version: "1", emits: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], // redis's events entrypoint subscribes to its own lifecycle events (an audit-trail log), so it - // consumes them too — declared, or the substrate never makes the queue the runtime binds and it + // consumes them too — declared, or the foundation never makes the queue the runtime binds and it // crashes on start with a 404 (novox/hq ADR 0046: a consume is declared). consumes: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], "own-secrets": { default: "/var/lib/redis-module/default.secret", broker: "/var/lib/mesh/redis/broker" }, @@ -190,7 +190,7 @@ test("the mesh assigns redis, and its runtime serves tools and provisions grants }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`); + await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`); await mesh("module add /redis.json"); const issued = await mesh(`module issue redis --node ${MACHINE}`); diff --git a/test/integration/assigned-schedule-tick.test.ts b/test/integration/assigned-schedule-tick.test.ts index 7cf53f8..e7487d1 100644 --- a/test/integration/assigned-schedule-tick.test.ts +++ b/test/integration/assigned-schedule-tick.test.ts @@ -25,8 +25,8 @@ * registry by digest; the host pulls and runs it on the cadence. * * MESH_LAB_HOST_BINARY=.../mesh-host (feat/apply-schedule — the scheduler that fires the step) - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock - * MESH_LAB_MODULES=.../mesh-control/examples/modules (feat/schedule-container — the parser that + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock + * MESH_LAB_MODULES=.../mesh-controller/examples/modules (feat/schedule-container — the parser that * carries `schedule` through). scenarios/schedule-tick.yml stocks alpine:latest (which must be in * the local daemon) and serves it by digest; there is no runtime image — schedtest is a bare tick. */ @@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -50,7 +50,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "schedule-tick"; @@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -90,9 +90,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -119,7 +119,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -151,12 +151,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -197,7 +197,7 @@ test("the mesh assigns schedtest: installing the schedule does not run it, and t ], }); - await must(`printf %s ${quote(manifest)} > /tmp/schedtest.json && docker cp /tmp/schedtest.json mesh-control:/schedtest.json`); + await must(`printf %s ${quote(manifest)} > /tmp/schedtest.json && docker cp /tmp/schedtest.json mesh-controller:/schedtest.json`); await mesh("module add /schedtest.json"); // No `module issue`: schedtest serves/consumes nothing and carries no runtime, so it needs no // broker account. `assign` resolves its plan (no own-secret to fill) and ONE push converges it. diff --git a/test/integration/assigned-sonarr.test.ts b/test/integration/assigned-sonarr.test.ts index 656f903..dc63f19 100644 --- a/test/integration/assigned-sonarr.test.ts +++ b/test/integration/assigned-sonarr.test.ts @@ -8,7 +8,7 @@ * Sonarr here), registers its tools, and serves them under a scoped account. There is no live Sonarr * to reach — that the serve queue is bound is the proof the key was detected and the tools loaded. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh sonarr builds mesh-runtime-sonarr:development into the local * daemon, which scenarios/sonarr-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it. */ @@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -32,7 +32,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "sonarr-node"; @@ -61,7 +61,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -70,7 +70,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -83,7 +83,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -115,11 +115,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -167,7 +167,7 @@ test("the mesh assigns sonarr's runtime, and it detects its key and serves its t }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/sonarr.json && docker cp /tmp/sonarr.json mesh-control:/sonarr.json`); + await must(`printf %s ${quote(manifest)} > /tmp/sonarr.json && docker cp /tmp/sonarr.json mesh-controller:/sonarr.json`); await mesh("module add /sonarr.json"); const issued = await mesh(`module issue sonarr --node ${MACHINE}`); diff --git a/test/integration/assigned-tools-confluence.test.ts b/test/integration/assigned-tools-confluence.test.ts index 681ebf3..00f3fed 100644 --- a/test/integration/assigned-tools-confluence.test.ts +++ b/test/integration/assigned-tools-confluence.test.ts @@ -17,7 +17,7 @@ * A tool would only fail if it were actually invoked without real creds — which this bed does not do, * because the point is exactly that serving does not require them. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh confluence builds mesh-runtime-confluence:development into the * local daemon; scenarios/tools-confluence.yml stocks it. There is no service image. */ @@ -29,7 +29,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -41,7 +41,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "tools-confluence"; @@ -72,7 +72,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -81,9 +81,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -96,7 +96,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -128,12 +128,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -185,7 +185,7 @@ test("the mesh assigns confluence: its tools-only runtime comes up and serves th ], }); - await must(`printf %s ${quote(manifest)} > /tmp/confluence.json && docker cp /tmp/confluence.json mesh-control:/confluence.json`); + await must(`printf %s ${quote(manifest)} > /tmp/confluence.json && docker cp /tmp/confluence.json mesh-controller:/confluence.json`); await mesh("module add /confluence.json"); // confluence serves tools, so it is issued a scoped broker account (it emits/consumes nothing else). const issued = await mesh(`module issue confluence --node ${MACHINE}`); diff --git a/test/integration/assigned-tools-gitlab.test.ts b/test/integration/assigned-tools-gitlab.test.ts index 70fcc19..622882d 100644 --- a/test/integration/assigned-tools-gitlab.test.ts +++ b/test/integration/assigned-tools-gitlab.test.ts @@ -16,7 +16,7 @@ * A tool would only fail if it were actually invoked without real creds — which this bed does not do, * because the point is exactly that serving does not require them. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh gitlab builds mesh-runtime-gitlab:development into the local * daemon; scenarios/tools-gitlab.yml stocks it. There is no service image — gitlab is tools-only. */ @@ -28,7 +28,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -40,7 +40,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "tools-gitlab"; @@ -71,7 +71,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -80,9 +80,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -95,7 +95,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -127,12 +127,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it @@ -184,7 +184,7 @@ test("the mesh assigns gitlab: its tools-only runtime comes up and serves the fu ], }); - await must(`printf %s ${quote(manifest)} > /tmp/gitlab.json && docker cp /tmp/gitlab.json mesh-control:/gitlab.json`); + await must(`printf %s ${quote(manifest)} > /tmp/gitlab.json && docker cp /tmp/gitlab.json mesh-controller:/gitlab.json`); await mesh("module add /gitlab.json"); // gitlab serves tools, so it is issued a scoped broker account (it emits/consumes nothing else). const issued = await mesh(`module issue gitlab --node ${MACHINE}`); diff --git a/test/integration/assigned-two-node-db.test.ts b/test/integration/assigned-two-node-db.test.ts index 33ea3d0..d8602f8 100644 --- a/test/integration/assigned-two-node-db.test.ts +++ b/test/integration/assigned-two-node-db.test.ts @@ -1,18 +1,18 @@ /** * The DB-consumer chain a single node cannot host, proved across two machines. * - * app-postgres and the mesh's own substrate store both want host port 5432, so they cannot share a - * machine. Every earlier catalogue bed put the provider on the same node as the substrate and got + * app-postgres and the mesh's own foundation store both want host port 5432, so they cannot share a + * machine. Every earlier catalogue bed put the provider on the same node as the foundation and got * away with it only because the provider published no 5432 a consumer ever reached, or because the - * substrate's store and the module's postgres were the same container. The moment a real + * foundation's store and the module's postgres were the same container. The moment a real * postgres PROVIDER must publish 5432 for real consumers to connect, it collides with the store the - * substrate already has there — and the chain is blocked single-node. + * foundation already has there — and the chain is blocked single-node. * - * This is the split that unblocks it. `anchor` runs the substrate (store, broker, control) and + * This is the split that unblocks it. `anchor` runs the foundation (store, broker, control) and * NOTHING else. `laptop` runs the whole chain: the postgres and redis PROVIDERS, and the baserow * and letta CONSUMERS that require them. Provider and consumers are co-located on laptop, so the * grant never crosses a node boundary and no overlay is needed — only enrolment crosses to anchor, - * over the underlay both machines share. And because the substrate store is on the OTHER node, the + * over the underlay both machines share. And because the foundation store is on the OTHER node, the * provider owns laptop's 5432 uncontested. * * The four manifests are the committed catalogue shapes (novox/hq ADR 0039/0047/0048), verbatim @@ -22,10 +22,10 @@ * ONCE; laptop converges once with every one up, and the two consumers are provisioned against the * database the provider on their own node gave them. * - * It needs a host binary and the substrate bundle: + * It needs a host binary and the foundation bundle: * * MESH_LAB_HOST_BINARY=.../mesh-host - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * * HELPER — stock the four runtimes into the local daemon before the run (some may already be there): * scripts/build-module-runtime.sh postgres /tmp/postgres.tar @@ -44,7 +44,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -56,11 +56,11 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "two-node-db"; -/** The node that carries the whole DB-consumer chain. anchor carries only the substrate. */ +/** The node that carries the whole DB-consumer chain. anchor carries only the foundation. */ const NODE = "laptop"; let instanceId = ""; @@ -88,7 +88,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane, a container on the first node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -97,9 +97,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -128,7 +128,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise { } | undefined; let said = ""; try { - const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`); + const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`); said = asked.out; if (asked.ok) state = JSON.parse(said); } catch (err) { @@ -169,13 +169,13 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // The first node raises the substrate — store, broker, control — from the bundle its host carries, + // The first node raises the foundation — store, broker, control — from the bundle its host carries, // its digests rewritten to the ones this scenario's own registry serves. - await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // Both machines join the one mesh, each with a token that says what the mesh calls it, and each @@ -196,7 +196,7 @@ after(async () => { await destroyAll(`${SCENARIO}-`); }, { timeout: 600_000 }); -test("the provider and its consumers ride the second node while the substrate owns 5432 on the first", { +test("the provider and its consumers ride the second node while the foundation owns 5432 on the first", { skip, timeout: 1_500_000, }, async () => { // ================================================================================================ @@ -392,7 +392,7 @@ test("the provider and its consumers ride the second node while the substrate ow // --- add, issue a scoped broker account, assign to laptop, then ONE push ------------------------- async function addIssueAssign(name: string, manifest: string): Promise { - await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); const issued = await mesh(`module issue ${name} --node ${NODE}`); assert.match(issued, /scoped to what it emits and consumes/, issued); @@ -400,7 +400,7 @@ test("the provider and its consumers ride the second node while the substrate ow } // The consumer connects to its provider by the provider's PRIVATE-NETWORK address — the binding's - // `at`, which mesh-control fills as "where the consuming machine is on the private network, empty + // `at`, which mesh-controller fills as "where the consuming machine is on the private network, empty // if it is not on one" (declaration.go). So even though provider and consumer are co-located on // laptop, the address baserow is handed is the mesh OVERLAY address, and it is empty unless the // machine is on the overlay. The overlay networking is therefore assigned first, to both nodes. @@ -411,7 +411,7 @@ test("the provider and its consumers ride the second node while the substrate ow // Providers first, then the consumers that require them. The mesh resolves the whole set at push // time regardless of order; this order simply reads like the dependency graph. Provider AND - // consumers all go to laptop; the 5432 conflict is gone because the substrate store is on anchor. + // consumers all go to laptop; the 5432 conflict is gone because the foundation store is on anchor. await addIssueAssign("postgres", postgresManifest); await addIssueAssign("redis", redisManifest); await addIssueAssign("baserow", baserowManifest); @@ -422,14 +422,14 @@ test("the provider and its consumers ride the second node while the substrate ow await settled(NODE); // ================================================================================================ - // THE two-node split — the substrate owns 5432 on anchor, the provider owns it on laptop. + // THE two-node split — the foundation owns 5432 on anchor, the provider owns it on laptop. // ================================================================================================ const onAnchor = await must("anchor", `docker ps --format '{{.Names}}'`); const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`); - assert.match(onAnchor, /(^|\n)mesh-store(\n|$)/, "the substrate store is not on the first node"); + assert.match(onAnchor, /(^|\n)mesh-store(\n|$)/, "the foundation store is not on the first node"); assert.doesNotMatch(onAnchor, /(^|\n)postgres(\n|$)/, - "the postgres provider landed on the substrate node — the 5432 collision this bed exists to avoid"); - assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the substrate store leaked onto the second node"); + "the postgres provider landed on the foundation node — the 5432 collision this bed exists to avoid"); + assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the foundation store leaked onto the second node"); assert.match(onLaptop, /(^|\n)postgres(\n|$)/, "the postgres provider is not on the second node"); // ================================================================================================ @@ -453,7 +453,7 @@ test("the provider and its consumers ride the second node while the substrate ow // REGRESSION (provider-seal-key): baserow's server.env DATABASE_PASSWORD is filled from the // ${secret:postgres-database} placeholder; its database.secret file carries the same credential via // the secrets: map. Both are baserow's one postgres password and MUST be equal. Before the - // mesh-control fix (secrets_into_files.go matched a need by provision name alone, not by consuming + // mesh-controller fix (secrets_into_files.go matched a need by provision name alone, not by consuming // module) the placeholder path took whichever co-located consumer came last — letta's — so the two // diverged and baserow authenticated with the wrong password. novox/hq 04-ISSUES/022. { @@ -496,7 +496,7 @@ test("the provider and its consumers ride the second node while the substrate ow } // ================================================================================================ - // Each module got its own scoped broker account on the substrate's broker (which is on anchor, + // Each module got its own scoped broker account on the foundation's broker (which is on anchor, // reached from laptop over the shared segment) — named for the node that runs it and the module. // ================================================================================================ const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`); diff --git a/test/integration/builds.test.ts b/test/integration/builds.test.ts index 539068b..5118a44 100644 --- a/test/integration/builds.test.ts +++ b/test/integration/builds.test.ts @@ -9,7 +9,7 @@ * agree over a wire. Everything either side of the wire is already asserted in its own suite. * * MESH_LAB_HOST_BINARY a built mesh-host - * MESH_LAB_BUNDLE the substrate bundle + * MESH_LAB_BUNDLE the foundation bundle * MESH_LAB_BUILDER a built mesh-builder */ @@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; import { incus } from "../../src/incus/client.ts"; import { machineName } from "../../src/lifecycle/names.ts"; @@ -35,7 +35,7 @@ const skip = !capability.usable : !host || !existsSync(host) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle" : !builder || !existsSync(builder) ? "MESH_LAB_BUILDER is not set to a built mesh-builder" : false; @@ -79,12 +79,12 @@ async function must(command: string): Promise { } async function mesh(command: string): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`); + return must(`docker exec mesh-controller /mesh-controller ${command}`); } /** The bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } before(async () => { @@ -92,8 +92,8 @@ before(async () => { const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {}); instanceId = raised.instanceId; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`); // The mesh's artifact store, standing where the `registry` module would. Read back rather than // assumed: a builder publishing into a registry that never came up fails several minutes later, @@ -166,7 +166,7 @@ test("a build that cannot succeed says why, and records nothing", { skip, timeou // A failure is a result. A build that fails silently is indistinguishable from a builder that // is not running, and those want completely different responses. const { out, ok } = await on( - `docker exec mesh-control /mesh-control build /root/does-not-exist --wait 120s`, + `docker exec mesh-controller /mesh-controller build /root/does-not-exist --wait 120s`, ); assert.equal(ok, false, "a build of nothing reported success"); assert.match(out, /could not build/, out); diff --git a/test/integration/canary.test.ts b/test/integration/canary.test.ts index 7584f22..e94b27e 100644 --- a/test/integration/canary.test.ts +++ b/test/integration/canary.test.ts @@ -35,7 +35,7 @@ const skip = !capability.usable : !host || !existsSync(host) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle" : false; const SCENARIO = "first-node"; @@ -62,7 +62,7 @@ async function must(command: string, timeoutMs?: number): Promise { } const mesh = (command: string, timeoutMs?: number) => - must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); before(async () => { if (skip) return; @@ -70,9 +70,9 @@ before(async () => { onProgress: (m) => console.log(`raise: ${m}`), }); instanceId = raised.instanceId; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${ + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${ pinnedInto(readFileSync(bundle, "utf8"), raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 300_000); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 300_000); // **And the machine joins.** Applying the bundle raises a control plane; it does not tell that // control plane a machine exists. Leaving this out is what the first run of this canary found, @@ -111,7 +111,7 @@ test("a module reaches the machine", { skip, timeout: 600_000 }, async () => { { id: "note", type: "file", path: "/var/lib/canary/it-arrived", content: "yes\n", mode: "0644" }, ], }))} > /tmp/canary.json`); - await must(`docker cp /tmp/canary.json mesh-control:/canary.json`); + await must(`docker cp /tmp/canary.json mesh-controller:/canary.json`); await mesh("module add /canary.json"); await mesh(`assign ${MACHINE} canary`); await mesh(`push ${MACHINE}`, 300_000); @@ -151,7 +151,7 @@ test("a consumer gets a credential it can use", { skip, timeout: 600_000 }, asyn ], }))} > /tmp/canary-app.json`); for (const name of ["canary-store", "canary-app"]) { - await must(`docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must(`docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); await mesh(`assign ${MACHINE} ${name}`); } diff --git a/test/integration/certificates.test.ts b/test/integration/certificates.test.ts index 37aa663..1fe8e64 100644 --- a/test/integration/certificates.test.ts +++ b/test/integration/certificates.test.ts @@ -25,7 +25,7 @@ const proxy = process.env["MESH_LAB_ROUTE_PROXY"] ?? ""; const skip = !capability.usable ? `lab not usable: ${capability.why}` : !proxy - ? "set MESH_LAB_ROUTE_PROXY to a built proxy (mesh-control: go build ./examples/route-proxy)" + ? "set MESH_LAB_ROUTE_PROXY to a built proxy (mesh-controller: go build ./examples/route-proxy)" : false; const SCENARIO = "a-public-name"; diff --git a/test/integration/events.test.ts b/test/integration/events.test.ts index 3ed2758..c986311 100644 --- a/test/integration/events.test.ts +++ b/test/integration/events.test.ts @@ -5,8 +5,8 @@ * credential is delivered over it. This proves the other half of the bus (novox/hq ADR 0046): the * events exchange, where a module emits and any number listen, and the audit logger consumes `#` * and writes down what happened. It runs against the `mesh-broker` this scenario's own host raised - * from the substrate bundle — not a broker a test stood up — because "the mesh hosts the broker" - * (tier-1 substrate) is the thing being relied on. + * from the foundation bundle — not a broker a test stood up — because "the mesh hosts the broker" + * (tier-1 foundation) is the thing being relied on. * * The wire shape it asserts is ADR 0047: metadata rides as headers so the body is only the * payload, a consumer gets a durable per-consumer queue `..events`, and a @@ -14,10 +14,10 @@ * on the raised broker is the check that the runtime provisioned the contract, not just that a * message happened to arrive. * - * It needs the host binary and the substrate bundle, like the mesh walk, plus a runtime image: + * It needs the host binary and the foundation bundle, like the mesh walk, plus a runtime image: * * MESH_LAB_HOST_BINARY=.../mesh-host - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * MESH_LAB_RUNTIME=.../mesh-runtime-audit.tar (docker save of the runtime+audit-logger image; * built by scripts/build-runtime-image.sh) * @@ -33,7 +33,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; import { incus } from "../../src/incus/client.ts"; import { machineName } from "../../src/lifecycle/names.ts"; @@ -48,7 +48,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : !runtime || !existsSync(runtime) ? "MESH_LAB_RUNTIME is not set to a runtime image tar (scripts/build-runtime-image.sh)" : false; @@ -58,7 +58,7 @@ const MACHINE = "anchor"; /** Where the audit-logger container writes its trail, on the machine — mounted from a host dir. */ const TRAIL_DIR = "/var/lib/mesh-audit"; const TRAIL = `${TRAIL_DIR}/audit.log`; -/** The broker the substrate raised, reachable on the node's loopback (novox/hq ADR 0001). */ +/** The broker the foundation raised, reachable on the node's loopback (novox/hq ADR 0001). */ const BROKER = "amqp://guest:guest@127.0.0.1:5672/"; let instanceId = ""; @@ -73,7 +73,7 @@ function quote(s: string): string { * A command on the machine, its exit read from a marker on its own line. * * `exec 2>&1` on its own first line and the marker on its own last line, so a command that carries - * a heredoc — the substrate bundle is written with one — terminates where it says it does rather + * a heredoc — the foundation bundle is written with one — terminates where it says it does rather * than swallowing the marker (the fault mesh.test.ts documents). */ async function on(command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> { @@ -92,14 +92,14 @@ async function must(command: string, timeoutMs?: number): Promise { } /** - * The substrate bundle, its image references pointed at this scenario's own registry. + * The foundation bundle, its image references pointed at this scenario's own registry. * * A digest belongs to whatever registry serves it, so the committed bundle names a registry that * is not this one; matching by repository and rewriting to the digest this registry assigned is * what makes it applicable (the same rewrite mesh.test.ts does). */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } /** Read the trail back as parsed JSON lines. */ @@ -120,14 +120,14 @@ before(async () => { }); instanceId = raised.instanceId; - // The node raises its substrate — store, broker and the rest — from the bundle, applied from a + // The node raises its foundation — store, broker and the rest — from the bundle, applied from a // file because the control plane's image is named by the ID this machine holds it under, // which is not knowable until it has been handed over. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const running = await must(`docker ps --format '{{.Names}}'`); - assert.match(running, /mesh-broker/, `the substrate did not raise a broker:\n${running}`); + assert.match(running, /mesh-broker/, `the foundation did not raise a broker:\n${running}`); // Bring the runtime+audit-logger image onto the machine. Loaded, not pulled: the machine has no // route out (novox/hq the lab is a closed address space), so the image arrives as a tar the way diff --git a/test/integration/fresh-mesh.test.ts b/test/integration/fresh-mesh.test.ts new file mode 100644 index 0000000..a14a221 --- /dev/null +++ b/test/integration/fresh-mesh.test.ts @@ -0,0 +1,514 @@ +/** + * FOUR FRESH MACHINES, AND NOTHING HANDED TO THEM. + * + * The four-machine bed (`whole-mesh-full`) proves the mesh converges. It does so by loading + * thirty-four of the mesh's own images onto its machines from the workstation, because it does not + * build them — something beside the bed built them and copied them in. No real installation looks + * like that, and the lab has been burned by exactly this shape before: it used to raise a registry + * inside the scenario, and a bootstrap that only worked against that registry went green here and + * would have failed on any bare machine. + * + * This bed hands over nothing. The scenario has no `images:` list at all. What the machines get is + * a container runtime and the host binary — prerequisites of a machine, not parts of a mesh — and + * from there: + * + * 1. novox is raised into a mesh of one by the installer, which BUILDS the control plane. + * 2. ace, shanks and g14 JOIN it, across a household NAT, with a token and nothing else. + * 3. The mesh builds the shared base from source, with its own builder. + * 4. The mesh builds a real module standing on that base. + * 5. The anchor runs it, pinned to a digest the mesh's own registry assigned. + * 6. A JOINED machine runs it — which means pulling from a registry that asks who it is. + * + * Steps 1 and 2 are proven elsewhere and are here because the later ones need them. **Steps 3 + * through 6 are what this bed exists for**, and 6 is the one nothing has ever checked: genesis + * puts the builder, the registry and everything they produce on ONE machine, so every earlier + * proof of a mesh-built module running is a proof about the machine that built it. A second + * machine has to fetch, and fetching needs an account nothing yet grants (novox/hq issue 042). + * + * Each step is recorded separately rather than allowed to throw, so a gap at 6 reports as a gap at + * 6 instead of erasing the evidence for 3, 4 and 5. + * + * 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/foundation-first-node.lock + * MESH_LAB_CATALOG=.../mesh-catalog/modules + * MESH_LAB_SOURCE=/mesh-controller.git MESH_LAB_SOURCE_REF= + * MESH_LAB_KEEP=1 to leave it standing afterwards + */ +import { test, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +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, foundationBundle } from "./harness.ts"; +import { genesis, type GenesisResult } from "./genesis.ts"; + +const SCENARIO = "fresh-mesh"; +const CONTROL = "anchor"; +const HOME_NODES = ["home-server", "workstation", "laptop"]; +/** The joined machine asked to run a mesh-built module. Any of the three would do. */ +const SECOND = "home-server"; + +/** The anchor's public address — what every other machine dials, and what its own token must name. */ +const ANCHOR = "192.0.2.20"; +/** Where this mesh's registry answers, on the anchor's public address so a joined node can reach it. */ +const REGISTRY = `${ANCHOR}:5000`; + +/** + * The module built on top of the base, and the base it stands on. + * + * `amqp-ping` is deliberately small and deliberately REAL: its own TypeScript, compiled by the + * shared toolchain, running on the shared runtime, talking to the broker. A module whose artifact + * is a mirrored public image would pass every assertion below while skipping the whole of what is + * under test (novox/hq SELF-UPGRADE-PLAN, rule 1). + */ +const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" }; +/** + * 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 + * provider in the graph, and this is it. No build: its image is upstream. + */ +const PROVIDER = { module: "lavinmq", repo: "mesh-catalog", path: "modules/lavinmq", container: "mesh-lavinmq" }; +/** + * The store, and the catalogue that cannot exist without it. + * + * Both were missing from this test entirely, and nothing complained, because nothing asked. A mesh + * with no catalogue holds no module graph — it cannot say what it has, what a module is made of, + * what a change reaches, or what must be rebuilt. It ran anyway, which is the point: "the mesh is + * up" was being read off genesis finishing. + */ +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-controller", repo: "mesh-controller", path: "", container: "mesh-controller" }; +/** 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 BASE_BUILT = "the mesh builds the shared base from source"; +const STORE_RUNS = "the mesh builds and runs a store of its own"; +const CATALOGUE_RUNS = "the mesh builds and runs its own catalogue"; +const CONTROL_REBUILT = "the mesh rebuilds its own control plane from source"; +const MODULE_BUILT = "the mesh builds a module standing on that base"; +const ANCHOR_RUNS = "the anchor runs the module the mesh built"; +const JOINED = "three machines join the mesh, and reach it over its private network"; +const SECOND_RUNS = "a joined machine runs the module the mesh built"; +const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" }; + +const capability = await labIsUsable(); +const binary = hostBinaryPath(); +const installer = bootstrapBinaryPath(); +const bundle = process.env["MESH_LAB_BUNDLE"] ?? ""; +const catalogDir = process.env["MESH_LAB_CATALOG"] ?? ""; +const source = process.env["MESH_LAB_SOURCE"] ?? ""; +const sourceRef = process.env["MESH_LAB_SOURCE_REF"] ?? ""; +const KEEP = !!process.env["MESH_LAB_KEEP"]; +const FIXED_ID = process.env["MESH_LAB_INSTANCE_ID"] ?? (KEEP ? "fresh-mesh-live" : undefined); + +/** + * Where a repository other than the control plane's lives. + * + * Derived from `MESH_LAB_SOURCE` by swapping the last path segment, because every one of these + * repositories sits beside the others under the same owner on the same forge. Overridable, so a + * forge that is arranged differently does not need this bed edited. + */ +function forgeUrl(repo: string): string { + const override = process.env[`MESH_LAB_SOURCE_${repo.toUpperCase().replaceAll("-", "_")}`]; + if (override) return override; + return source.replace(/[^/]+\.git$/, `${repo}.git`); +} +/** + * What to build, per repository. + * + * A branch is acceptable for an ordinary build; only genesis insists on a commit (ADR 0071). Per + * repository rather than one value for all of them, because a change under test usually lives in + * one repository and the rest should be built from what everyone else has — building them all from + * a feature branch would prove that branch against itself. + * + * MESH_LAB_BUILD_REF the default for every repository + * MESH_LAB_BUILD_REF_MESH_CATALOG ...overridden for one + */ +function refFor(repo: string): string { + const override = process.env[`MESH_LAB_BUILD_REF_${repo.toUpperCase().replaceAll("-", "_")}`]; + return override ?? process.env["MESH_LAB_BUILD_REF"] ?? "main"; +} + +/** + * The shared base's manifest, on this workstation. + * + * The base is a repository with a manifest at its root (novox/hq ADR 0069), so unlike the + * catalogue's modules it is not under `MESH_LAB_CATALOG`. Derived from that path on the convention + * that the checkouts sit beside each other, and overridable for a layout where they do not. + */ +const baseManifest = process.env["MESH_LAB_BASE_MANIFEST"] ?? + resolve(catalogDir, "..", "..", BASE.repo, "module.json"); + +const skip = + !capability.usable ? capability.why : + !binary ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : + !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 foundation template" : + !catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" : + false; + +let instanceId = ""; +let raised: GenesisResult; + +// ---- talking to the machines ------------------------------------------------------------------ + +function quote(s: string): string { + return `'${s.replaceAll("'", `'\\''`)}'`; +} +async function on(machine: string, command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> { + const { stdout } = await exec(instanceId, machine, [ + "sh", "-c", `exec 2>&1\n${command}\necho "__exit=$?"`, + ], timeoutMs); + const marker = stdout.lastIndexOf("__exit="); + if (marker < 0) return { out: stdout, ok: false }; + return { out: stdout.slice(0, marker), ok: stdout.slice(marker + 7).trim() === "0" }; +} +async function must(machine: string, command: string, timeoutMs?: number): Promise { + const { out, ok } = await on(machine, command, timeoutMs); + if (!ok) throw new Error(`${machine}: ${command}\n${out}`); + return out; +} +async function mesh(command: string, timeoutMs?: number): Promise { + return must(CONTROL, `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); +} + +/** A module the mesh has to make for itself: where its source is, and what it runs when it works. */ +interface CoreModule { module: string; repo: string; path: string; container: string } + +/** + * Build a module from source, install it, and wait for it to actually run. + * + * The whole sequence a module goes through, in one place because the mesh has to do it for several + * before it is a mesh at all: register the manifest, build it from its repository and path, issue + * the broker account its runtime needs, assign it, send it, and then ask the machine whether the + * container is up. + * + * **The account is not optional and is not swallowed.** A module's runtime is a tool host: it + * connects to the mesh's broker before doing anything. Without an account the mesh still fills the + * secret the module declares it owns — with a generated value — so the container starts, fails to + * parse a password as a credential document, and loops on a JSON syntax error mentioning no + * missing account. That cost a step that reported PASS. + */ +async function bringUp(m: CoreModule): Promise { + await registerModule(m.module, resolve(catalogDir, m.module, "module.json")); + const built = await mesh( + `build ${forgeUrl(m.repo)} --path ${m.path} --ref ${refFor(m.repo)} --wait 1200s`, 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + await mesh(`module issue ${m.module} --node ${CONTROL}`); + await mesh(`assign ${CONTROL} ${m.module}`); + await mesh(`push ${CONTROL}`, 600_000); + return `${built}\n${await waitForContainer(CONTROL, m.container)}`; +} + +/** + * Wait for one container, BY NAME, to be running. + * + * **Named exactly, because substring matching passed a step that had failed.** Waiting for "a + * container whose name contains lavinmq" was satisfied by the broker — `lavinmq`, up and healthy — + * while the thing actually under test, the module's own runtime `mesh-lavinmq`, was crash-looping + * beside it. The step went green and the fault was found by reading `docker ps` by hand. + */ +async function waitForContainer(node: string, container: string, seconds = 200): Promise { + const deadline = Date.now() + seconds * 1_000; + let last = ""; + while (Date.now() < deadline) { + const ps = (await on(node, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out; + last = ps; + const line = ps.split("\n").find((l) => l.split("\t")[0]?.trim() === container); + if (line && /^Up /.test(line.split("\t")[1]?.trim() ?? "")) return ps; + await new Promise((r) => setTimeout(r, 5_000)); + } + const logs = (await on(node, `docker logs --tail 15 ${container} 2>&1`)).out; + throw new Error( + `${container} is not running on ${node}.\n\ncontainers:\n${last}\n\nwhat it said:\n${logs}`); +} + +/** + * Register a module from a manifest on this workstation. + * + * **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-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` + * to copy into — `docker cp` says so in those words. `/` is the one directory every image has. + */ +async function registerModule(module: string, manifest: string): Promise { + assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`); + const onMachine = `/tmp/${module}.json`; + const inContainer = `/${module}.json`; + await push(instanceId, CONTROL, manifest, onMachine); + await must(CONTROL, `docker cp ${onMachine} mesh-controller:${inContainer}`); + return mesh(`module add ${inContainer}`); +} +function tokenFrom(said: string): string { + const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" ")); + assert.ok(found, `no token in:\n${said}`); + return found; +} + +// ---- steps, recorded rather than thrown -------------------------------------------------------- + +interface Step { ok: boolean; why: string; said: string } +const steps = new Map(); +const order: string[] = []; + +/** Run a step, remember what it said, and never throw. A step whose predecessor failed is skipped. */ +async function step(name: string, after_: string | null, fn: () => Promise): Promise { + order.push(name); + if (after_ && !steps.get(after_)?.ok) { + steps.set(name, { ok: false, why: `not attempted — "${after_}" did not succeed`, said: "" }); + console.log(`SKIPPED ${name}`); + return; + } + console.log(`\n======== ${name} ========`); + try { + const said = await fn(); + steps.set(name, { ok: true, why: "", said }); + console.log(`OK ${name}`); + } catch (err) { + const why = (err as Error).message; + steps.set(name, { ok: false, why, said: "" }); + console.log(`FAILED ${name}\n${why.split("\n").slice(0, 25).join("\n")}`); + } +} + +function report(name: string): string { + const s = steps.get(name); + if (!s) return `${name}: never ran`; + const lines = order.map((n) => { + const it = steps.get(n); + return ` ${it?.ok ? "PASS" : "FAIL"} ${n}`; + }); + return `${s.why}\n\nWhere this bed got to:\n${lines.join("\n")}`; +} + +before(async () => { + if (skip) return; + + const bed = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), { + onProgress: (m) => console.log(`raise: ${m}`), + ...(FIXED_ID ? { instanceId: FIXED_ID } : {}), + }); + instanceId = bed.instanceId; + console.log(`INSTANCE ${instanceId}${KEEP ? " (KEEP — will be left standing)" : ""}`); + console.log(`NOTHING WAS LOADED: this scenario names no images. Every image on every machine ` + + `below was pulled from the internet or built by the mesh.`); + + // ---- 1. GENESIS ----------------------------------------------------------------------------- + // + // The shared description, the same one `genesis-single` calls. The bundle is the TEMPLATE with + // nothing held: no image is pre-resolved, because none is here to resolve to. + await step("a bare machine becomes a mesh of one, raised by the installer", null, async () => { + try { + raised = await genesis({ + instanceId, + node: CONTROL, + installer: installer as string, + catalogDir, + // The broker's advertised address, corrected. + // + // The template hardcodes 192.0.2.10:5671 — the address of the anchor in the single-machine + // bed. A token carries this verbatim as the endpoint an enrolling node dials, so on a mesh + // 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: foundationBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`), + registry: REGISTRY, + source, + sourceRef, + toolsSource: forgeUrl(BASE.repo), + catalogSource: forgeUrl(MODULE.repo), + catalogRef: refFor(MODULE.repo), + log: (m) => console.log(m), + }); + } catch (err) { + throw new Error(`the installer never ran: ${(err as Error).message}`); + } + if (!raised.ok) throw new Error(`${raised.step || "no step named"}: ${raised.why}\n\n${raised.report.join("\n")}`); + return raised.report.join("\n"); + }); + + // ---- 2..6. THE MESH BECOMES ONE -------------------------------------------------------------- + // + // **Joining used to be here, second, and that was the wrong order.** Three machines were enrolled + // into a mesh that could not yet produce a single module, and the step was reported as though + // something had been shown. They do join — reliably — but joining a mesh that can build nothing + // proves only that enrolment works, which was never the doubtful part. + // + // `17-raising-a-mesh` says it plainly: genesis ends with a mesh of one that RUNS, and that is not + // the same as a mesh that WORKS; what remains after the core modules are built is "adding + // machines, and deciding what they run". So everything a mesh needs to be a mesh happens first, + // and machines arrive at the end. + + // The toolchain and runtime every module with code of its own stands on. It is a module, and it + // is built like one — cloned from the forge by the builder installing put here, compiled on the + // machine, published into the mesh's own registry. + await step(BASE_BUILT, GENESIS, async () => { + // Registered from the manifest the builder will also read, so what the mesh holds and what it + // builds are the same description of the same module. + // + // **Not swallowed.** This call used to end in `.catch(() => {})`, on the reasoning that the + // base might already be known. It hid a real failure — the manifest was being named at a path + // inside a container that had never seen it — and the step passed anyway, because a base with + // nothing to stand on builds whether or not the mesh has a record of it. + await registerModule(BASE.module, baseManifest); + const built = await mesh( + `build ${forgeUrl(BASE.repo)} --ref ${refFor(BASE.repo)} --wait 1200s`, 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + return built; + }); + + // 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. + await step(STORE_RUNS, BASE_BUILT, () => bringUp(STORE)); + + // And the catalogue, which was missing from this test altogether. + // + // Without it the mesh holds no module graph: it cannot say what it has, what a module is made + // of, what a change to one reaches, or what must be rebuilt. A mesh in that state still runs, + // which is exactly how its absence went unnoticed — "the mesh is up" was being read off the + // installer finishing rather than off the mesh being able to answer anything. + await step(CATALOGUE_RUNS, STORE_RUNS, () => bringUp(CATALOGUE)); + + // The control plane, rebuilt from its own repository and rolled out. + // + // The installer built it once, which is what got the mesh running. Building it again THROUGH THE + // MODULE PATH — build, notice the version moved, roll it out — is a different claim: it is the + // moment the mesh stops depending on the installer for anything, and the first time the thing + // that performs an upgrade performs one on itself. + await step(CONTROL_REBUILT, CATALOGUE_RUNS, async () => { + const built = await mesh( + `build ${forgeUrl(CONTROL_PLANE.repo)} --ref ${sourceRef} --wait 1200s`, 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + const rolled = await mesh(`upgrade ${CONTROL_PLANE.module} roll-out`, 900_000); + // Asked of the machine rather than believed from the command: the control plane that answers + // afterwards is the one that has to be running for anything below this line to mean anything. + await waitForContainer(CONTROL, CONTROL_PLANE.container); + return `${built}\n${rolled}`; + }); + + // ---- 7..8. SOMETHING TO RUN --------------------------------------------------------------- + await step(MODULE_BUILT, CONTROL_REBUILT, async () => { + await registerModule(MODULE.module, resolve(catalogDir, MODULE.module, "module.json")); + const built = await mesh( + `build ${forgeUrl(MODULE.repo)} --path ${MODULE.path} --ref ${refFor(MODULE.repo)} --wait 1200s`, + 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + // The point of the whole step: what came out is named by a digest this mesh's registry + // assigned, not by a placeholder and not by a tag. + const builds = await mesh(`builds ${MODULE.module}`); + assert.match(builds, /sha256:[0-9a-f]{12}/, + `the build recorded no digest — the module is not pinned to anything this registry serves:\n${builds}`); + return `${built}\n${builds}`; + }); + + // `amqp-ping` requires the `amqp` provision, and the mesh refused to place it: "nothing provides + // amqp, wanted by amqp-ping". That refusal is correct and is the reason this step exists rather + // than the reason to pick an easier module. + // + // `lavinmq` is that module. It was first added here on the belief that it needed no building — + // its broker is an upstream image — and the mesh refused it again: two of its three containers + // named a placeholder digest, "which is never a real image". Also right. The broker is upstream, + // but the module is not only the broker: it carries a run-once bootstrap that writes the broker's + // configuration, a provisioner that grants each consumer its own vhost and user, tools and an + // event consumer, all of it its own code. + await step(NEEDS, MODULE_BUILT, () => bringUp(PROVIDER)); + + // The machine that built it. This is the case every earlier proof covered, and it is here as the + // control for the last step: if this fails, that one's failure says nothing about fetching. + await step(ANCHOR_RUNS, NEEDS, async () => { + await mesh(`module issue ${MODULE.module} --node ${CONTROL}`); + await mesh(`assign ${CONTROL} ${MODULE.module}`); + await mesh(`push ${CONTROL}`, 600_000); + return waitForContainer(CONTROL, MODULE.module); + }); + + // ---- 9. NOW MACHINES MAY ARRIVE --------------------------------------------------------------- + // + // Host binary and a token, and nothing else. The anchor is NOT in this loop — the installer + // enrolled it, and enrolling it again would offer the mesh a second identity for a node it + // already knows. + // + // And they are placed on the mesh's private network, which the earlier ordering never did. The + // mesh refuses to bind a consumer to a provider on another machine when either is missing from + // it — "they are not both on the private network" — so without this the last step fails for a + // reason that has nothing to do with what it is asking. + await step(JOINED, ANCHOR_RUNS, async () => { + const said: string[] = []; + await mesh(`overlay place ${CONTROL} --hub --endpoint ${ANCHOR}:51820 --site hosting`); + for (const machine of HOME_NODES) { + await mesh(`node add ${machine}`); + const token = tokenFrom(await mesh(`token issue --node ${machine}`)); + const out = await must(machine, `${HOST_PATH} enrol --token ${quote(token)}`, 180_000); + assert.match(out, new RegExp(`enrolled as ${machine}`), out); + await must(machine, `nohup ${HOST_PATH} run > /var/log/mesh-host.log 2>&1 & sleep 3`); + await mesh(`overlay place ${machine} --site home`); + said.push(` ${machine} enrolled, running, and on the private network`); + } + said.push((await mesh("node list")).trim()); + said.push((await mesh("overlay show")).trim()); + return said.join("\n"); + }); + + // ---- 10. AND A MACHINE THAT DID NOT BUILD IT -------------------------------------------------- + // + // **The thing nothing has ever checked.** This machine did not build the image and has never seen + // it. To run it, it must fetch it from the mesh's registry — and a joined node has no account + // there (novox/hq issue 042), nor any reason to trust a registry serving plain HTTP over the + // network (issue 048). Both are open, and both are invisible on a mesh of one. + // + // Asked anyway, and asked LAST, so that when it fails everything above still stands as evidence + // of what does work. + await step(SECOND_RUNS, JOINED, async () => { + await mesh(`module issue ${MODULE.module} --node ${SECOND}`); + await mesh(`assign ${SECOND} ${MODULE.module}`); + await mesh(`push ${SECOND}`, 600_000); + try { + return await waitForContainer(SECOND, MODULE.module); + } catch (err) { + const log = (await on(SECOND, `tail -40 /var/log/mesh-host.log`)).out; + throw new Error(`${(err as Error).message}\n\nwhat the host said:\n${log}`); + } + }); + + console.log(`\n================ WHAT THIS MESH DID FOR ITSELF ================`); + for (const n of order) console.log(` ${steps.get(n)?.ok ? "PASS" : "FAIL"} ${n}`); +}, { timeout: 7_200_000 }); + +after(async () => { + if (KEEP) { + console.log(`\nLEFT STANDING: ${instanceId} — not destroyed (MESH_LAB_KEEP).`); + return; + } + if (instanceId) await destroy(instanceId); + await destroyAll(`${SCENARIO}-`); +}, { timeout: 900_000 }); + +for (const name of [ + "a bare machine becomes a mesh of one, raised by the installer", + "three machines join it across the household gateway", + "the mesh builds the shared base from source", + "the mesh builds a module standing on that base", + NEEDS, + "the anchor runs the module the mesh built", + "a joined machine runs the module the mesh built", +]) { + test(name, { skip, timeout: 60_000 }, () => { + assert.ok(steps.get(name)?.ok, report(name)); + }); +} diff --git a/test/integration/genesis-single.test.ts b/test/integration/genesis-single.test.ts index 7d02ff1..d27ca2f 100644 --- a/test/integration/genesis-single.test.ts +++ b/test/integration/genesis-single.test.ts @@ -12,7 +12,7 @@ * 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_KEEP=1 to leave it standing afterwards */ @@ -23,7 +23,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy } from "../../src/lifecycle/operate.ts"; import { bootstrapBinaryPath, hostBinaryPath } 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"; const SCENARIO = "genesis-single"; @@ -49,7 +49,7 @@ const skip = "bootstrap IMAGE=mesh-builder:development`)" : !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; @@ -75,12 +75,15 @@ before(async () => { installer: installer as string, catalogDir, // The template, not a bundle. The store and broker become the references mesh-catalog pins, - // and the machine pulls them over its uplink like any first node. mesh-control is left + // and the machine pulls them over its uplink like any first node. mesh-controller is left // naming a registry that does not exist — the installer overwrites it, and leaving it proves // that it does. - bundleTemplate: substrateBundle(bundle, []), + bundleTemplate: foundationBundle(bundle, []), source, sourceRef, + // Phase two builds from the same forge; the repositories sit beside the control plane's. + toolsSource: source.replace(/[^/]+\.git$/, "mesh-tools.git"), + catalogSource: source.replace(/[^/]+\.git$/, "mesh-catalog.git"), log: (m) => console.log(m), }); } catch (err) { diff --git a/test/integration/genesis.ts b/test/integration/genesis.ts index 44c988c..2793b52 100644 --- a/test/integration/genesis.ts +++ b/test/integration/genesis.ts @@ -12,10 +12,12 @@ * description of genesis, and both the single-node bed and the four-node bed call it. */ import { existsSync, writeFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts"; -import { placeBootstrap, BOOTSTRAP_PATH, HOST_PATH } from "../../src/lifecycle/place.ts"; +import { placeBootstrap, installHostService, BOOTSTRAP_PATH, HOST_PATH } from "../../src/lifecycle/place.ts"; +import { dirname } from "node:path"; /** What genesis did, or where it stopped. */ export interface GenesisResult { @@ -35,7 +37,7 @@ export interface GenesisOptions { /** A checkout of mesh-catalog's `modules/` on this workstation. */ catalogDir: string; /** - * The substrate TEMPLATE's content — not a bundle. The installer produces the bundle from it, + * The foundation TEMPLATE's content — not a bundle. The installer produces the bundle from it, * replacing the control plane's image with the id of the image it carries. */ bundleTemplate: string; @@ -53,6 +55,31 @@ export interface GenesisOptions { */ source: string; sourceRef: string; + /** + * Phase two: where the shared base and the catalogue's modules are built from. + * + * The installer no longer stops at a mesh that runs — it builds the base, a store, the + * catalogue, chooses the network and the filter, and asks a human where one must choose. This + * bed has no human, so every choice arrives as a flag, and a required choice with no flag is + * the installer refusing — which is the behaviour, not a lab problem. + */ + toolsSource: string; + catalogSource: string; + /** Where the shared library is built from — published before the base resolves it (ADR 0076). */ + sdkSource: string; + sdkRef?: string; + /** What of the base repo to build. Defaults to main; a feature branch under test overrides it. */ + toolsRef?: string; + /** The site the anchor is placed at on the private network. Must match how the operator/test + * places it afterwards, or the first re-place changes the overlay and recreates the control plane. */ + site?: string; + /** Supervise the host as a systemd service (the real install path) instead of --host-in-background, + * so it survives a reboot. Needs hostBinary to locate the packaging. */ + hostService?: boolean; + /** The built mesh-host binary on this workstation; its repo's packaging/ dir supplies the unit. */ + hostBinary?: string; + /** What of the catalogue to build. A branch under test is the usual reason this is not main. */ + catalogRef?: string; log?: (m: string) => void; } @@ -64,7 +91,7 @@ export function stepIn(said: string): string { export async function genesis(o: GenesisOptions): Promise { const node = o.node; const registry = o.registry ?? "127.0.0.1:5000"; - const modules = o.catalogueModules ?? ["registry", "mesh-control", "builder"]; + const modules = o.catalogueModules ?? ["distribution", "mesh-controller", "builder"]; const catalogueOnMachine = o.catalogueOnMachine ?? "/opt/mesh-catalog"; const log = o.log ?? (() => {}); @@ -98,30 +125,66 @@ export async function genesis(o: GenesisOptions): Promise { // The catalogue. `mesh-bootstrap --catalog` reads manifests from a CHECKOUT on the machine, // because at this moment the mesh has no forge, no build machine and — until the registry step // finishes — no registry. A manifest is a file, and somebody has to have put it there. - await must(`mkdir -p ${modules.map((m) => `${catalogueOnMachine}/modules/${m}`).join(" ")}`); + // + // **The WHOLE checkout, not the three genesis names.** `--catalog` is documented as a checkout + // of the catalogue repository, and phase two reads more from it than genesis does — postgres, + // mesh-catalog, the packet filter, whatever extras. Stocking only the bootstrap three left + // phase two unable to read a manifest that was never put there (novox/hq installer step 14). The + // production equivalent is an operator with a full checkout, or the installer cloning the repo; + // the lab stands in for that by copying the whole tree once. + const bundleTar = join(tmpdir(), `mesh-lab-catalogue-${process.pid}-${node}.tar`); + execFileSync("tar", ["-cf", bundleTar, "-C", o.catalogDir, "."]); + await must(`mkdir -p ${catalogueOnMachine}/modules`); + await push(o.instanceId, node, bundleTar, "/tmp/catalogue.tar"); + await must(`tar -xf /tmp/catalogue.tar -C ${catalogueOnMachine}/modules`); + // The three genesis itself needs must be present, or the pivot cannot even begin — checked here + // rather than discovered at step 8, where the message is about a missing file. for (const module of modules) { - const from = resolve(o.catalogDir, module, "module.json"); - if (!existsSync(from)) return stop("preparing the catalogue", `the catalogue has no ${module}/module.json at ${from}`); - await push(o.instanceId, node, from, `${catalogueOnMachine}/modules/${module}/module.json`); + const at = `${catalogueOnMachine}/modules/${module}/module.json`; + if (!(await on(`test -f ${at}`)).ok) { + return stop("preparing the catalogue", `the catalogue has no ${module}/module.json`); + } } - report.push(` catalogue ${modules.join(", ")} at ${catalogueOnMachine}`); + report.push(` catalogue full checkout at ${catalogueOnMachine} (${modules.join(", ")} + phase two)`); - const local = join(tmpdir(), `mesh-lab-substrate-${process.pid}-${node}.lock`); + const local = join(tmpdir(), `mesh-lab-foundation-${process.pid}-${node}.lock`); writeFileSync(local, o.bundleTemplate); - await push(o.instanceId, node, local, "/tmp/substrate-template.lock"); + await push(o.instanceId, node, local, "/tmp/foundation-template.lock"); + + // Supervise the host as a service (the real install path) when asked — the only way it survives a + // reboot. Installs the shipped packaging in the machine, then lets --host-service start+enable it. + if (o.hostService) { + if (!o.hostBinary) { + return stop("preparing the host service", "hostService needs hostBinary to find the packaging"); + } + await installHostService(name, node, join(dirname(o.hostBinary), "packaging"), (m) => log(`genesis:${m}`)); + report.push(` host supervised by nox-mesh-host.service`); + } const command = [ BOOTSTRAP_PATH, `--source ${o.source}`, `--source-ref ${o.sourceRef}`, - `--bundle /tmp/substrate-template.lock`, + `--bundle /tmp/foundation-template.lock`, `--catalog ${catalogueOnMachine}`, `--node ${node}`, `--registry ${registry}`, + `--tools-source ${o.toolsSource}`, + `--tools-ref ${o.toolsRef ?? "main"}`, + `--catalog-source ${o.catalogSource}`, + `--catalog-ref ${o.catalogRef ?? "main"}`, + `--sdk-source ${o.sdkSource}`, + `--sdk-ref ${o.sdkRef ?? "main"}`, + // The choices, answered the unattended way. Both have one option today, so these are + // redundant on purpose: the day a second filter exists, this bed keeps working instead of + // refusing, and choosing becomes a thing it visibly does. + `--site ${o.site ?? "main"}`, + `--private-network wireguard`, + `--packet-filter nftables`, `--host ${HOST_PATH}`, - // The lab has no unit to supervise the host with, and the installer refuses to invent one — a - // unit file is a packaging decision. A host started this way does not survive a reboot. - `--host-in-background`, + // A service when the packaging was installed above (survives a reboot); otherwise the + // background process, which does not — the installer refuses to invent a unit either way. + ...(o.hostService ? [] as string[] : [`--host-in-background`]), ].join(" "); // Run up to three times. Not to paper over a failure — every attempt's failing step is printed — @@ -149,9 +212,9 @@ export async function genesis(o: GenesisOptions): Promise { // ------------------------------------------------------------------------------------------ // 1. The control plane answers, asked of the PERMANENT container by name. - const answered = await on(`docker exec mesh-control /mesh-control status`, 60_000); + const answered = await on(`docker exec mesh-controller /mesh-controller status`, 60_000); report.push(` control plane ${answered.ok ? answered.out.split("\n")[0] : "NO ANSWER"}`); - if (!answered.ok) return stop("after the last step", `mesh-control does not answer:\n${answered.out}`); + if (!answered.ok) return stop("after the last step", `mesh-controller does not answer:\n${answered.out}`); // 2. The registry replies on /v2/. A container that is up is not a registry that serves. const v2 = await on(`curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://${registry}/v2/`); @@ -161,17 +224,17 @@ export async function genesis(o: GenesisOptions): Promise { // 3. THE PIVOT COMPLETED — the running control plane is pinned by a digest THIS MESH'S REGISTRY // assigned, not by an image id (ADR 0067 states this check in as many words). - const pinnedTo = (await on(`docker inspect --format '{{.Config.Image}}' mesh-control`)).out.trim(); + const pinnedTo = (await on(`docker inspect --format '{{.Config.Image}}' mesh-controller`)).out.trim(); report.push(` pinned to ${pinnedTo || "(nothing)"}`); if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) { return stop("after the last step", - `mesh-control is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` + + `mesh-controller is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` + `own configuration, which no registry ever served. The pivot did not happen, so this mesh ` + `cannot upgrade itself (novox/hq ADR 0067, "the pivot completed").`); } - if (!new RegExp(`^${registry.replaceAll(".", "\\.")}/mesh-control@sha256:[0-9a-f]{64}$`).test(pinnedTo)) { + if (!new RegExp(`^${registry.replaceAll(".", "\\.")}/mesh-controller@sha256:[0-9a-f]{64}$`).test(pinnedTo)) { return stop("after the last step", - `mesh-control is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` + + `mesh-controller is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` + `digest assigned by ${registry}.`); } @@ -186,34 +249,34 @@ export async function genesis(o: GenesisOptions): Promise { // installer that quietly built something else would satisfy a weaker check and raise a mesh // nobody asked for. const wanted = o.sourceRef.slice(0, 8); - if (!new RegExp(`built mesh-control from ${wanted}`).test(said)) { + if (!new RegExp(`built mesh-controller from ${wanted}`).test(said)) { return stop("after the last step", - `the installer never said it built mesh-control from ${wanted}. What runs may have been ` + + `the installer never said it built mesh-controller from ${wanted}. What runs may have been ` + `carried rather than made here, which is a mesh that cannot rebuild its own control plane. ` + `The installer said:\n${said.split("\n").filter((l) => /built|build/.test(l)).join("\n") || "(nothing about building)"}`); } - report.push(` built here mesh-control from ${wanted}, by the carried builder`); + report.push(` built here mesh-controller from ${wanted}, by the carried builder`); // 3b. And the registry really serves it. A reference is a claim; a tag list is the registry agreeing. - const tags = await on(`curl -s --max-time 10 http://${registry}/v2/mesh-control/tags/list`); + const tags = await on(`curl -s --max-time 10 http://${registry}/v2/mesh-controller/tags/list`); report.push(` registry holds ${tags.out.trim() || "nothing"}`); if (!tags.out.includes("genesis")) { return stop("after the last step", - `${registry} does not serve mesh-control, so the digest the container is pinned to names an ` + + `${registry} does not serve mesh-controller, so the digest the container is pinned to names an ` + `image nothing can pull: ${tags.out.trim()}`); } // 4. The temporary control plane is GONE. The name is the audit. - const temp = await on(`docker inspect --format '{{.State.Status}}' temp-mesh-control`); - report.push(` temp-mesh-control ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`); + const temp = await on(`docker inspect --format '{{.State.Status}}' temp-mesh-controller`); + report.push(` temp-mesh-controller ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`); if (temp.ok) { return stop("after the last step", - `temp-mesh-control is still ${temp.out.trim()}. Two control planes are consuming this mesh's ` + + `temp-mesh-controller is still ${temp.out.trim()}. Two control planes are consuming this mesh's ` + `broker queues; neither is wrong and the pivot is not finished.`); } // 5. And the mesh has heard from its one node. - const nodes = await on(`docker exec mesh-control /mesh-control node list`); + const nodes = await on(`docker exec mesh-controller /mesh-controller node list`); report.push(` node list ${nodes.out.trim().split("\n").join(" | ")}`); const line = nodes.out.split("\n").map((l) => l.trim()).find((l) => l.startsWith(`${node} `)); if (!line || !/^\S+\s+here\b/.test(line)) { diff --git a/test/integration/harness.ts b/test/integration/harness.ts index 8d82bc0..4cc9f99 100644 --- a/test/integration/harness.ts +++ b/test/integration/harness.ts @@ -17,19 +17,19 @@ import { duplicateAddresses, describeConflicts, type Held } from "../../src/life import type { Scenario } from "../../src/declaration/types.ts"; import { mustBeHandedOver, pinnedInto, referenceFor, repositoryOf, type HeldImage } from "../../src/pinning.ts"; -// --- the substrate bundle, and what its three images are on a real machine --------------------- +// --- the foundation bundle, and what its three images are on a real machine --------------------- /** * The example bundle in mesh-host names a registry that no longer exists. * - * `examples/substrate-first-node.lock` was written **for a target**, and the target was the lab: it + * `examples/foundation-first-node.lock` was written **for a target**, and the target was the lab: it * pins `192.0.2.250:5000/…` because that is where the registry the lab used to raise served from. * That registry is gone, so those three references name nothing. * * Two of them are ordinary third-party images and belong to the internet. Rather than invent * digests here, they are the ones the mesh's own modules already pin — mesh-catalog's `postgres` - * and `lavinmq` — so the substrate's store and broker are literally the images the mesh runs. The - * third, mesh-control, exists in no registry at all and becomes the ID the machine holds it under. + * and `lavinmq` — so the foundation's store and broker are literally the images the mesh runs. The + * third, mesh-controller, exists in no registry at all and becomes the ID the machine holds it under. * * **The bundle itself should be fixed in mesh-host**, and this substitution deleted with it. It is * here because the file lives in another repository and because a fixture that lies about where an @@ -41,13 +41,13 @@ const UPSTREAM_BROKER = "cloudamqp/lavinmq@sha256:3eb54c12916d700a978c2ea86e6362cd4974b0e3189508718006d4e6d341246b"; /** - * The substrate bundle as a machine should receive it. + * The foundation bundle as a machine should receive it. * * Third-party references become upstream ones, which the machine pulls over its uplink; ours * become the ID the machine was handed. Nothing points inside the scenario any more, which is the * whole of this change: what the bed proves about a bootstrap is now what would happen anywhere. */ -export function substrateBundle(path: string, held: HeldImage[]): string { +export function foundationBundle(path: string, held: HeldImage[]): string { let text = readFileSync(path, "utf8"); text = text.replaceAll(/[A-Za-z0-9_.:-]+\/postgres@sha256:[0-9a-f]{64}/g, UPSTREAM_STORE); text = text.replaceAll( diff --git a/test/integration/lavinmq-bed.test.ts b/test/integration/lavinmq-bed.test.ts index e2326d5..97a68a7 100644 --- a/test/integration/lavinmq-bed.test.ts +++ b/test/integration/lavinmq-bed.test.ts @@ -1,12 +1,12 @@ /** - * A lavinmq PROVIDER and a consumer of it ride one node while the substrate's own broker owns 5672 on + * A lavinmq PROVIDER and a consumer of it ride one node while the foundation's own broker owns 5672 on * another — the end-to-end proof that a module which needs a message queue gets its OWN broker. * * lavinmq is the mesh's control-plane broker AND a user-facing capability: a consumer that requires * `amqp` is given a scoped vhost + user on a lavinmq PROVIDER, not an account on the control broker - * (novox/hq ADR 0048). That makes it the two-node case, the twin of two-node-db: the substrate's + * (novox/hq ADR 0048). That makes it the two-node case, the twin of two-node-db: the foundation's * broker publishes 5672 on anchor, and a lavinmq provider must publish 5672 for its consumers to - * reach it — so the two cannot share a machine. `anchor` runs the substrate and nothing else; `laptop` + * reach it — so the two cannot share a machine. `anchor` runs the foundation and nothing else; `laptop` * runs the provider AND the amqp-ping consumer, co-located, and owns laptop's 5672 uncontested. * * The proof is layered: @@ -14,7 +14,7 @@ * config BEFORE the broker started (ADR 0052) — proven because the broker came up at all and is * gone from `docker ps -a` (a step, not a service); * - the lavinmq service and BOTH runtimes (bootstrap done, provisioner running) are up and stable; - * - each module got its own scoped broker account on the substrate broker (anchor), named for the + * - each module got its own scoped broker account on the foundation broker (anchor), named for the * node that runs it; * - the provisioner (in mesh-lavinmq on laptop) created the consumer's vhost AND user, both named * for the login the mesh derived — checked with `lavinmqctl` inside the broker; @@ -25,12 +25,12 @@ * its vhost — the provider named the vhost after the login — and nothing is hardcoded; the provider's * `serves` carries the port so the consumer references `${bound:amqp:port}`. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * * HELPER — stock the two runtimes into the local daemon before the run (some may already be there): * scripts/build-module-runtime.sh lavinmq /tmp/lavinmq.tar * scripts/build-module-runtime.sh amqp-ping /tmp/amqp-ping.tar - * cloudamqp/lavinmq:latest must be in the local daemon too (it is the substrate's own broker image); + * cloudamqp/lavinmq:latest must be in the local daemon too (it is the foundation's own broker image); * scenarios/lavinmq-bed.yml stocks all of them, and each node pulls what it runs by digest. */ @@ -41,7 +41,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -53,11 +53,11 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "lavinmq-bed"; -/** The node that carries the provider and its consumer. anchor carries only the substrate. */ +/** The node that carries the provider and its consumer. anchor carries only the foundation. */ const NODE = "laptop"; /** The login the mesh derives for the consumer: mesh__ (slug "ping"; ADR 0049). */ const CONSUMER_LOGIN = "mesh_laptop_ping"; @@ -86,7 +86,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane, a container on the first node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -95,9 +95,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -118,7 +118,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise { } | undefined; let said = ""; try { - const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`); + const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`); said = asked.out; if (asked.ok) state = JSON.parse(said); } catch (err) { @@ -157,11 +157,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) { @@ -178,7 +178,7 @@ after(async () => { await destroyAll(`${SCENARIO}-`); }, { timeout: 600_000 }); -test("the lavinmq provider and its consumer ride laptop while the substrate broker owns 5672 on anchor", { +test("the lavinmq provider and its consumer ride laptop while the foundation broker owns 5672 on anchor", { skip, timeout: 1_500_000, }, async () => { // ================================================================================================ @@ -287,7 +287,7 @@ test("the lavinmq provider and its consumer ride laptop while the substrate brok // --- add, issue a scoped broker account, assign to laptop -------------------------------------- async function addIssueAssign(name: string, manifest: string): Promise { - await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); const issued = await mesh(`module issue ${name} --node ${NODE}`); assert.match(issued, /broker account/, issued); @@ -311,14 +311,14 @@ test("the lavinmq provider and its consumer ride laptop while the substrate brok await settled(NODE); // ================================================================================================ - // THE two-node split — the substrate broker owns 5672 on anchor, the provider owns it on laptop. + // THE two-node split — the foundation broker owns 5672 on anchor, the provider owns it on laptop. // ================================================================================================ const onAnchor = await must("anchor", `docker ps --format '{{.Names}}'`); const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`); - assert.match(onAnchor, /(^|\n)mesh-broker(\n|$)/, "the substrate broker is not on the first node"); + assert.match(onAnchor, /(^|\n)mesh-broker(\n|$)/, "the foundation broker is not on the first node"); assert.doesNotMatch(onAnchor, /(^|\n)lavinmq(\n|$)/, - "the lavinmq provider landed on the substrate node — the 5672 collision this bed exists to avoid"); - assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the substrate store leaked onto the second node"); + "the lavinmq provider landed on the foundation node — the 5672 collision this bed exists to avoid"); + assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the foundation store leaked onto the second node"); assert.match(onLaptop, /(^|\n)lavinmq(\n|$)/, "the lavinmq provider is not on the second node"); // ================================================================================================ @@ -359,7 +359,7 @@ test("the lavinmq provider and its consumer ride laptop while the substrate brok } // ================================================================================================ - // Each module got its own scoped broker account on the substrate broker (anchor), named for the + // Each module got its own scoped broker account on the foundation broker (anchor), named for the // node that runs it and the module. // ================================================================================================ const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`); diff --git a/test/integration/local-model-bed.test.ts b/test/integration/local-model-bed.test.ts index 981734c..7a01fe0 100644 --- a/test/integration/local-model-bed.test.ts +++ b/test/integration/local-model-bed.test.ts @@ -15,7 +15,7 @@ * asserts the templated URL and that a request to it reaches the running server (ollama answers * /v1/models even with no model pulled — the wiring is what is proven, not a model's output). * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * No module runtime image is built — both modules are pure declaration. */ @@ -26,7 +26,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -38,7 +38,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "local-model-bed"; @@ -67,11 +67,11 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } async function meshTry(command: string): Promise<{ out: string; ok: boolean }> { - return on(`docker exec mesh-control /mesh-control ${command}`); + return on(`docker exec mesh-controller /mesh-controller ${command}`); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -80,7 +80,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -117,7 +117,7 @@ async function settled(withinMs = 600_000): Promise { } async function addAssign(name: string, manifest: string): Promise { - await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); await mesh(`module issue ${name} --node ${MACHINE}`); await mesh(`assign ${MACHINE} ${name}`); @@ -132,11 +132,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); diff --git a/test/integration/mesh-grant-end-to-end.test.ts b/test/integration/mesh-grant-end-to-end.test.ts index 0d96850..167aff9 100644 --- a/test/integration/mesh-grant-end-to-end.test.ts +++ b/test/integration/mesh-grant-end-to-end.test.ts @@ -11,7 +11,7 @@ * Mint on one side and create on the other agreeing, with no shared key and nothing placed by the * test, is the entire provider/consumer contract working as one thing. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which * scenarios/redis-node.yml stocks. */ @@ -23,7 +23,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -35,7 +35,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "redis-node"; @@ -64,7 +64,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -73,7 +73,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -86,7 +86,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -118,11 +118,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -201,12 +201,12 @@ test("the mesh grants a consumer redis's cache, and the credential it delivers a resources: [{ id: "state", type: "directory", path: "/var/lib/cacheuser", mode: "0700" }], }); - await must(`printf %s ${quote(redisManifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`); + await must(`printf %s ${quote(redisManifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`); await mesh("module add /redis.json"); await mesh(`module issue redis --node ${MACHINE}`); await mesh(`assign ${MACHINE} redis`); - await must(`printf %s ${quote(consumerManifest)} > /tmp/cacheuser.json && docker cp /tmp/cacheuser.json mesh-control:/cacheuser.json`); + await must(`printf %s ${quote(consumerManifest)} > /tmp/cacheuser.json && docker cp /tmp/cacheuser.json mesh-controller:/cacheuser.json`); await mesh("module add /cacheuser.json"); await mesh(`assign ${MACHINE} cacheuser`); diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index 5c249db..46487cd 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -6,10 +6,10 @@ * project keeps saying cannot be checked any other way (novox/hq ADR 0001: every fault of * 2026-08-22 was found in production because nothing could be stood up locally). * - * It needs a host binary and the substrate bundle: + * It needs a host binary and the foundation bundle: * * MESH_LAB_HOST_BINARY=.../mesh-host - * MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * * The bundle's image references are rewritten to the ones this scenario's own registry serves. * A digest belongs to whatever registry serves it, so a committed bundle names a registry that is @@ -24,7 +24,7 @@ import { raise } from "../../src/lifecycle/raise.ts"; import { pinnedInto, stillUnpinned, type HeldImage } from "../../src/pinning.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import { incus } from "../../src/incus/client.ts"; import { machineName } from "../../src/lifecycle/names.ts"; import { ready, returnTo, keep, rememberStock, warmStock } from "../../src/warm.ts"; @@ -36,7 +36,7 @@ const capability = await labIsUsable(); const binary = hostBinaryPath(); const bundle = process.env["MESH_LAB_BUNDLE"] ?? ""; const builder = process.env["MESH_LAB_BUILDER"] ?? ""; -/** mesh-control's `examples/modules`, so the manifests proven here are the ones that ship. */ +/** mesh-controller's `examples/modules`, so the manifests proven here are the ones that ship. */ const moduleExamples = process.env["MESH_LAB_MODULES"] ?? ""; const skip = !capability.usable @@ -44,7 +44,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "two-nodes"; @@ -105,7 +105,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane, which runs in a container on the first node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** @@ -144,7 +144,7 @@ async function settled(node: string, withinMs = 480_000): Promise { let said = ""; try { const asked = await on("anchor", - `docker exec mesh-control /mesh-control status --json`); + `docker exec mesh-controller /mesh-controller status --json`); said = asked.out; // Parsed inside the try on purpose: a truncated answer from a struggling machine is the // same fact as no answer, and the likeliest moment for one is exactly the machine this @@ -186,11 +186,11 @@ async function settled(node: string, withinMs = 480_000): Promise { * The bundle, as a machine should receive it. * * The committed example was written for a target that had a registry the lab raised. Its two - * third-party images become upstream references the machine pulls itself; mesh-control, which + * third-party images become upstream references the machine pulls itself; mesh-controller, which * exists in no registry, becomes the ID this machine was handed. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } /** Take a token out of what `token issue` printed. It is the one base64url blob on its own line. */ @@ -253,8 +253,8 @@ before(async () => { // because the control plane's image is named by the ID this machine holds it under, which is not // knowable until it has been handed over. held = raised.images; - await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`); + await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`); // A build machine, so anything here can ask the mesh to build something. Placed rather than // assumed: nothing else in this scenario would start one. @@ -289,7 +289,7 @@ after(async () => { test("a bare machine becomes a mesh", { skip, timeout: 600_000 }, async () => { const running = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const container of ["mesh-store", "mesh-broker", "mesh-control"]) { + for (const container of ["mesh-store", "mesh-broker", "mesh-controller"]) { assert.match(running, new RegExp(container), `${container} is not running`); } // Answering, not merely up. A container that is running is not a control plane that replies — @@ -334,8 +334,8 @@ test("a credential reaches both ends and the mesh holds neither", { skip, timeou `"content":"PGHOST=$\{bound:postgres-database:at\}\\nPGPORT=$\{bound:postgres-database:port\}\\n` + `PGUSER=$\{bound:postgres-database:as\}\\nPGPASSWORD=$\{secret:postgres-database\}\\n"}]}' ` + `> /tmp/app.json`); - await must("anchor", `docker cp /tmp/pg.json mesh-control:/pg.json`); - await must("anchor", `docker cp /tmp/app.json mesh-control:/app.json`); + await must("anchor", `docker cp /tmp/pg.json mesh-controller:/pg.json`); + await must("anchor", `docker cp /tmp/app.json mesh-controller:/app.json`); await mesh("module add /pg.json"); await mesh("module add /app.json"); @@ -423,7 +423,7 @@ test("when a machine cannot do what it was told, the mesh says which and why", { // is the situation `status` exists to distinguish from a machine that refused everything. await must("anchor", `printf %s '{"module":"impossible","version":"1","resources":[` + `{"id":"nothing","type":"package","package":"a-package-that-does-not-exist"}]}' > /tmp/imp.json`); - await must("anchor", `docker cp /tmp/imp.json mesh-control:/imp.json`); + await must("anchor", `docker cp /tmp/imp.json mesh-controller:/imp.json`); await mesh("module add /imp.json"); await mesh("assign laptop impossible"); await mesh("push laptop"); @@ -471,7 +471,7 @@ test("a declaration waits for a machine that is switched off", { skip, timeout: await must("anchor", `printf %s '{"module":"while-away","version":"1","resources":[` + `{"id":"note","type":"file","path":"/etc/mesh-while-away","content":"waited"}]}' > /tmp/away.json`); - await must("anchor", `docker cp /tmp/away.json mesh-control:/away.json`); + await must("anchor", `docker cp /tmp/away.json mesh-controller:/away.json`); await mesh("module add /away.json"); await mesh("assign laptop while-away"); await mesh("push laptop"); @@ -512,13 +512,13 @@ test("a declaration waits for a machine that is switched off", { skip, timeout: test("unassigning takes away exactly what it should", { skip, timeout: 900_000 }, async () => { // Removal is the half nobody tests. The mesh takes away what IT declared and no longer declares, // and never what the machine raised for itself from its bundle — which is the fault that - // destroyed a substrate once (novox/hq 04-ISSUES/010). + // destroyed a foundation once (novox/hq 04-ISSUES/010). // // Two modules, so the test can tell "removed the right one" from "removed everything". for (const [name, path] of [["kept", "/etc/mesh-kept"], ["going", "/etc/mesh-going"]] as const) { await must("anchor", `printf %s '{"module":"${name}","version":"1","resources":[` + `{"id":"note","type":"file","path":"${path}","content":"${name}"}]}' > /tmp/${name}.json`); - await must("anchor", `docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); await mesh(`assign anchor ${name}`); } @@ -536,11 +536,11 @@ test("unassigning takes away exactly what it should", { skip, timeout: 900_000 } assert.ok((await on("anchor", `test -f /etc/mesh-kept`)).ok, "unassigning one module took another one's file with it"); - // And the substrate this machine raised from its own bundle is untouched. It was not declared by + // And the foundation this machine raised from its own bundle is untouched. It was not declared by // the mesh, so the mesh must never remove it — the machine would take its own control plane // away, which is exactly what happened before origins existed. const running = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const container of ["mesh-store", "mesh-broker", "mesh-control"]) { + for (const container of ["mesh-store", "mesh-broker", "mesh-controller"]) { assert.match(running, new RegExp(container), `${container} was removed by a declaration that never declared it`); } @@ -565,7 +565,7 @@ test("a machine that fell behind catches up without being named", { skip, timeou await must("anchor", `printf %s '{"module":"fixable","version":"1","resources":[` + `{"id":"pkg","type":"package","package":"a-package-that-does-not-exist-yet"},` + `{"id":"note","type":"file","path":"/etc/mesh-fixable","content":"here"}]}' > /tmp/fix.json`); - await must("anchor", `docker cp /tmp/fix.json mesh-control:/fix.json`); + await must("anchor", `docker cp /tmp/fix.json mesh-controller:/fix.json`); await mesh("module add /fix.json"); await mesh("assign laptop fixable"); await mesh("push laptop"); @@ -585,7 +585,7 @@ test("a machine that fell behind catches up without being named", { skip, timeou // Fix the cause, the way somebody would: the module stops asking for the impossible thing. await must("anchor", `printf %s '{"module":"fixable","version":"1","resources":[` + `{"id":"note","type":"file","path":"/etc/mesh-fixable","content":"here"}]}' > /tmp/fix.json`); - await must("anchor", `docker cp /tmp/fix.json mesh-control:/fix.json`); + await must("anchor", `docker cp /tmp/fix.json mesh-controller:/fix.json`); await mesh("module add /fix.json"); // And nobody names the machine. @@ -626,7 +626,7 @@ test("the mesh runs its own artifact store", { skip, timeout: 900_000 }, async ( // and a builder will not start without an artifact store to publish to, so a mesh that has just // bootstrapped cannot build the module that gives it one. Adding the manifest directly is the // path a real first mesh has to take, so it is the path this walks. - await must("anchor", `docker cp /root/registry/module.json mesh-control:/registry.json`); + await must("anchor", `docker cp /root/registry/module.json mesh-controller:/registry.json`); await mesh("module add /registry.json"); await mesh("assign anchor registry"); await mesh("push anchor"); @@ -658,7 +658,7 @@ test("a machine serves its internal name with a certificate the mesh issued", { `"certificate":{"into":"/etc/mesh/serving.crt","authority":"/etc/mesh/authority.crt"},` + `"resources":[{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"}]}' ` + `> /tmp/served.json`); - await must("anchor", `docker cp /tmp/served.json mesh-control:/served.json`); + await must("anchor", `docker cp /tmp/served.json mesh-controller:/served.json`); await mesh("module add /served.json"); await mesh("assign anchor served"); await mesh("push anchor"); @@ -768,7 +768,7 @@ test("a machine filters exactly what its modules declared, and nothing else", { `{"id":"filter","type":"service","unit":"mesh-filter.service","state":"running",` + `"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`); for (const f of ["talker", "firewall"]) { - await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`); + await must("anchor", `docker cp /tmp/${f}.json mesh-controller:/${f}.json`); await mesh(`module add /${f}.json`); } await mesh("assign laptop talker"); @@ -972,7 +972,7 @@ test("rotating a credential moves both ends, and the old one stops working", { `"resources":[{"id":"dir","type":"directory","path":"/etc/realapp","mode":"0755"}]}' ` + `> /tmp/realapp.json`); for (const f of ["realstore", "realapp"]) { - await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`); + await must("anchor", `docker cp /tmp/${f}.json mesh-controller:/${f}.json`); await mesh(`module add /${f}.json`); } await mesh("assign anchor realstore"); @@ -1081,7 +1081,7 @@ test("a route is a grant: a workload is reached by the name it asked for", { `{"id":"app","type":"container","name":"storefront",` + `"image":"${ARTIFACT_STORE}","ports":["8088:5000"]}]}' > /tmp/storefront.json`); for (const f of ["frontdoor", "storefront"]) { - await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`); + await must("anchor", `docker cp /tmp/${f}.json mesh-controller:/${f}.json`); await mesh(`module add /${f}.json`); } await mesh("assign anchor frontdoor"); @@ -1135,7 +1135,7 @@ test("a route is a grant: a workload is reached by the name it asked for", { assert.ok(withdrawn, `the route outlived the module that asked for it:\n${after}\n\n` + `the machine did apply — this is what the mesh would send now:\n` + - `${(await on("anchor", `docker exec mesh-control /mesh-control plan anchor --files`)).out}`); + `${(await on("anchor", `docker exec mesh-controller /mesh-controller plan anchor --files`)).out}`); let gone = false; for (let i = 0; i < 15 && !gone; i++) { @@ -1159,7 +1159,7 @@ test("model access is answered by a record, and the key the mesh took is one it `"secrets":{"model-access":"/etc/assistant/key"},` + `"resources":[{"id":"dir","type":"directory","path":"/etc/assistant","mode":"0755"}]}' ` + `> /tmp/assistant.json`); - await must("anchor", `docker cp /tmp/assistant.json mesh-control:/assistant.json`); + await must("anchor", `docker cp /tmp/assistant.json mesh-controller:/assistant.json`); await mesh("module add /assistant.json"); // The licences first. With none recorded at all the honest answer is that nothing provides @@ -1171,7 +1171,7 @@ test("model access is answered by a record, and the key the mesh took is one it // then says the machine's set cannot be applied. The refusal names both candidates and the // command. ADR 0024 warns this will be felt — which is correct, and correct is not the same as // usable. - const refused = await on("anchor", `docker exec mesh-control /mesh-control assign laptop assistant`); + const refused = await on("anchor", `docker exec mesh-controller /mesh-controller assign laptop assistant`); assert.ok(!refused.ok, `a consumer was given model access without anybody saying which:\n${refused.out}`); for (const want of ["personal", "the-organisation", "licence use"]) { @@ -1182,7 +1182,7 @@ test("model access is answered by a record, and the key the mesh took is one it await mesh("licence use personal laptop assistant"); // Chosen, and still no key: the mesh has one thing to deliver and has not been given it. - const noKey = await on("anchor", `docker exec mesh-control /mesh-control plan laptop`); + const noKey = await on("anchor", `docker exec mesh-controller /mesh-controller plan laptop`); assert.ok(!noKey.ok, `a module was planned with a licence that has no key:\n${noKey.out}`); assert.match(noKey.out, /licence key personal/, noKey.out); @@ -1190,7 +1190,7 @@ test("model access is answered by a record, and the key the mesh took is one it // command line is a key in shell history and in every process listing taken while it ran. const secret = "sk-test-" + "0123456789abcdef".repeat(2); const accepted = await must("anchor", - `printf %s ${quote(secret)} | docker exec -i mesh-control /mesh-control licence key personal`); + `printf %s ${quote(secret)} | docker exec -i mesh-controller /mesh-controller licence key personal`); assert.match(accepted, /sealed to 1 holder/, accepted); assert.doesNotMatch(accepted, new RegExp(secret), "the key was echoed back, so the one copy that matters is on a terminal"); @@ -1298,11 +1298,11 @@ test("the board names the machine that is not doing what it was told", { await must("anchor", `printf %s '{"module":"board","version":"1",` + `"listens":[{"port":8090,"from":"mesh","why":"the board"}],` + `"resources":[]}' > /tmp/board.json`); - await must("anchor", `docker cp /tmp/board.json mesh-control:/board.json`); + await must("anchor", `docker cp /tmp/board.json mesh-controller:/board.json`); await mesh("module add /board.json"); // Served from the control plane's own container, reading the mesh on every request. - await must("anchor", `docker exec -d mesh-control /mesh-control board --listen 0.0.0.0:8090`); + await must("anchor", `docker exec -d mesh-controller /mesh-controller board --listen 0.0.0.0:8090`); await new Promise((r) => setTimeout(r, 3000)); const read = async (path: string) => @@ -1325,7 +1325,7 @@ test("the board names the machine that is not doing what it was told", { await must("anchor", `printf %s '{"module":"impossible","version":"1",` + `"resources":[{"id":"nowhere","type":"service","unit":"nothing-like-this.service",` + `"state":"running"}]}' > /tmp/impossible.json`); - await must("anchor", `docker cp /tmp/impossible.json mesh-control:/impossible.json`); + await must("anchor", `docker cp /tmp/impossible.json mesh-controller:/impossible.json`); await mesh("module add /impossible.json"); await mesh("assign laptop impossible"); await mesh("push laptop"); @@ -1389,7 +1389,7 @@ test("the hub can be filtered without severing the mesh", { `ExecStart=/usr/bin/nft -f ${rules}\\n[Install]\\nWantedBy=multi-user.target\\n"},` + `{"id":"filter","type":"service","unit":"hub-filter.service","state":"running",` + `"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/hubfilter.json`); - await must("anchor", `docker cp /tmp/hubfilter.json mesh-control:/hubfilter.json`); + await must("anchor", `docker cp /tmp/hubfilter.json mesh-controller:/hubfilter.json`); await mesh("module add /hubfilter.json"); await mesh("assign anchor hubfilter"); await mesh("push anchor"); @@ -1414,7 +1414,7 @@ test("the hub can be filtered without severing the mesh", { await must("anchor", `printf %s '{"module":"stillworks","version":"1",` + `"resources":[{"id":"marker","type":"file","path":"/etc/mesh-still-works",` + `"content":"yes","mode":"0644"}]}' > /tmp/stillworks.json`); - await must("anchor", `docker cp /tmp/stillworks.json mesh-control:/stillworks.json`); + await must("anchor", `docker cp /tmp/stillworks.json mesh-controller:/stillworks.json`); await mesh("module add /stillworks.json"); await mesh("assign laptop stillworks"); await mesh("push laptop"); @@ -1450,7 +1450,7 @@ test("a container reaches another machine by the name the mesh gave it", { `"capabilities":["container-runtime"],` + `"resources":[{"id":"idle","type":"container","name":"resolves",` + `"image":"${ARTIFACT_STORE}"}]}' > /tmp/resolves.json`); - await must("anchor", `docker cp /tmp/resolves.json mesh-control:/resolves.json`); + await must("anchor", `docker cp /tmp/resolves.json mesh-controller:/resolves.json`); await mesh("module add /resolves.json"); await mesh("assign laptop resolves"); await mesh("push laptop"); @@ -1539,7 +1539,7 @@ test("every name under a machine resolves to that machine", { }); test("a service is reached by a name under the machine it runs on", { - skip: skip || (!moduleExamples ? "set MESH_LAB_MODULES to mesh-control's examples/modules" : false), + skip: skip || (!moduleExamples ? "set MESH_LAB_MODULES to mesh-controller's examples/modules" : false), timeout: 900_000, }, async () => { // postgres.novox.internal, plex.ace.internal — the first label is the service and the rest is @@ -1555,7 +1555,7 @@ test("a service is reached by a name under the machine it runs on", { for (const name of ["dnsmasq", "resolved-split-dns"]) { const manifest = readFileSync(`${moduleExamples}/${name}.json`, "utf8"); await must("anchor", `cat > /tmp/${name}.json <<'MANIFEST'\n${manifest}\nMANIFEST`); - await must("anchor", `docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); } @@ -1700,18 +1700,18 @@ test("a third-party workload is adopted, with the credential it already had", { }, ], }))} > /umami.json`); - await must("anchor", `docker cp /umami.json mesh-control:/umami.json`); + await must("anchor", `docker cp /umami.json mesh-controller:/umami.json`); await mesh("module add /umami.json"); // **Accepted, not generated.** The value is what the database already answers to; the mesh // seals it and cannot read it again. Given whole, as the environment lines the containers read. await must("anchor", `printf %s ${quote(`POSTGRES_PASSWORD=${password}`)} | ` + - `docker exec -i mesh-control /mesh-control secret accept anchor umami database --from -`); + `docker exec -i mesh-controller /mesh-controller secret accept anchor umami database --from -`); await must("anchor", `printf %s ${quote( `DATABASE_URL=postgresql://umami:${password}@umami-db:5432/umami`)} | ` + - `docker exec -i mesh-control /mesh-control secret accept anchor umami app --from -`); + `docker exec -i mesh-controller /mesh-controller secret accept anchor umami app --from -`); await mesh("assign anchor umami"); await mesh("push anchor", 300_000); @@ -1815,7 +1815,7 @@ test("the real modules resolve together, and compose a declaration a host accept } planned.push(name); await must("anchor", `printf %s ${quote(pinned)} > /${name}.json`); - await must("anchor", `docker cp /${name}.json mesh-control:/${name}.json`); + await must("anchor", `docker cp /${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); } @@ -1932,7 +1932,7 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000 assert.deepEqual(stillUnpinned(pinned), [], `${name} still names an image nothing serves, so it could not start`); await must("anchor", `printf %s ${quote(pinned)} > /run-${name}.json`); - await must("anchor", `docker cp /run-${name}.json mesh-control:/run-${name}.json`); + await must("anchor", `docker cp /run-${name}.json mesh-controller:/run-${name}.json`); await mesh(`module add /run-${name}.json`); await mesh(`assign anchor ${name}`); } @@ -2018,7 +2018,7 @@ test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600 assert.deepEqual(stillUnpinned(pinned), [], "redis still names an image nothing serves, so it could not start"); await must("anchor", `printf %s ${quote(pinned)} > /run-redis.json`); - await must("anchor", `docker cp /run-redis.json mesh-control:/run-redis.json`); + await must("anchor", `docker cp /run-redis.json mesh-controller:/run-redis.json`); await mesh("module add /run-redis.json"); // A consumer with no container: what is under test is the credential's reach, and files on the @@ -2030,7 +2030,7 @@ test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600 `"secrets":{"redis-cache":"/var/lib/cachetest/cache.secret"},` + `"resources":[{"id":"state","type":"directory","path":"/var/lib/cachetest","mode":"0700"}]}' ` + `> /cachetest.json`); - await must("anchor", `docker cp /cachetest.json mesh-control:/cachetest.json`); + await must("anchor", `docker cp /cachetest.json mesh-controller:/cachetest.json`); await mesh("module add /cachetest.json"); await mesh("assign anchor redis"); diff --git a/test/integration/minio-grant-end-to-end.test.ts b/test/integration/minio-grant-end-to-end.test.ts index 1fae1de..34fd53e 100644 --- a/test/integration/minio-grant-end-to-end.test.ts +++ b/test/integration/minio-grant-end-to-end.test.ts @@ -9,7 +9,7 @@ * the consumer reaching its bucket with the access key and secret the mesh delivered it. Nothing is * placed by the test. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh minio builds mesh-runtime-minio:development (with mc), which * scenarios/minio-node.yml stocks. minio/minio:latest must be in the local daemon. */ @@ -21,7 +21,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -36,7 +36,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "minio-node"; @@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -74,7 +74,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -93,7 +93,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -125,11 +125,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -203,12 +203,12 @@ test("the mesh grants a consumer an S3 bucket, and the credential it delivers re resources: [{ id: "state", type: "directory", path: "/var/lib/bucketuser", mode: "0700" }], }); - await must(`printf %s ${quote(minioManifest)} > /tmp/minio.json && docker cp /tmp/minio.json mesh-control:/minio.json`); + await must(`printf %s ${quote(minioManifest)} > /tmp/minio.json && docker cp /tmp/minio.json mesh-controller:/minio.json`); await mesh("module add /minio.json"); await mesh(`module issue minio --node ${MACHINE}`); await mesh(`assign ${MACHINE} minio`); - await must(`printf %s ${quote(consumerManifest)} > /tmp/bucketuser.json && docker cp /tmp/bucketuser.json mesh-control:/bucketuser.json`); + await must(`printf %s ${quote(consumerManifest)} > /tmp/bucketuser.json && docker cp /tmp/bucketuser.json mesh-controller:/bucketuser.json`); await mesh("module add /bucketuser.json"); await mesh(`assign ${MACHINE} bucketuser`); diff --git a/test/integration/objectstore.test.ts b/test/integration/objectstore.test.ts index 337025a..1b08379 100644 --- a/test/integration/objectstore.test.ts +++ b/test/integration/objectstore.test.ts @@ -33,7 +33,7 @@ const skip = !capability.usable ? `lab not usable: ${capability.why}` : !provisioner ? "set MESH_LAB_OBJECTSTORE_PROVISIONER to a built provisioner " + - "(mesh-control: go build ./examples/objectstore-provisioner)" + "(mesh-controller: go build ./examples/objectstore-provisioner)" : false; const SCENARIO = "an-object-store"; diff --git a/test/integration/one-node-mesh.test.ts b/test/integration/one-node-mesh.test.ts new file mode 100644 index 0000000..6fd0c81 --- /dev/null +++ b/test/integration/one-node-mesh.test.ts @@ -0,0 +1,1233 @@ +/** + * FOUR FRESH MACHINES, AND NOTHING HANDED TO THEM. + * + * The four-machine bed (`whole-mesh-full`) proves the mesh converges. It does so by loading + * thirty-four of the mesh's own images onto its machines from the workstation, because it does not + * build them — something beside the bed built them and copied them in. No real installation looks + * like that, and the lab has been burned by exactly this shape before: it used to raise a registry + * inside the scenario, and a bootstrap that only worked against that registry went green here and + * would have failed on any bare machine. + * + * This bed hands over nothing. The scenario has no `images:` list at all. What the machines get is + * a container runtime and the host binary — prerequisites of a machine, not parts of a mesh — and + * from there: + * + * 1. novox is raised into a mesh of one by the installer, which BUILDS the control plane. + * 2. ace, shanks and g14 JOIN it, across a household NAT, with a token and nothing else. + * 3. The mesh builds the shared base from source, with its own builder. + * 4. The mesh builds a real module standing on that base. + * 5. The anchor runs it, pinned to a digest the mesh's own registry assigned. + * 6. A JOINED machine runs it — which means pulling from a registry that asks who it is. + * + * Steps 1 and 2 are proven elsewhere and are here because the later ones need them. **Steps 3 + * through 6 are what this bed exists for**, and 6 is the one nothing has ever checked: genesis + * puts the builder, the registry and everything they produce on ONE machine, so every earlier + * proof of a mesh-built module running is a proof about the machine that built it. A second + * machine has to fetch, and fetching needs an account nothing yet grants (novox/hq issue 042). + * + * Each step is recorded separately rather than allowed to throw, so a gap at 6 reports as a gap at + * 6 instead of erasing the evidence for 3, 4 and 5. + * + * 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/foundation-first-node.lock + * MESH_LAB_CATALOG=.../mesh-catalog/modules + * MESH_LAB_SOURCE=/mesh-controller.git MESH_LAB_SOURCE_REF= + * MESH_LAB_KEEP=1 to leave it standing afterwards + */ +import { test, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { resolve } from "node:path"; +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, 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"; +import { waitUntilAllUsable } from "../../src/lifecycle/ready.ts"; + +const SCENARIO = "one-node-mesh"; +const CONTROL = "anchor"; + +/** The anchor's public address — what every other machine dials, and what its own token must name. */ +const ANCHOR = "192.0.2.10"; +/** Where this mesh's registry answers, on the anchor's public address so a joined node can reach it. */ +const REGISTRY = `${ANCHOR}:5000`; + +/** + * The module built on top of the base, and the base it stands on. + * + * `amqp-ping` is deliberately small and deliberately REAL: its own TypeScript, compiled by the + * shared toolchain, running on the shared runtime, talking to the broker. A module whose artifact + * is a mirrored public image would pass every assertion below while skipping the whole of what is + * under test (novox/hq SELF-UPGRADE-PLAN, rule 1). + */ +const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" }; +/** + * 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 + * provider in the graph, and this is it. No build: its image is upstream. + */ +const PROVIDER = { module: "lavinmq", repo: "mesh-catalog", path: "modules/lavinmq", container: "mesh-lavinmq" }; +/** + * The store, and the catalogue that cannot exist without it. + * + * Both were missing from this test entirely, and nothing complained, because nothing asked. A mesh + * with no catalogue holds no module graph — it cannot say what it has, what a module is made of, + * what a change reaches, or what must be rebuilt. It ran anyway, which is the point: "the mesh is + * up" was being read off genesis finishing. + */ +/** The one word that puts a mesh on a private network and gives its machines names. */ +const NETWORK_MODULE = "networking"; +/** + * The packet filter, which is a module too and was assigned to nothing. + * + * The rules are generated from what every module declares it listens on, so a mesh with no filter + * is not "open by accident" — it is a mesh where the whole of that generation has never run. It + * claims a seat (`the-packet-filter`) because a machine has one of these and two things writing + * rules is a coin toss about which survives. + */ +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-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. + * + * Written down as a list rather than checked one step at a time, because "is this a mesh" is a + * question about the set. The three the build loop cannot produce for itself — the control plane, + * 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-controller", "distribution", "builder", "mesh-tools", "postgres", + "mesh-catalog", "lavinmq", "amqp-ping"]; +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 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"; +const ENROLLED = "the machine is enrolled, and an agent is running on it"; +const HAS_BUILDER = "the builder is installed as a module, with an account"; +const BASE_BUILT = "the mesh builds the shared base from source"; +const STORE_RUNS = "the mesh builds and runs a store of its own"; +const CATALOGUE_RUNS = "the mesh builds and runs its own catalogue"; +const CONTROL_REBUILT = "the mesh rebuilds its own control plane from source"; +const NETWORKED = "the mesh puts itself on a private network, and its machine has a name"; +const FILTERED = "the machine has a packet filter, loaded from what modules declared"; +const MODULE_BUILT = "the mesh builds a module standing on that base"; +const ANCHOR_RUNS = "the anchor runs the module the mesh built"; +const DESCRIBES = "the control plane can describe the mesh, and what it says is true"; +const CATALOGUED = "the catalogue holds every module this mesh built"; +const NETWORK = "the machine's networking is what the modules asked for"; +const DECLARED = "every resource the mesh declared is true on the machine"; +const FOLLOWS = "a change to a module's source reaches the machine on its own"; +const STORE_UPGRADES = "the store is upgraded in place, and the controller reads it through the window"; +const BROKER_UPGRADES = "the broker is upgraded in place, and the mesh talks over the window"; +const SURVIVES = "the mesh comes back after the machine reboots"; +const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping", container: "amqp-ping" }; + +const capability = await labIsUsable(); +const binary = hostBinaryPath(); +const installer = bootstrapBinaryPath(); +const bundle = process.env["MESH_LAB_BUNDLE"] ?? ""; +const catalogDir = process.env["MESH_LAB_CATALOG"] ?? ""; +const source = process.env["MESH_LAB_SOURCE"] ?? ""; +const sourceRef = process.env["MESH_LAB_SOURCE_REF"] ?? ""; +const KEEP = !!process.env["MESH_LAB_KEEP"]; +const FIXED_ID = process.env["MESH_LAB_INSTANCE_ID"] ?? (KEEP ? "fresh-mesh-live" : undefined); + +/** + * Where a repository other than the control plane's lives. + * + * Derived from `MESH_LAB_SOURCE` by swapping the last path segment, because every one of these + * repositories sits beside the others under the same owner on the same forge. Overridable, so a + * forge that is arranged differently does not need this bed edited. + */ +function forgeUrl(repo: string): string { + const override = process.env[`MESH_LAB_SOURCE_${repo.toUpperCase().replaceAll("-", "_")}`]; + if (override) return override; + return source.replace(/[^/]+\.git$/, `${repo}.git`); +} +/** + * What to build, per repository. + * + * A branch is acceptable for an ordinary build; only genesis insists on a commit (ADR 0071). Per + * repository rather than one value for all of them, because a change under test usually lives in + * one repository and the rest should be built from what everyone else has — building them all from + * a feature branch would prove that branch against itself. + * + * MESH_LAB_BUILD_REF the default for every repository + * MESH_LAB_BUILD_REF_MESH_CATALOG ...overridden for one + */ +function refFor(repo: string): string { + const override = process.env[`MESH_LAB_BUILD_REF_${repo.toUpperCase().replaceAll("-", "_")}`]; + return override ?? process.env["MESH_LAB_BUILD_REF"] ?? "main"; +} + +/** + * The shared base's manifest, on this workstation. + * + * The base is a repository with a manifest at its root (novox/hq ADR 0069), so unlike the + * catalogue's modules it is not under `MESH_LAB_CATALOG`. Derived from that path on the convention + * that the checkouts sit beside each other, and overridable for a layout where they do not. + */ +const baseManifest = process.env["MESH_LAB_BASE_MANIFEST"] ?? + resolve(catalogDir, "..", "..", BASE.repo, "module.json"); + +const skip = + !capability.usable ? capability.why : + !binary ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : + !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 foundation template" : + !catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" : + false; + +let instanceId = ""; +let raised: GenesisResult; + +// ---- talking to the machines ------------------------------------------------------------------ + +function quote(s: string): string { + return `'${s.replaceAll("'", `'\\''`)}'`; +} +async function on(machine: string, command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> { + const { stdout } = await exec(instanceId, machine, [ + "sh", "-c", `exec 2>&1\n${command}\necho "__exit=$?"`, + ], timeoutMs); + const marker = stdout.lastIndexOf("__exit="); + if (marker < 0) return { out: stdout, ok: false }; + return { out: stdout.slice(0, marker), ok: stdout.slice(marker + 7).trim() === "0" }; +} +async function must(machine: string, command: string, timeoutMs?: number): Promise { + const { out, ok } = await on(machine, command, timeoutMs); + if (!ok) throw new Error(`${machine}: ${command}\n${out}`); + return out; +} +async function mesh(command: string, timeoutMs?: number): Promise { + // 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-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-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-controller /mesh-controller ${command}\n${out}`); + } +} + +/** + * Stop the machine and start it again, the way a power cut or a kernel upgrade would. + * + * There is no restart in the lab's own vocabulary, which is its own small finding: nothing had ever + * needed one, because nothing had ever asked whether a mesh comes back. + */ +async function restartMachine(machine: string): Promise { + const name = await instanceNameOf(instanceId, machine); + await incus(["restart", name], 180_000); + await waitUntilAllUsable([name], 300, (m) => console.log(` restart: ${m}`)); +} + +/** A module the mesh has to make for itself: where its source is, and what it runs when it works. */ +interface CoreModule { module: string; repo: string; path: string; container: string } + +/** + * Build a module from source, install it, and wait for it to actually run. + * + * The whole sequence a module goes through, in one place because the mesh has to do it for several + * before it is a mesh at all: register the manifest, build it from its repository and path, issue + * the broker account its runtime needs, assign it, send it, and then ask the machine whether the + * container is up. + * + * **The account is not optional and is not swallowed.** A module's runtime is a tool host: it + * connects to the mesh's broker before doing anything. Without an account the mesh still fills the + * secret the module declares it owns — with a generated value — so the container starts, fails to + * parse a password as a credential document, and loops on a JSON syntax error mentioning no + * missing account. That cost a step that reported PASS. + */ +async function bringUp(m: CoreModule): Promise { + await registerModule(m.module, resolve(catalogDir, m.module, "module.json")); + const built = await mesh( + `build ${forgeUrl(m.repo)} --path ${m.path} --ref ${refFor(m.repo)} --wait 1200s`, 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + await mesh(`module issue ${m.module} --node ${CONTROL}`); + await mesh(`assign ${CONTROL} ${m.module}`); + await mesh(`push ${CONTROL}`, 600_000); + return `${built}\n${await waitForContainer(CONTROL, m.container)}`; +} + +/** + * Wait for one container, BY NAME, to be running. + * + * **Named exactly, because substring matching passed a step that had failed.** Waiting for "a + * container whose name contains lavinmq" was satisfied by the broker — `lavinmq`, up and healthy — + * while the thing actually under test, the module's own runtime `mesh-lavinmq`, was crash-looping + * beside it. The step went green and the fault was found by reading `docker ps` by hand. + */ +async function waitForContainer(node: string, container: string, seconds = 200): Promise { + const deadline = Date.now() + seconds * 1_000; + let last = ""; + while (Date.now() < deadline) { + const ps = (await on(node, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out; + last = ps; + const line = ps.split("\n").find((l) => l.split("\t")[0]?.trim() === container); + if (line && /^Up /.test(line.split("\t")[1]?.trim() ?? "")) return ps; + await new Promise((r) => setTimeout(r, 5_000)); + } + const logs = (await on(node, `docker logs --tail 15 ${container} 2>&1`)).out; + throw new Error( + `${container} is not running on ${node}.\n\ncontainers:\n${last}\n\nwhat it said:\n${logs}`); +} + +/** + * Register a module from a manifest on this workstation. + * + * **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-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` + * to copy into — `docker cp` says so in those words. `/` is the one directory every image has. + */ +async function registerModule(module: string, manifest: string): Promise { + assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`); + const onMachine = `/tmp/${module}.json`; + const inContainer = `/${module}.json`; + await push(instanceId, CONTROL, manifest, onMachine); + await must(CONTROL, `docker cp ${onMachine} mesh-controller:${inContainer}`); + return mesh(`module add ${inContainer}`); +} +function tokenFrom(said: string): string { + const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" ")); + assert.ok(found, `no token in:\n${said}`); + return found; +} + +// ---- steps, recorded rather than thrown -------------------------------------------------------- + +interface Step { + code: string; title: string; ok: boolean; why: string; said: string; seconds: number; +} +const steps = new Map(); +const order: string[] = []; + +/** + * Run one step of the plan, remember what it said, and never throw. + * + * **Each step carries a code, and the code is the point.** A step used to be identified by its own + * sentence, so "which one failed" meant reading prose, and rewording a step silently made it a + * different step with no history. A code is stable, sorts, and can be pointed at: "V1 failed" says + * something that a whole sentence does not. + * + * A step whose dependency did not succeed is not attempted — its answer would be meaningless and + * its failure would be attributed to the wrong cause. The run still continues to the end, so the + * report says what was established and what was never asked, which are different things. + */ +async function step( + code: string, title: string, needs: string | null, fn: () => Promise, +): Promise { + order.push(title); + if (needs && !steps.get(needs)?.ok) { + steps.set(title, { code, title, ok: false, seconds: 0, said: "", + why: `not attempted — ${steps.get(needs)?.code ?? "?"} (${needs}) did not succeed` }); + console.log(`[${code}] SKIP ${title}`); + return; + } + console.log(`\n[${code}] ---- ${title} ----`); + const began = Date.now(); + const took = () => Math.round((Date.now() - began) / 1000); + try { + const said = await fn(); + steps.set(title, { code, title, ok: true, why: "", said, seconds: took() }); + console.log(`[${code}] PASS ${title} (${took()}s)`); + } catch (err) { + const why = (err as Error).message; + steps.set(title, { code, title, ok: false, why, said: "", seconds: took() }); + console.log(`[${code}] FAIL ${title} (${took()}s)\n${why.split("\n").slice(0, 25).join("\n")}`); + } +} + +function report(name: string): string { + const s = steps.get(name); + if (!s) return `${name}: never ran`; + const lines = order.map((n) => { + const it = steps.get(n); + return ` ${it?.ok ? "PASS" : "FAIL"} ${n}`; + }); + return `${s.why}\n\nWhere this bed got to:\n${lines.join("\n")}`; +} + + +/** + * The plan, as data. + * + * Stated before any of it is attempted, so a reader knows what the run is trying to establish + * rather than inferring it from whatever happens to be printed. Codes are stable; titles may be + * reworded without the step losing its identity. + * + * Five phases, and the order is the argument: a mesh is RAISED, then it must be able to PRODUCE, + * then something is USED on it, then it is asked to describe itself and its networking is + * VERIFIED, and finally it has to ENDURE — a change following on its own, and coming back after + * the machine stops. + */ +const PLAN: { code: string; title: string }[] = [ + { code: "R1", title: GENESIS }, + { code: "R2", title: FOUNDATION }, + { code: "R3", title: BUILT_CP }, + { code: "R4", title: PIVOTED }, + { code: "R5", title: HAS_REGISTRY }, + { code: "R6", title: ENROLLED }, + { code: "R7", title: HAS_BUILDER }, + { code: "P1", title: BASE_BUILT }, + { code: "P2", title: STORE_RUNS }, + { code: "P3", title: CATALOGUE_RUNS }, + { code: "P4", title: CONTROL_REBUILT }, + { code: "N1", title: NETWORKED }, + { code: "N2", title: FILTERED }, + { code: "U1", title: MODULE_BUILT }, + { code: "U2", title: NEEDS }, + { code: "U3", title: ANCHOR_RUNS }, + { code: "V1", title: DESCRIBES }, + { code: "V2", title: CATALOGUED }, + { code: "V3", title: NETWORK }, + { code: "V4", title: DECLARED }, + { code: "E1", title: FOLLOWS }, + { code: "S1", title: STORE_UPGRADES }, + { code: "S2", title: BROKER_UPGRADES }, + { code: "E2", title: SURVIVES }, +]; + +/** What this run intends to establish, said before any of it is attempted. */ +function statePlan(): void { + console.log(`\n================ THE PLAN ================`); + for (const s of PLAN) console.log(` ${s.code.padEnd(3)} ${s.title}`); + console.log(` ${PLAN.length} steps. A step whose dependency fails is not attempted.\n`); +} + +/** The run's verdict: a table, and a file something other than a person can read. */ +function stateOutcome(): void { + console.log(`\n================ WHAT THIS MESH DID FOR ITSELF ================`); + let established = 0; + for (const title of order) { + const s = steps.get(title); + if (!s) continue; + if (s.ok) established++; + const mark = s.ok ? "PASS" : s.why.startsWith("not attempted") ? "SKIP" : "FAIL"; + console.log(` ${s.code.padEnd(3)} ${mark} ${title}${s.seconds ? ` (${s.seconds}s)` : ""}`); + } + console.log(` ${established}/${PLAN.length} established.`); + const where = process.env["MESH_LAB_REPORT"] ?? "one-node-mesh-report.json"; + try { + writeFileSync(where, JSON.stringify({ scenario: SCENARIO, established, of: PLAN.length, + steps: PLAN.map(({ code, title }) => { + const s = steps.get(title); + return { code, title, status: !s ? "never-ran" + : s.ok ? "pass" : s.why.startsWith("not attempted") ? "skip" : "fail", + seconds: s?.seconds ?? 0, why: s?.ok ? "" : s?.why ?? "" }; + }) }, null, 2)); + console.log(` report written to ${where}`); + } catch { /* a report that cannot be written must not fail a run that passed */ } +} + +before(async () => { + if (skip) return; + statePlan(); + + const bed = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), { + onProgress: (m) => console.log(`raise: ${m}`), + ...(FIXED_ID ? { instanceId: FIXED_ID } : {}), + }); + instanceId = bed.instanceId; + console.log(`INSTANCE ${instanceId}${KEEP ? " (KEEP — will be left standing)" : ""}`); + console.log(`NOTHING WAS LOADED: this scenario names no images. Every image on every machine ` + + `below was pulled from the internet or built by the mesh.`); + + // ---- 1. GENESIS ----------------------------------------------------------------------------- + // + // The shared description, the same one `genesis-single` calls. The bundle is the TEMPLATE with + // nothing held: no image is pre-resolved, because none is here to resolve to. + await step("R1", GENESIS, null, async () => { + try { + raised = await genesis({ + instanceId, + node: CONTROL, + installer: installer as string, + catalogDir, + // The broker's advertised address, corrected. + // + // The template hardcodes 192.0.2.10:5671 — the address of the anchor in the single-machine + // bed. A token carries this verbatim as the endpoint an enrolling node dials, so on a mesh + // 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: foundationBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`), + registry: REGISTRY, + source, + sourceRef, + toolsSource: forgeUrl(BASE.repo), + toolsRef: refFor(BASE.repo), + catalogSource: forgeUrl(MODULE.repo), + catalogRef: refFor(MODULE.repo), + sdkSource: forgeUrl("mesh-sdk"), + sdkRef: refFor("mesh-sdk"), + // The same site N1 re-places the anchor at, so N1 is a no-op and the control plane is not + // recreated mid-test (its --add-host would otherwise change). + site: "hosting", + // Supervise the host as a service so it survives the reboot check (E2). + hostService: true, + hostBinary: binary, + log: (m) => console.log(m), + }); + } catch (err) { + throw new Error(`the installer never ran: ${(err as Error).message}`); + } + if (!raised.ok) throw new Error(`${raised.step || "no step named"}: ${raised.why}\n\n${raised.report.join("\n")}`); + return raised.report.join("\n"); + }); + + // ---- WHAT GENESIS CLAIMED, ASKED OF THE MACHINE ---------------------------------------------- + // + // **Genesis is twelve steps and was reported as one line.** All the work of raising a mesh + // happens inside it, so "genesis failed" said nothing about which of its claims broke, and + // "genesis passed" was one tick standing in for six separate things being true. + // + // Each is asked of the machine rather than read from the installer's own output — the installer + // saying it published an image and the registry serving one are different facts, and it is the + // second that matters. + + 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; + }); + + // **The whole reason the installer carries a builder.** A carried control-plane image would + // satisfy "a control plane is running" equally well, which is what makes this worth asserting: + // 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-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/"), + `the control plane runs an image no registry of this mesh served: ${image}`); + return image; + }); + + await step("R4", PIVOTED, BUILT_CP, async () => { + const ps = (await on(CONTROL, `docker ps -a --format '{{.Names}}'`)).out; + 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, 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-controller/, + `the registry serves no mesh-controller, so nothing was published into it:\n${held}`); + return held.trim(); + }); + + await step("R6", ENROLLED, GENESIS, async () => { + const nodes = await mesh("node list"); + assert.match(nodes, new RegExp(`^${CONTROL}\\b`, "m"), + `the mesh has not heard from the machine it is running on:\n${nodes}`); + // Heard from once is not an agent running, and the difference is the whole of joining. + const agent = (await on(CONTROL, `pgrep -af '[m]esh-host run' | head -3`)).out.trim(); + assert.ok(agent, `no host agent is running, so nothing would apply what the mesh sends`); + return `${nodes.trim()}\n${agent}`; + }); + + await step("R7", HAS_BUILDER, HAS_REGISTRY, async () => { + await waitForContainer(CONTROL, "mesh-builder", 120); + const modules = await mesh("module list"); + assert.match(modules, /^builder\b/m, + `the builder is running but the mesh holds no record of it as a module:\n${modules}`); + return modules; + }); + + // ---- 2..6. THE MESH BECOMES ONE -------------------------------------------------------------- + // + // **Joining used to be here, second, and that was the wrong order.** Three machines were enrolled + // into a mesh that could not yet produce a single module, and the step was reported as though + // something had been shown. They do join — reliably — but joining a mesh that can build nothing + // proves only that enrolment works, which was never the doubtful part. + // + // `17-raising-a-mesh` says it plainly: genesis ends with a mesh of one that RUNS, and that is not + // the same as a mesh that WORKS; what remains after the core modules are built is "adding + // machines, and deciding what they run". So everything a mesh needs to be a mesh happens first, + // and machines arrive at the end. + + // The toolchain and runtime every module with code of its own stands on. It is a module, and it + // is built like one — cloned from the forge by the builder installing put here, compiled on the + // machine, published into the mesh's own registry. + await step("P1", BASE_BUILT, HAS_BUILDER, async () => { + // Registered from the manifest the builder will also read, so what the mesh holds and what it + // builds are the same description of the same module. + // + // **Not swallowed.** This call used to end in `.catch(() => {})`, on the reasoning that the + // base might already be known. It hid a real failure — the manifest was being named at a path + // inside a container that had never seen it — and the step passed anyway, because a base with + // nothing to stand on builds whether or not the mesh has a record of it. + await registerModule(BASE.module, baseManifest); + const built = await mesh( + `build ${forgeUrl(BASE.repo)} --ref ${refFor(BASE.repo)} --wait 1200s`, 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + return built; + }); + + // 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. + await step("P2", STORE_RUNS, BASE_BUILT, () => bringUp(STORE)); + + // And the catalogue, which was missing from this test altogether. + // + // Without it the mesh holds no module graph: it cannot say what it has, what a module is made + // of, what a change to one reaches, or what must be rebuilt. A mesh in that state still runs, + // which is exactly how its absence went unnoticed — "the mesh is up" was being read off the + // installer finishing rather than off the mesh being able to answer anything. + await step("P3", CATALOGUE_RUNS, STORE_RUNS, () => bringUp(CATALOGUE)); + + // The control plane, rebuilt from its own repository and rolled out. + // + // The installer built it once, which is what got the mesh running. Building it again THROUGH THE + // MODULE PATH — build, notice the version moved, roll it out — is a different claim: it is the + // moment the mesh stops depending on the installer for anything, and the first time the thing + // that performs an upgrade performs one on itself. + await step("P4", CONTROL_REBUILT, CATALOGUE_RUNS, async () => { + const built = await mesh( + `build ${forgeUrl(CONTROL_PLANE.repo)} --ref ${sourceRef} --wait 1200s`, 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + const rolled = await mesh(`upgrade ${CONTROL_PLANE.module} roll-out`, 900_000); + // Asked of the machine rather than believed from the command: the control plane that answers + // afterwards is the one that has to be running for anything below this line to mean anything. + await waitForContainer(CONTROL, CONTROL_PLANE.container); + return `${built}\n${rolled}`; + }); + + // ---- THE MESH'S OWN NETWORKING --------------------------------------------------------------- + // + // **Four modules the control plane computes were assigned to nothing, and nothing complained.** + // `networking`, `mesh-wireguard`, `mesh-names` and `mesh-resolver` all existed as records that + // had never been placed on a machine — so no names were written, no private network was raised, + // and `/etc/hosts` held nothing. A module is a definition until it is assigned; being generated + // by the control plane does not place it. + // + // One word, by design: `networking` has no files of its own and is requirements only, so + // assigning it finds one answer to each and takes them. The day the catalogue holds a second VPN + // there are two answers, the mesh refuses and names both, and choosing is assigning the one you + // want. + await step("N1", NETWORKED, CONTROL_REBUILT, async () => { + await mesh(`assign ${CONTROL} ${NETWORK_MODULE}`); + // **Assigned is not placed, and they are two different acts.** Assigning installs the module + // that answers "how do machines reach each other"; placing says where THIS machine is on the + // resulting network. Without the second the module runs and writes a names file with no names + // in it — deliberately, because "a node with no address on the network has no name here", and a + // name resolving to nothing is worse than no name: a connection to an address that does not + // answer hangs, where a name that does not resolve fails at once and says so. + await mesh(`overlay place ${CONTROL} --hub --endpoint ${ANCHOR}:51820 --site hosting`); + await mesh(`push ${CONTROL}`, 600_000); + // What it was assigned for. A machine on a private network with no name on it has had the + // harder half done and the visible half not. + const deadline = Date.now() + 120_000; + let hosts = ""; + while (Date.now() < deadline) { + hosts = (await on(CONTROL, `cat /etc/hosts`)).out; + if (/\.internal/.test(hosts)) break; + await new Promise((r) => setTimeout(r, 5_000)); + } + assert.match(hosts, /\.internal/, + `${NETWORK_MODULE} is assigned and no machine has a name:\n${hosts}`); + const modules = await mesh("module list"); + return `${hosts.trim()}\n\n${modules.trim()}`; + }); + + // ---- THE PACKET FILTER ------------------------------------------------------------------------- + // + // Assigned separately from `networking` because they answer different questions: one is how + // machines reach each other, the other is what may reach this one. Both were assigned to nothing, + // and the second is the more alarming of the two — every rule the mesh generates from module + // declarations had never been applied to any machine in this test. + await step("N2", FILTERED, NETWORKED, async () => { + // **Registered first: the mesh had never heard of it.** The networking family is computed by + // the control plane, so the mesh knows those modules exist without anyone saying so. The + // firewall is an ordinary catalogue module and needs adding like any other — "no module of + // that name: firewall" — which is a second way for a module to be absent, and less obvious + // than being present and placed nowhere. + // + // No build: its resources are a package and a service, so there is nothing to compile. + await registerModule(FILTER_MODULE, resolve(catalogDir, FILTER_MODULE, "module.json")); + await mesh(`assign ${CONTROL} ${FILTER_MODULE}`); + await mesh(`push ${CONTROL}`, 600_000); + const deadline = Date.now() + 180_000; + let ruleset = ""; + while (Date.now() < deadline) { + ruleset = (await on(CONTROL, `nft list table inet mesh 2>&1`)).out; + if (/chain input/.test(ruleset)) break; + await new Promise((r) => setTimeout(r, 5_000)); + } + assert.match(ruleset, /chain input/, + `${FILTER_MODULE} is assigned and the machine has no mesh filter:\n${ruleset}`); + return ruleset; + }); + + // ---- 7..8. SOMETHING TO RUN --------------------------------------------------------------- + await step("U1", MODULE_BUILT, FILTERED, async () => { + await registerModule(MODULE.module, resolve(catalogDir, MODULE.module, "module.json")); + const built = await mesh( + `build ${forgeUrl(MODULE.repo)} --path ${MODULE.path} --ref ${refFor(MODULE.repo)} --wait 1200s`, + 1_500_000); + assert.doesNotMatch(built, /failed/i, built); + // The point of the whole step: what came out is named by a digest this mesh's registry + // assigned, not by a placeholder and not by a tag. + const builds = await mesh(`builds ${MODULE.module}`); + assert.match(builds, /sha256:[0-9a-f]{12}/, + `the build recorded no digest — the module is not pinned to anything this registry serves:\n${builds}`); + return `${built}\n${builds}`; + }); + + // `amqp-ping` requires the `amqp` provision, and the mesh refused to place it: "nothing provides + // amqp, wanted by amqp-ping". That refusal is correct and is the reason this step exists rather + // than the reason to pick an easier module. + // + // `lavinmq` is that module. It was first added here on the belief that it needed no building — + // its broker is an upstream image — and the mesh refused it again: two of its three containers + // named a placeholder digest, "which is never a real image". Also right. The broker is upstream, + // but the module is not only the broker: it carries a run-once bootstrap that writes the broker's + // configuration, a provisioner that grants each consumer its own vhost and user, tools and an + // event consumer, all of it its own code. + await step("U2", NEEDS, MODULE_BUILT, () => bringUp(PROVIDER)); + + // The machine that built it. This is the case every earlier proof covered, and it is here as the + // control for the last step: if this fails, that one's failure says nothing about fetching. + await step("U3", ANCHOR_RUNS, NEEDS, async () => { + await mesh(`module issue ${MODULE.module} --node ${CONTROL}`); + await mesh(`assign ${CONTROL} ${MODULE.module}`); + await mesh(`push ${CONTROL}`, 600_000); + return waitForContainer(CONTROL, MODULE.module); + }); + + // ---- 9. IT CAN DESCRIBE ITSELF --------------------------------------------------------------- + // + // **Presence is not function, and this step exists because I kept confusing them.** A container + // being up was taken as the catalogue working; a name containing "lavinmq" was taken as the + // module running. The mesh holds a graph — nodes, what each is assigned, what capabilities each + // has, which claims are occupied, what each module is configured with, which provisions exist and + // who holds them — and none of it was ever asked a question. + await step("V1", DESCRIBES, ANCHOR_RUNS, async () => { + const said: string[] = []; + + // The mesh's own verdict on itself. Nothing wrong, nothing waiting, nothing behind. + const state = JSON.parse(await mesh("status --json")) as { + wrong: { node: string; outcome: string; refused?: string }[]; + waiting: { node: string }[]; + reported: { node: string; outcome: string; current: boolean }[]; + }; + assert.equal(state.wrong.length, 0, + `the mesh reports something wrong:\n${JSON.stringify(state.wrong, null, 2)}`); + assert.equal(state.waiting.length, 0, + `the mesh is waiting on a node:\n${JSON.stringify(state.waiting, null, 2)}`); + const node = state.reported.find((r) => r.node === CONTROL); + assert.ok(node, `the mesh does not report the only machine it has:\n${JSON.stringify(state)}`); + assert.equal(node.outcome, "applied", `${CONTROL} did not apply what it was sent: ${node.outcome}`); + assert.ok(node.current, `${CONTROL} is not running what the mesh would send it`); + said.push(` status one node, applied, current, nothing wrong`); + + // Every module a mesh has to hold, including the three it cannot build for itself. + const modules = await mesh("module list"); + for (const m of MUST_HOLD) { + assert.match(modules, new RegExp(`^${m}\\b`, "m"), + `the mesh holds no ${m}. A mesh without it is not finished:\n${modules}`); + } + said.push(` module list ${MUST_HOLD.length} modules, all present`); + + // What the machine would be sent, and what it names. **No placeholder may survive here** — a + // digest of all zeroes is never a real image, and a declaration carrying one reaches a machine + // that will try to fetch it. + const plan = await mesh(`plan ${CONTROL} --json`); + assert.doesNotMatch(plan, /sha256:0{64}/, + `the machine's own plan names a placeholder digest, which is never a real image`); + assert.match(plan, /sha256:[0-9a-f]{64}/, `the plan pins nothing by digest at all`); + said.push(` plan every image pinned, no placeholders`); + + // And the catalogue, ASKED rather than observed. These five questions are what it exists for. + return said.join("\n"); + }); + + // ---- V2. AND THE CATALOGUE HOLDS WHAT WAS BUILT ----------------------------------------------- + // + // **Split from the step above, because they are two claims and only one of them fails.** The + // control plane describing the mesh correctly and the catalogue holding a complete record of it + // are different things, and bundling them meant one open fault stopped three later steps from + // ever being attempted. + await step("V2", CATALOGUED, DESCRIBES, async () => { + // **Asked as an operator would have to, which turns out to be nobody.** + // + // The obvious move — run the invoke inside the catalogue's own container — is refused by the + // 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 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(); + const ask = async (tool: string, args = "{}") => + must(CONTROL, + `docker run --rm --network container:mesh-broker ` + + `-e MESH_BROKER_URL=amqp://guest:guest@127.0.0.1:5672/ ` + + `${image} invoke mesh-catalog ${tool} ${quote(args)}`, + 120_000); + + const held = await ask("catalog_modules"); + // Compared against what the control plane ordered, rather than against a list written here: a + // catalogue cannot know what it was never told, so it must be measured against something that + // does. novox/hq issue 050 — on a fresh mesh the modules built before the catalogue existed + // are exactly the ones it needed in order to exist, so the hole is always the foundation. + const missing = MUST_HOLD.filter((m) => + !["distribution", "builder"].includes(m) && !held.includes(m)); + assert.deepEqual(missing, [], + `the catalogue does not hold ${missing.join(", ")} — the mesh built them and its own ` + + `record has no trace of it (novox/hq issue 050):\n${held}`); + assert.doesNotMatch(held, /sha256:0{64}/, `the catalogue holds a placeholder version`); + + const provides = await ask("catalog_provides", JSON.stringify({ provision: "amqp" })); + assert.ok(provides.includes(PROVIDER.module), + `the catalogue cannot say what provides amqp, which is a question it exists for:\n${provides}`); + + const stale = await ask("catalog_stale"); + return `${held}\n${provides}\n${stale}`; + }); + + // ---- V3. AND ITS NETWORKING IS WHAT WAS ASKED FOR --------------------------------------------- + // + // **Left out of this test entirely until it was pointed out**, which is hard to defend: the + // firewall is generated from what modules declare they listen on, and a firewall that opens the + // wrong set is either a service nobody can reach or a port nobody meant to publish. Neither shows + // up as a failed container. + await step("V3", NETWORK, DESCRIBES, async () => { + const said: string[] = []; + + const ruleset = (await on(CONTROL, `nft list table inet mesh 2>&1`)).out; + assert.match(ruleset, /chain input/, `the mesh's own firewall table is not there:\n${ruleset}`); + // Closed by default, or the rules below decide nothing. + assert.match(ruleset, /policy drop/, `the firewall does not default to closed:\n${ruleset}`); + // The floor: a machine that cannot be reached over ssh is a machine nobody can repair. + assert.match(ruleset, /\b22\b/, `ssh is not allowed anywhere in the ruleset`); + // What a module actually declared. The registry says it listens on 5000 for the mesh. + assert.match(ruleset, /\b5000\b/, + `the registry declares it listens on 5000 and nothing opened it:\n${ruleset}`); + said.push(` firewall default closed, ssh open, declared ports open`); + + // The names the mesh writes for itself. A consumer reaching a provider by its `.internal` + // address depends on this file, and on it reaching inside containers. + const hosts = (await on(CONTROL, `cat /etc/hosts`)).out; + assert.match(hosts, /\.internal/, `the mesh wrote no .internal names:\n${hosts}`); + said.push(` names ${(hosts.match(/[a-z0-9-]+\.internal/g) ?? []).join(" ")}`); + + // The networks the declarations asked for, rather than whatever the runtime had lying around. + // `lavinmq` no longer declares one: it adopts the foundation's mesh-broker rather than raising a + // server of its own on a private network (novox/hq issue 051, WBS 3.2), so the only module + // network here is the consumer's own. + const networks = (await on(CONTROL, `docker network ls --format '{{.Name}}'`)).out; + for (const wanted of ["amqp-ping"]) { + assert.match(networks, new RegExp(`^${wanted}$`, "m"), + `the ${wanted} module declares a network and none exists:\n${networks}`); + } + said.push(` networks module networks present`); + return said.join("\n"); + }); + + // ---- V4. THE WHOLE VOCABULARY, NOT THE PART I KEPT LOOKING AT -------------------------------- + // + // **Every check in this file until now asked about containers.** A container is one resource kind + // out of ten — directory, file, user, network, access, archive, service, package, container, + // action — and a module is far more often the others: the firewall is a package and a service, + // the mesh's names are a file, a run-once step is an action or a container that exits. Asking + // only about containers is how a module with no container at all went unnoticed for this long. + // + // So this takes the declaration the machine was actually sent and verifies each resource in it, + // by kind, on the machine. Nothing is hand-picked: whatever the installed modules declared is + // what gets checked, and a kind nothing declared is REPORTED as unexercised rather than quietly + // counted as working. + await step("V4", DECLARED, NETWORK, async () => { + const plan = JSON.parse(await mesh(`plan ${CONTROL} --json`)) as { + resources: Record[]; + }; + const seen = new Map(); + const wrong: string[] = []; + const unchecked: string[] = []; + + for (const r of plan.resources) { + const kind = String(r["type"] ?? ""); + seen.set(kind, (seen.get(kind) ?? 0) + 1); + const id = String(r["id"] ?? kind); + const check = async (command: string, why: string) => { + if (!(await on(CONTROL, command)).ok) wrong.push(`${kind} ${id}: ${why}`); + }; + switch (kind) { + case "directory": + await check(`test -d ${quote(String(r["path"]))}`, `no directory at ${r["path"]}`); + break; + case "file": + await check(`test -f ${quote(String(r["path"]))}`, `no file at ${r["path"]}`); + break; + case "access": + await check(`test -e ${quote(String(r["path"]))}`, `nothing at ${r["path"]}`); + break; + case "archive": + await check(`test -e ${quote(String(r["path"]))}`, + `nothing unpacked at ${r["path"]} — the archive was never fetched`); + break; + case "package": + await check(`command -v pacman >/dev/null && pacman -Q ${quote(String(r["package"]))}`, + `the package ${r["package"]} is not installed`); + break; + case "service": { + // A unit the host put into a state. "running" is the state worth checking; a one-shot + // that has done its work reports inactive and that is correct (this is the reading that + // made the firewall module appear broken on every machine for months). + const unit = String(r["unit"]); + if (String(r["state"]) === "running") { + await check(`systemctl is-active ${quote(unit)} >/dev/null || ` + + `systemctl show -p ExecMainStatus --value ${quote(unit)} | grep -qx 0`, + `the unit ${unit} is neither active nor a one-shot that succeeded`); + } + break; + } + case "network": + await check(`docker network inspect ${quote(String(r["name"]))} >/dev/null 2>&1`, + `no network named ${r["name"]}`); + break; + case "container": { + const name = String(r["name"]); + if (r["run-once"] === true || r["schedule"]) { + // **A run-once step leaves nothing to ask, deliberately.** The host removes the exited + // container so a later apply is not confused by a stopped one, and keeps the record + // that it ran in its own store instead. So asserting the container exists asserts the + // opposite of correct behaviour — which this did, and reported a working mesh as + // broken on its first run. + // + // A scheduled step is the same between fires. Both are counted as unverified here + // rather than assumed good: what would verify them is the host's own record, and this + // asks the machine rather than the host. + unchecked.push(`${kind} ${id} (a step leaves nothing running to ask)`); + } else { + await check(`docker ps --format '{{.Names}}' | grep -qx ${quote(name)}`, + `the container ${name} is not running`); + } + break; + } + default: + unchecked.push(`${kind} ${id}`); + } + } + + const kinds = [...seen.entries()].sort().map(([k, n]) => `${k}×${n}`).join(" "); + const never = ["directory", "file", "user", "network", "access", "archive", "service", + "package", "container", "action"].filter((k) => !seen.has(k)); + assert.deepEqual(wrong, [], + `the machine is not what the mesh said it should be:\n ${wrong.join("\n ")}`); + return [ + ` declared ${plan.resources.length} resources — ${kinds}`, + unchecked.length ? ` not verified here ${unchecked.join(", ")}` : ` every kind present was verified`, + never.length ? ` NOT EXERCISED ${never.join(", ")} — nothing installed here declares one` : ``, + ].filter(Boolean).join("\n"); + }); + + // ---- 11. A CHANGE REACHES THE MACHINE ON ITS OWN ---------------------------------------------- + // + // The whole point of the mesh, and the capability the migration depends on: move a module's + // source and the running copy follows, with nobody driving the steps. Everything above is + // machinery; this is what the machinery is for. + await step("E1", FOLLOWS, DECLARED, async () => { + const before = await mesh(`builds ${MODULE.module}`); + const was = before.match(/sha256:[0-9a-f]{64}/)?.[0] ?? ""; + assert.ok(was, `nothing is pinned to rebuild from:\n${before}`); + + // **The source has to actually move, and it cannot be faked.** + // + // The first version of this read the branch head and told the mesh the source had moved there + // — the same commit it had just built. The mesh answered, correctly, that everything was + // current. Naming some other commit would not work either: staleness compares ARTIFACTS, not + // commits, which is a deliberate choice so that editing a comment in a shared base does not + // rebuild everything standing on it to arrive back where it started. + // + // So this makes a real change to the module's source and pushes it. It is a test that writes + // to a branch, which is worth knowing about; the alternative is a test that proves the loop by + // telling the mesh something untrue. + const checkout = resolve(catalogDir, ".."); + const marker = `// changed by the one-node test at build ${was.slice(7, 19)}\n`; + const file = resolve(catalogDir, MODULE.module, "index.ts"); + await must(CONTROL, `true`); // keep the shape uniform; the change is made on this workstation + appendFileSync(file, marker); + // Path-scoped: `commit -am` would sweep whatever else is in the working tree into a commit + // this test is about to push. + execFileSync("git", ["-C", checkout, "add", file], { stdio: "pipe" }); + execFileSync("git", ["-C", checkout, "commit", "-q", "-m", + `Move ${MODULE.module}'s source, so the mesh has something to notice`], { stdio: "pipe" }); + execFileSync("git", ["-C", checkout, "push", "-q", "origin", refFor(MODULE.repo)], + { stdio: "pipe" }); + const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], + { encoding: "utf8" }).trim(); + + // Now the mesh is told. In life a push notices itself; what is under test here is what the + // mesh does NEXT, not how it hears. + await mesh(`module moved ${MODULE.module} ${head}`); + const behind = await mesh(`status`); + assert.match(behind, /behind|build --behind/, + `the source moved and the mesh does not report the module behind it:\n${behind}`); + + await mesh(`build --behind --wait 1200s`, 1_500_000); + const rolled = await mesh(`upgrade ${MODULE.module} roll-out`, 900_000); + await waitForContainer(CONTROL, MODULE.container); + + const after = await mesh(`builds ${MODULE.module}`); + const now = after.match(/sha256:[0-9a-f]{64}/)?.[0] ?? ""; + assert.notEqual(now, was, + `the module was rebuilt and came back on the same artifact, so nothing reached the machine:` + + `\n${after}`); + return `${behind}\n${rolled}\n${after}`; + }); + + // ---- S1. THE STORE IS UPGRADED IN PLACE, THROUGH A STATED WINDOW ----------------------------- + // + // The store the foundation raised is now the `postgres` module (novox/hq issue 051), so it can be + // upgraded like any other — and upgrading a SERVER, unlike its provisioner, recreates the + // container the control plane keeps its own records in. This is the window: the data survives on + // the named volume, and the control plane's connection pool reconnects to the server that comes + // back. A benign marker on the server's spec stands in for a version bump; the mechanism — the + // applier replacing the container while the volume persists — is the same one a real bump uses. + await step("S1", STORE_UPGRADES, FOLLOWS, async () => { + const said: string[] = []; + + // 3.4: the foundation is source-tracked now. Before it was adopted, the store was a container + // the installer raised with no source the mesh could hold; now it is a module `status` includes + // in what can be behind — the question that "could not form" before. + const list = await mesh("module list"); + assert.match(list, /postgres/, `the store is not a module the mesh lists:\n${list}`); + assert.match(list, /lavinmq/, `the broker is not a module the mesh lists:\n${list}`); + said.push(" source-tracked postgres and lavinmq are modules the mesh can report behind"); + + // What has to survive the window: every database in the store. Counted, not named, so this does + // not depend on which consumers happened to ask for one. + const count = async () => + (await on(CONTROL, + `docker exec mesh-store psql -U postgres -tAc "select count(*) from pg_database where datistemplate=false"`)) + .out.trim(); + const before = await count(); + + // The mesh's own upgrade path: move the source, build, roll out. Two files move — the + // provisioner, so the artifact changes and the build has something to publish (staleness + // compares artifacts, not commits, exactly as E1 records), and the server's spec, so the + // roll-out recreates mesh-store, which is the window. A benign marker stands in for a version + // bump; the applier replacing the container while the volume persists is the same either way. + const checkout = resolve(catalogDir, ".."); + const provisioner = resolve(catalogDir, "postgres", "provisioner", "index.ts"); + const path = resolve(catalogDir, "postgres", "module.json"); + appendFileSync(provisioner, `// store upgrade marker ${before}\n`); + const bumped = JSON.parse(readFileSync(path, "utf8")); + bumped.resources.find((r: { name?: string }) => r.name === "mesh-store").env.MESH_UPGRADE_MARKER = "s1"; + writeFileSync(path, JSON.stringify(bumped, null, 2)); + execFileSync("git", ["-C", checkout, "add", provisioner, path], { stdio: "pipe" }); + execFileSync("git", ["-C", checkout, "commit", "-q", "-m", + "Upgrade the store, so the mesh rebuilds and recreates it"], { stdio: "pipe" }); + execFileSync("git", ["-C", checkout, "push", "-q", "origin", refFor(STORE.repo)], { stdio: "pipe" }); + const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + + await mesh(`module moved postgres ${head}`); + const behind = await mesh("status"); + assert.match(behind, /behind|build --behind/, + `the store's source moved and the mesh does not report it behind:\n${behind}`); + await mesh(`build --behind --wait 1200s`, 1_500_000); + await mesh(`upgrade postgres roll-out`, 900_000); + // roll-out rolls out the behind ARTIFACT (the provisioner runtime); the server's change is a + // manifest edit, not a new artifact, so a full push is what applies it — recreating mesh-store. + await mesh(`push ${CONTROL}`, 600_000); + await waitForContainer(CONTROL, "mesh-store"); + + // Poll, not a single read: the recreate can settle after waitForContainer sees a container up, + // so a single inspect races the replacement. The window is real; this waits for it to close. + const deadline = Date.now() + 120_000; + let env = ""; + while (Date.now() < deadline) { + env = (await on(CONTROL, `docker inspect mesh-store --format '{{.Config.Env}}'`)).out; + if (/MESH_UPGRADE_MARKER=s1/.test(env)) break; + await new Promise((r) => setTimeout(r, 5_000)); + } + assert.match(env, /MESH_UPGRADE_MARKER=s1/, + `mesh-store did not come back on the upgraded spec within the window:\n${env}`); + // The control plane read through the window: `status` opens all three of its stores, so a clean + // answer is proof the pool reconnected to the server that came back. + const status = await mesh("status"); + assert.doesNotMatch(status, /cannot|refused|could not/i, + `the control plane did not read through the store window:\n${status}`); + const after = await count(); + assert.equal(after, before, + `databases did not survive the store upgrade (before=${before} after=${after})`); + said.push(" window mesh-store recreated, every database kept, the pool reconnected"); + return said.join("\n"); + }); + + // ---- S2. THE BROKER IS UPGRADED IN PLACE, OVER THE BROKER ------------------------------------ + // + // The harder one: the broker is what the push that upgrades it travels over. Recreating + // mesh-broker drops the bus mid-apply, and the machine has to finish the replacement locally and + // the mesh reconnect to the broker that comes back. Same benign-marker stand-in for a version + // bump; what is under test is that the mesh survives replacing its own bus. + await step("S2", BROKER_UPGRADES, STORE_UPGRADES, async () => { + const said: string[] = []; + // Same upgrade path as S1, for the broker: move the provisioner and the server's spec, build, + // roll out. The roll-out recreates mesh-broker, and it is what the roll-out itself travels over, + // so this proves the mesh finishes replacing its own bus and reconnects to the one that returns. + const checkout = resolve(catalogDir, ".."); + const provisioner = resolve(catalogDir, "lavinmq", "provisioner", "index.ts"); + const path = resolve(catalogDir, "lavinmq", "module.json"); + appendFileSync(provisioner, `// broker upgrade marker\n`); + const bumped = JSON.parse(readFileSync(path, "utf8")); + bumped.resources.find((r: { name?: string }) => r.name === "mesh-broker").env = { + ...bumped.resources.find((r: { name?: string }) => r.name === "mesh-broker").env, + MESH_UPGRADE_MARKER: "s2", + }; + writeFileSync(path, JSON.stringify(bumped, null, 2)); + execFileSync("git", ["-C", checkout, "add", provisioner, path], { stdio: "pipe" }); + execFileSync("git", ["-C", checkout, "commit", "-q", "-m", + "Upgrade the broker, so the mesh rebuilds and recreates it"], { stdio: "pipe" }); + execFileSync("git", ["-C", checkout, "push", "-q", "origin", refFor(PROVIDER.repo)], { stdio: "pipe" }); + const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + + await mesh(`module moved lavinmq ${head}`); + const behind = await mesh("status"); + assert.match(behind, /behind|build --behind/, + `the broker's source moved and the mesh does not report it behind:\n${behind}`); + await mesh(`build --behind --wait 1200s`, 1_500_000); + await mesh(`upgrade lavinmq roll-out`, 900_000); + // As in S1: the server's manifest change lands on a full push, not the artifact roll-out. + await mesh(`push ${CONTROL}`, 600_000); + await waitForContainer(CONTROL, "mesh-broker"); + + // Poll, not a single read: recreating the broker drops the bus the push travelled over, so the + // control plane reconnects and the recreate settles a cycle later than the store's did — the + // window here is a reconnection, and it is longer. + const deadline = Date.now() + 120_000; + let env = ""; + while (Date.now() < deadline) { + env = (await on(CONTROL, `docker inspect mesh-broker --format '{{.Config.Env}}'`)).out; + if (/MESH_UPGRADE_MARKER=s2/.test(env)) break; + await new Promise((r) => setTimeout(r, 5_000)); + } + assert.match(env, /MESH_UPGRADE_MARKER=s2/, + `mesh-broker did not come back on the upgraded spec within the window:\n${env}`); + // The mesh talks over the broker that came back: a fresh push composes and delivers, which needs + // the bus, so a clean one is proof the reconnection happened. + const again = await mesh(`push ${CONTROL}`, 600_000); + assert.doesNotMatch(again, /cannot|refused|could not/i, + `the mesh did not talk over the broker that came back:\n${again}`); + said.push(" window mesh-broker recreated, the mesh talks over the one that came back"); + return said.join("\n"); + }); + + // ---- 12. AND IT SURVIVES THE MACHINE STOPPING ------------------------------------------------- + // + // **Never once tested.** A mesh that works until the machine reboots is a demonstration, not + // something to move real services onto — and the installer is explicit that a host started the + // way the lab starts it does not survive a reboot, which makes this the check that says whether + // that matters. + await step("E2", SURVIVES, BROKER_UPGRADES, async () => { + await restartMachine(CONTROL); + const missing: string[] = []; + for (const container of MUST_RUN) { + try { + await waitForContainer(CONTROL, container, 240); + } catch { + missing.push(container); + } + } + const ps = (await on(CONTROL, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out; + assert.equal(missing.length, 0, + `after a reboot these are not running: ${missing.join(", ")}\n\ncontainers:\n${ps}`); + + // **Containers coming back is not the mesh coming back.** The runtime restarts containers on + // its own; what makes a machine part of a mesh is an agent listening for what it should be. A + // machine whose containers returned and whose agent did not looks healthy and cannot be told + // anything. + // + // **A failure here is this lab's arrangement, not the mesh's capability.** mesh-host ships + // `nox-mesh-host.service` and two companions in packaging/; the installer declines to place + // them because a unit file is a packaging decision, and the lab starts the host with + // --host-in-background, which says in its own help that it does not survive a reboot. So this + // check is honest and the thing it catches is the step nobody runs — not a mesh that cannot + // come back. + const agent = (await on(CONTROL, `pgrep -af '[m]esh-host run' | head -3`)).out.trim(); + assert.ok(agent, + `every container came back and no host agent did, so the machine is running the right ` + + `things and can no longer be told anything:\n${ps}`); + return `${ps}\n${agent}`; + }); + + stateOutcome(); +}, { timeout: 7_200_000 }); + +after(async () => { + if (KEEP) { + console.log(`\nLEFT STANDING: ${instanceId} — not destroyed (MESH_LAB_KEEP).`); + return; + } + if (instanceId) await destroy(instanceId); + await destroyAll(`${SCENARIO}-`); +}, { timeout: 900_000 }); + +for (const name of [ + GENESIS, + FOUNDATION, + BUILT_CP, + PIVOTED, + HAS_REGISTRY, + ENROLLED, + HAS_BUILDER, + BASE_BUILT, + STORE_RUNS, + CATALOGUE_RUNS, + CONTROL_REBUILT, + NETWORKED, + FILTERED, + MODULE_BUILT, + NEEDS, + ANCHOR_RUNS, + DESCRIBES, + CATALOGUED, + NETWORK, + DECLARED, + FOLLOWS, + SURVIVES, +]) { + test(name, { skip, timeout: 60_000 }, () => { + assert.ok(steps.get(name)?.ok, report(name)); + }); +} diff --git a/test/integration/openai-bed.test.ts b/test/integration/openai-bed.test.ts index f15d57d..f80f327 100644 --- a/test/integration/openai-bed.test.ts +++ b/test/integration/openai-bed.test.ts @@ -12,7 +12,7 @@ * ordinary sealed-delivery path — with NO manager, NO refresh, NO access/refresh split, NO usage. The * whole of OpenAI's integration in the control plane is one registry line (vendor -> static-key). * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * Build the consumer runtime image into the local daemon first: * scripts/build-module-runtime.sh openai-consumer /tmp/openai-consumer.tar */ @@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -36,7 +36,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "openai-bed"; @@ -69,11 +69,11 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } async function meshTry(command: string): Promise<{ out: string; ok: boolean }> { - return on(`docker exec mesh-control /mesh-control ${command}`); + return on(`docker exec mesh-controller /mesh-controller ${command}`); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -82,7 +82,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -135,11 +135,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -159,7 +159,7 @@ test("a static-key model-access licence delivers the operator's API key to the c const consumerImage = pinned("mesh-runtime-openai-consumer"); // --- the licence, a record with vendor openai (static-key) ------------------------------------- - // No manager: a static-key licence has none (mesh-control refuses `licence manager` on it). The + // No manager: a static-key licence has none (mesh-controller refuses `licence manager` on it). The // consumer is put on the licence BEFORE the key is set — the static-key adapter's Accept seals to the // CURRENT holders, so a holder must exist first, or the key would seal to nobody. await mesh(`licence add openai personal --serves '{"model":"gpt-model"}'`); @@ -167,8 +167,8 @@ test("a static-key model-access licence delivers the operator's API key to the c // The operator sets the static key. `licence key` reads it from a file (never a CLI arg) and seals it // to the holder; the plaintext is discarded by the control plane. Staged 0644 so distroless - // mesh-control can read the file (docker cp preserves the mode). - await must(`printf %s ${quote(API_KEY)} > /tmp/openai-key && chmod 0644 /tmp/openai-key && docker cp /tmp/openai-key mesh-control:/openai-key`); + // mesh-controller can read the file (docker cp preserves the mode). + await must(`printf %s ${quote(API_KEY)} > /tmp/openai-key && chmod 0644 /tmp/openai-key && docker cp /tmp/openai-key mesh-controller:/openai-key`); await mesh(`licence key personal --file /openai-key`); // --- deploy the consumer ----------------------------------------------------------------------- @@ -199,7 +199,7 @@ test("a static-key model-access licence delivers the operator's API key to the c }, ], }); - await must(`printf %s ${quote(consumerManifest)} > /tmp/openai-consumer.json && docker cp /tmp/openai-consumer.json mesh-control:/openai-consumer.json`); + await must(`printf %s ${quote(consumerManifest)} > /tmp/openai-consumer.json && docker cp /tmp/openai-consumer.json mesh-controller:/openai-consumer.json`); await mesh(`module add /openai-consumer.json`); await mesh(`module issue openai-consumer --node ${MACHINE}`); await mesh(`assign ${MACHINE} openai-consumer`); diff --git a/test/integration/postgres-grant-end-to-end.test.ts b/test/integration/postgres-grant-end-to-end.test.ts index 3ee0521..1859f98 100644 --- a/test/integration/postgres-grant-end-to-end.test.ts +++ b/test/integration/postgres-grant-end-to-end.test.ts @@ -9,7 +9,7 @@ * the mesh minted. The proof is the consumer connecting to its database with the credential the mesh * delivered it. Nothing is placed by the test. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh postgres builds mesh-runtime-postgres:development (with psql), * which scenarios/postgres-node.yml stocks. */ @@ -21,7 +21,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -33,7 +33,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "postgres-node"; @@ -62,7 +62,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -71,7 +71,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -84,7 +84,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -116,11 +116,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -157,7 +157,7 @@ test("the mesh grants a consumer a postgres database, and the credential it deli { id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" }, { id: "net", type: "network", name: "postgres" }, { - // No published port here: the substrate's own store already holds host :5432 on this + // No published port here: the foundation's own store already holds host :5432 on this // single-node bed, and the consumer reaches postgres over the private network by name. The // committed manifest publishes it for cross-node consumers, which is a different node. id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres", @@ -194,12 +194,12 @@ test("the mesh grants a consumer a postgres database, and the credential it deli resources: [{ id: "state", type: "directory", path: "/var/lib/dbuser", mode: "0700" }], }); - await must(`printf %s ${quote(postgresManifest)} > /tmp/postgres.json && docker cp /tmp/postgres.json mesh-control:/postgres.json`); + await must(`printf %s ${quote(postgresManifest)} > /tmp/postgres.json && docker cp /tmp/postgres.json mesh-controller:/postgres.json`); await mesh("module add /postgres.json"); await mesh(`module issue postgres --node ${MACHINE}`); await mesh(`assign ${MACHINE} postgres`); - await must(`printf %s ${quote(consumerManifest)} > /tmp/dbuser.json && docker cp /tmp/dbuser.json mesh-control:/dbuser.json`); + await must(`printf %s ${quote(consumerManifest)} > /tmp/dbuser.json && docker cp /tmp/dbuser.json mesh-controller:/dbuser.json`); await mesh("module add /dbuser.json"); await mesh(`assign ${MACHINE} dbuser`); diff --git a/test/integration/provider-on-backend-network.test.ts b/test/integration/provider-on-backend-network.test.ts index 4b3f1b8..18b1894 100644 --- a/test/integration/provider-on-backend-network.test.ts +++ b/test/integration/provider-on-backend-network.test.ts @@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -32,7 +32,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "redis-node"; @@ -61,7 +61,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -70,7 +70,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -83,7 +83,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -115,11 +115,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -184,7 +184,7 @@ test("redis's runtime, on the backend's private network, binds the broker and pr }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`); + await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`); await mesh("module add /redis.json"); await mesh(`module issue redis --node ${MACHINE}`); await mesh(`assign ${MACHINE} redis`); diff --git a/test/integration/provider-uses-mesh-credential.test.ts b/test/integration/provider-uses-mesh-credential.test.ts index 1fca4ba..1e72897 100644 --- a/test/integration/provider-uses-mesh-credential.test.ts +++ b/test/integration/provider-uses-mesh-credential.test.ts @@ -9,10 +9,10 @@ * password gets PONG — where a provisioner that invented its own password would answer WRONGPASS. * * A hand-written contributions file and secret stand in for the control plane here (a full grant - * from a second module is a heavier bed); their SHAPE is exactly what mesh-control writes — a + * from a second module is a heavier bed); their SHAPE is exactly what mesh-controller writes — a * `receives` doc with `as`/`secret`, and the secret file the host leaves after unsealing. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which * scenarios/redis-node.yml stocks. */ @@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -36,7 +36,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "redis-node"; @@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -74,7 +74,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -87,7 +87,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -119,11 +119,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -184,7 +184,7 @@ test("redis creates a consumer's login with the password the mesh minted, sealin }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`); + await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`); await mesh("module add /redis.json"); await mesh(`module issue redis --node ${MACHINE}`); await mesh(`assign ${MACHINE} redis`); diff --git a/test/integration/provisioner.test.ts b/test/integration/provisioner.test.ts index 853d14b..f4a43ef 100644 --- a/test/integration/provisioner.test.ts +++ b/test/integration/provisioner.test.ts @@ -25,7 +25,7 @@ const provisioner = process.env["MESH_LAB_PROVISIONER"] ?? ""; const skip = !capability.usable ? `lab not usable: ${capability.why}` : !provisioner - ? "set MESH_LAB_PROVISIONER to a built provisioner (mesh-control: go build ./examples/postgres-provisioner)" + ? "set MESH_LAB_PROVISIONER to a built provisioner (mesh-controller: go build ./examples/postgres-provisioner)" : false; const SCENARIO = "a-provider"; diff --git a/test/integration/route-forwarding.test.ts b/test/integration/route-forwarding.test.ts index bb8fe23..9ded2b1 100644 --- a/test/integration/route-forwarding.test.ts +++ b/test/integration/route-forwarding.test.ts @@ -25,7 +25,7 @@ * publicly-trusted certificate and answering an HTTP-01 challenge at the name — is proven separately * by certificates.test.ts, which drives the same proxy binary against a real ACME server (Pebble). * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-route-proxy-image.sh builds mesh-route-proxy:development into the local daemon; * scenarios/route-forwarding.yml stocks it and alpine:latest, and serves both by digest. */ @@ -37,7 +37,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -49,7 +49,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "route-forwarding"; @@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise { /** The control plane, a container on the node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The reference a manifest should carry, once this scenario has been raised. */ @@ -90,9 +90,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -105,7 +105,7 @@ async function settled(withinMs = 600_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -143,12 +143,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // Raise the substrate — store, broker, control — from the bundle. - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + // Raise the foundation — store, broker, control — from the bundle. + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // The node joins its own mesh, so it is a node the mesh can assign to, and the host runs so it @@ -216,13 +216,13 @@ test("the mesh routes a public name through the proxy to the consumer, and withd ], }); - await must(`printf %s ${quote(proxyManifest)} > /tmp/route-proxy.json && docker cp /tmp/route-proxy.json mesh-control:/route-proxy.json`); + await must(`printf %s ${quote(proxyManifest)} > /tmp/route-proxy.json && docker cp /tmp/route-proxy.json mesh-controller:/route-proxy.json`); await mesh("module add /route-proxy.json"); // No `module issue`: route-proxy has no broker account and no own-secret to mint. `assign` resolves // its plan and the provider is matchable by a consumer's route from that alone. await mesh(`assign ${MACHINE} route-proxy`); - await must(`printf %s ${quote(webManifest)} > /tmp/hello-web.json && docker cp /tmp/hello-web.json mesh-control:/hello-web.json`); + await must(`printf %s ${quote(webManifest)} > /tmp/hello-web.json && docker cp /tmp/hello-web.json mesh-controller:/hello-web.json`); await mesh("module add /hello-web.json"); await mesh(`assign ${MACHINE} hello-web`); diff --git a/test/integration/runtime-restart-on-config.test.ts b/test/integration/runtime-restart-on-config.test.ts index 6eaf500..5dc1bcb 100644 --- a/test/integration/runtime-restart-on-config.test.ts +++ b/test/integration/runtime-restart-on-config.test.ts @@ -12,7 +12,7 @@ * the container was replaced (a new container id) and the config on disk carries the new value. * It builds the host from source (no --no-build), because the behaviour under test is the host's. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh grafana builds mesh-runtime-grafana:development, which * scenarios/grafana-node.yml stocks. */ @@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -36,7 +36,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "grafana-node"; @@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise { } async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -74,7 +74,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function tokenFrom(said: string): string { @@ -87,7 +87,7 @@ async function settled(withinMs = 480_000): Promise { const until = Date.now() + withinMs; let last = ""; while (Date.now() < until) { - const asked = await on(`docker exec mesh-control /mesh-control status --json`); + const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); if (asked.ok) { try { const state = JSON.parse(asked.out) as { @@ -112,7 +112,7 @@ async function settled(withinMs = 480_000): Promise { async function setToken(token: string): Promise { const settings = JSON.stringify({ url: "http://127.0.0.1:3000", token }); - await must(`printf %s ${quote(settings)} > /tmp/s.json && docker cp /tmp/s.json mesh-control:/s.json`); + await must(`printf %s ${quote(settings)} > /tmp/s.json && docker cp /tmp/s.json mesh-controller:/s.json`); await mesh(`settings set grafana /s.json --node ${MACHINE}`); } @@ -129,11 +129,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); + await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } await mesh(`node add ${MACHINE}`); @@ -170,7 +170,7 @@ test("a running runtime is recreated when its settings change, and reads the new }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-control:/grafana.json`); + await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-controller:/grafana.json`); await mesh("module add /grafana.json"); await setToken("token-alpha"); diff --git a/test/integration/whole-mesh-ace.test.ts b/test/integration/whole-mesh-ace.test.ts index 39606aa..44cca2d 100644 --- a/test/integration/whole-mesh-ace.test.ts +++ b/test/integration/whole-mesh-ace.test.ts @@ -1,9 +1,9 @@ /** * The whole `ace` server's converted service set, installed together on ONE node behind the - * substrate — the media / home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of + * foundation — the media / home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of * whole-mesh-novox.test.ts; same harness and topology, a larger, media-heavy module set. * - * Substrate (store, broker, control) rides `anchor` and nothing else; ALL of ace's services ride the + * Foundation (store, broker, control) rides `anchor` and nothing else; ALL of ace's services ride the * `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres/redis * providers co-located with them. The media stack shares the operator-owned library directories * under /services/media (ADR 0051 `accesses`): the mesh writes an `access` resource that CONFIRMS @@ -20,7 +20,7 @@ * references of OURS are rewritten to the IDs the machine holds, and the co-located * host-port collisions are remapped at load time (see REMAP). * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock */ import { test, before, after } from "node:test"; @@ -31,7 +31,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -44,7 +44,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "whole-mesh-ace"; @@ -171,7 +171,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi } async function mesh(command: string, timeoutMs?: number): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -180,7 +180,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function loadManifest(name: string): { manifest: string; broker: boolean } { @@ -214,7 +214,7 @@ interface NodeState { } async function nodeState(node: string): Promise { - const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`); + const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`); if (!asked.ok) return { reached: false, applied: false, current: false, waiting: false, raw: asked.out }; let state: { wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[]; @@ -248,11 +248,11 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000); + await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 900_000); const up = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } for (const machine of ["anchor", NODE]) { @@ -294,7 +294,7 @@ test("the whole ace service set resolves, installs and converges on one node in if (DROPPED.some((d) => d.name === name)) continue; try { const { manifest, broker } = loadManifest(name); - await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); if (broker) { await mesh(`module issue ${name} --node ${NODE}`); diff --git a/test/integration/whole-mesh-full.test.ts b/test/integration/whole-mesh-full.test.ts index 7198827..82f4009 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -5,12 +5,12 @@ * * hosting (public) home (private, behind a NAT access point) * novox 192.0.2.20 — the ANCHOR: ace 10.99.1.10 the home server, media/IoT set - * substrate (store/broker/ shanks 10.99.1.20 workstation (light: portainer only) + * foundation (store/broker/ shanks 10.99.1.20 workstation (light: portainer only) * control) + the whole novox g14 10.99.1.30 workstation (light: portainer only) * set + overlay hub + ingress * - * There is NO separate anchor: novox IS the anchor. The substrate runs on novox, and novox also - * enrols as a node and receives its own service set — the substrate host and a service node at once. + * There is NO separate anchor: novox IS the anchor. The foundation runs on novox, and novox also + * enrols as a node and receives its own service set — the foundation host and a service node at once. * * TWO ACTS, AND THE BED NOW DISTINGUISHES THEM (novox/hq ADR 0067). * @@ -22,9 +22,9 @@ * is a WORKING MESH OF ONE, and this bed asserts exactly that before going any further. * * JOINING — ace, shanks and g14 then join a mesh that already exists: host binary, token, - * `enrol`, run the agent. No bootstrap, no substrate, no registry. novox is NOT enrolled again. + * `enrol`, run the agent. No bootstrap, no foundation, no registry. novox is NOT enrolled again. * - * The bed used to do neither. It applied the substrate bundle itself and looped enrolment over all + * The bed used to do neither. It applied the foundation bundle itself and looped enrolment over all * four machines as one continuous operation — which got the order right by accident and modelled * the wrong shape, and is why ADR 0067's own acceptance check ("the bed bootstraps through the * installer rather than around it") went unmet. Genesis GATES joining: if it stops, the bed says @@ -39,10 +39,10 @@ * BEFORE any heavy module lands, so the cross-segment-overlay verdict survives whatever the module * convergence then does. Phase B converges the full node sets and reports per node. * - * SUBSTRATE-ON-NOVOX PORT COLLISIONS (a real consequence of collapsing the anchor onto novox that the - * separate-anchor beds never hit): the substrate store binds 127.0.0.1:5432 and novox's postgres - * provider publishes 5432; the substrate broker binds 5671 + 127.0.0.1:5672 and novox's lavinmq - * provider publishes 5672. The two provider host publishes are REMAPPED off the substrate's ports + * FOUNDATION-ON-NOVOX PORT COLLISIONS (a real consequence of collapsing the anchor onto novox that the + * separate-anchor beds never hit): the foundation store binds 127.0.0.1:5432 and novox's postgres + * provider publishes 5432; the foundation broker binds 5671 + 127.0.0.1:5672 and novox's lavinmq + * provider publishes 5672. The two provider host publishes are REMAPPED off the foundation's ports * (REMAP below); consumers reach the providers over the mesh network on the container port, so the * host side is free to move. Reported as a topology finding. * @@ -50,7 +50,7 @@ * (whole-mesh-full-live) and NOT torn down — it is left standing and browsable. Without it the bed * behaves like every other: raise in before(), destroy in after(). * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap MESH_LAB_CATALOG=.../mesh-catalog/modules */ @@ -65,7 +65,7 @@ import { destroy, exec, instanceNameOf, push } from "../../src/lifecycle/operate import { bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH, } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import { referenceFor, type HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -84,7 +84,7 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : !installer || !existsSync(installer) ? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap (mesh-host `make " + "bootstrap IMAGE=mesh-builder:development`). The anchor is raised BY the installer now, " + @@ -96,7 +96,7 @@ const skip = !capability.usable : false; const SCENARIO = "whole-mesh-full"; -/** novox hosts the substrate and the control plane; it is where `mesh` commands run. */ +/** novox hosts the foundation and the control plane; it is where `mesh` commands run. */ const CONTROL = "novox"; /** Every node in the mesh. novox is on hosting; the rest are behind the home gateway. */ const NODES = ["novox", "ace", "shanks", "g14"]; @@ -118,7 +118,7 @@ const CATALOGUE_ON_MACHINE = "/opt/mesh-catalog"; * `internal/bootstrap` RegistryModule and ControlPlaneModule. If it ever opens a third, this list * is where the bed finds out, by the installer saying which manifest it could not read. */ -const CATALOGUE_MODULES = ["registry", "mesh-control", "builder"]; +const CATALOGUE_MODULES = ["registry", "mesh-controller", "builder"]; /** * Where this mesh keeps its own images, as the anchor reaches it. @@ -221,7 +221,7 @@ const CORE_NOVOX = new Set([ const GAPS_NOVOX = new Set([ "umami", "mailu", "firewall", "fail2ban", "only-office", "de-spiegel", "amqp-email-forwarder", // step-ca is reported, not gated: the internal-CA ISSUANCE path is still being fixed in - // mesh-control, and this bed is not the place to discover that a fix has not landed yet. What is + // mesh-controller, and this bed is not the place to discover that a fix has not landed yet. What is // gated is the half that is decided and cheap — see the ADR 0066 section at the end. "step-ca", ]); @@ -274,13 +274,13 @@ const PLAN: { node: string; mods: Mod[]; core: Set; gaps: Set }[ /** * Host-port remaps (per module; host ports are per-VM so novox's and ace's never clash across nodes). - * The two SUBSTRATE collisions are the new ones: postgres 5432 and lavinmq 5672 are moved off the - * substrate store/broker's host ports, which only exist on novox because that is where the substrate + * The two FOUNDATION collisions are the new ones: postgres 5432 and lavinmq 5672 are moved off the + * foundation store/broker's host ports, which only exist on novox because that is where the foundation * runs. The rest break the novox web/app host-port collisions (route-proxy fronts 80/443). */ const REMAP: Record> = { // Moved, NOT bound to loopback. These two carried `127.0.0.1:` and it broke a consumer on a node - // with no substrate at all: a module is told to reach its provider at `.internal`, that + // with no foundation at all: a module is told to reach its provider at `.internal`, that // name resolves to the node's overlay address, and a provider listening only on loopback refuses // it. letta on ace died of exactly this — "is the server running on that host and accepting // TCP/IP connections?" — while postgres sat healthy beside it. @@ -344,7 +344,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane, a container on novox (the anchor). */ async function mesh(command: string, timeoutMs?: number): Promise { - return must(CONTROL, `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must(CONTROL, `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ @@ -353,7 +353,7 @@ function pinned(reference: string): string { } function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } function loadManifest(name: string): { manifest: string; broker: boolean } { @@ -405,7 +405,7 @@ interface NodeState { } async function nodeState(node: string): Promise { - const asked = await on(CONTROL, `docker exec mesh-control /mesh-control status --json`); + const asked = await on(CONTROL, `docker exec mesh-controller /mesh-controller status --json`); if (!asked.ok) return { reached: false, applied: false, current: false, waiting: false, raw: asked.out }; let state: { wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[]; @@ -467,9 +467,9 @@ async function deliverCaRoot(): Promise { // Safe here and nowhere else: these three exist for the seconds between being written and // being sealed to the machine, on a lab node, for a CA thrown away with the scenario. "chmod 0644 /tmp/ca/root.crt /tmp/ca/root.key /tmp/ca/key-password", - "docker cp /tmp/ca/root.crt mesh-control:/ca-root-cert", - "docker cp /tmp/ca/root.key mesh-control:/ca-root-key", - "docker cp /tmp/ca/key-password mesh-control:/ca-root-key-password", + "docker cp /tmp/ca/root.crt mesh-controller:/ca-root-cert", + "docker cp /tmp/ca/root.key mesh-controller:/ca-root-key", + "docker cp /tmp/ca/key-password mesh-controller:/ca-root-key-password", ].join("\n"), 180_000); if (!made.ok) { console.log(`CA ROOT NOT MADE on ${CONTROL}:\n${made.out.split("\n").slice(-8).join("\n")}`); @@ -527,7 +527,7 @@ function stepIn(said: string): string { * Put on the anchor what the installer needs to read, and run it. * * **This is the whole of what changed, and it is not a refactor.** The bed used to apply the - * substrate bundle itself, by hand, and then enrol four machines in one loop. It got the order + * foundation bundle itself, by hand, and then enrol four machines in one loop. It got the order * right by accident and it modelled the wrong shape: an install procedure that exists only as a * test fixture is exercised by whoever writes tests and never by whoever installs, which is why * every bootstrap fault this year was found late (novox/hq ADR 0067). The anchor is now raised by @@ -566,31 +566,31 @@ async function genesis(images: HeldImage[]): Promise { } report.push(` catalogue ${CATALOGUE_MODULES.join(", ")} at ${CATALOGUE_ON_MACHINE}`); - // The substrate TEMPLATE — not the bundle. The installer produces the bundle from it: it replaces + // The foundation TEMPLATE — not the bundle. The installer produces the bundle from it: it replaces // the control plane's image with the id of the image it carries, renames that container - // `temp-mesh-control`, and writes the result where a person can read it. + // `temp-mesh-controller`, and writes the result where a person can read it. // // Two substitutions still happen here, and both belong to the bed rather than to the installer. // The example names three images at a registry the lab no longer raises: the store and the broker // become the upstream references mesh-catalog pins (harness), and the machine pulls them over its - // uplink like any first node. The third, mesh-control, is deliberately LEFT naming that dead + // uplink like any first node. The third, mesh-controller, is deliberately LEFT naming that dead // registry — the installer overwrites it, and leaving it proves that it does. // // And the broker's advertised address. The template hardcodes 192.0.2.10:5671, the old // separate-anchor address; a token carries MESH_BROKER_ADDRESS verbatim as the endpoint an - // enrolling node dials, so with the substrate on novox it must be novox's own public address or + // enrolling node dials, so with the foundation on novox it must be novox's own public address or // every node would enrol against a dead one. The installer refuses to guess this and says so // loudly, which is right — it does not know what this machine is called from outside. const template = bundleFor(images).replaceAll("192.0.2.10:5671", "192.0.2.20:5671"); - const local = join(tmpdir(), `mesh-lab-substrate-${process.pid}.lock`); + const local = join(tmpdir(), `mesh-lab-foundation-${process.pid}.lock`); writeFileSync(local, template); - await push(instanceId, CONTROL, local, "/tmp/substrate-template.lock"); + await push(instanceId, CONTROL, local, "/tmp/foundation-template.lock"); const command = [ BOOTSTRAP_PATH, `--source ${source}`, `--source-ref ${sourceRef}`, - `--bundle /tmp/substrate-template.lock`, + `--bundle /tmp/foundation-template.lock`, `--catalog ${CATALOGUE_ON_MACHINE}`, `--node ${CONTROL}`, `--registry ${MESH_REGISTRY}`, @@ -628,10 +628,10 @@ async function genesis(images: HeldImage[]): Promise { // ------------------------------------------------------------------------------------------ // 1. The control plane answers. Asked of the PERMANENT container by name — `status` opens all - // three stores, so a reply proves the connections it was given are the substrate's own. - const answered = await on(CONTROL, `docker exec mesh-control /mesh-control status`, 60_000); + // three stores, so a reply proves the connections it was given are the foundation's own. + const answered = await on(CONTROL, `docker exec mesh-controller /mesh-controller status`, 60_000); report.push(` control plane ${answered.ok ? answered.out.split("\n")[0] : "NO ANSWER"}`); - if (!answered.ok) return stop("after step 10", `mesh-control does not answer:\n${answered.out}`); + if (!answered.ok) return stop("after step 10", `mesh-controller does not answer:\n${answered.out}`); // 2. The registry replies on /v2/ — the registry API's own "yes, I am one and I am ready". A // container that is up is not a registry that serves. @@ -643,22 +643,22 @@ async function genesis(images: HeldImage[]): Promise { // 3. THE PIVOT COMPLETED. ADR 0067 states this check in as many words: after installing, the // running control plane's image is pinned by a digest THE MESH'S OWN REGISTRY ASSIGNED — not - // by an image id. If it is still an image id, the substrate's container is what is running, + // by an image id. If it is still an image id, the foundation's container is what is running, // nothing was published, and this mesh can never roll out its own upgrades. - const pinnedTo = (await on(CONTROL, `docker inspect --format '{{.Config.Image}}' mesh-control`)) + const pinnedTo = (await on(CONTROL, `docker inspect --format '{{.Config.Image}}' mesh-controller`)) .out.trim(); report.push(` pinned to ${pinnedTo || "(nothing)"}`); if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) { return stop("after step 10", - `mesh-control is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` + + `mesh-controller is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` + `own configuration, which no registry ever served. The pivot did not happen: what is ` + `running is the image the installer carried, not one this mesh published, so this mesh ` + `cannot upgrade itself (novox/hq ADR 0067, "the pivot completed").`); } - if (!new RegExp(`^${MESH_REGISTRY.replaceAll(".", "\\.")}/mesh-control@sha256:[0-9a-f]{64}$`) + if (!new RegExp(`^${MESH_REGISTRY.replaceAll(".", "\\.")}/mesh-controller@sha256:[0-9a-f]{64}$`) .test(pinnedTo)) { return stop("after step 10", - `mesh-control is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` + + `mesh-controller is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` + `digest assigned by ${MESH_REGISTRY}.`); } @@ -669,36 +669,36 @@ async function genesis(images: HeldImage[]): Promise { // outside to one that can — same container, same digest, same registry. The difference is // whether a build happened, and the only place that is visible is the installer saying so. const wanted = sourceRef.slice(0, 8); - if (!new RegExp(`built mesh-control from ${wanted}`).test(said)) { + if (!new RegExp(`built mesh-controller from ${wanted}`).test(said)) { return stop("after the last step", - `the installer never said it built mesh-control from ${wanted}. What runs may have been ` + + `the installer never said it built mesh-controller from ${wanted}. What runs may have been ` + `carried rather than made here, which is a mesh that cannot rebuild its own control plane.`); } - report.push(` built here mesh-control from ${wanted}, by the carried builder`); + report.push(` built here mesh-controller from ${wanted}, by the carried builder`); // 3b. And the registry really serves it, asked of the registry rather than of the container. A // reference is a claim; a tag list is the registry agreeing. const tags = await on(CONTROL, - `curl -s --max-time 10 http://${MESH_REGISTRY}/v2/mesh-control/tags/list`); + `curl -s --max-time 10 http://${MESH_REGISTRY}/v2/mesh-controller/tags/list`); report.push(` registry holds ${tags.out.trim() || "nothing"}`); if (!tags.out.includes("genesis")) { return stop("after step 10", - `${MESH_REGISTRY} does not serve mesh-control, so the digest the container is pinned to ` + + `${MESH_REGISTRY} does not serve mesh-controller, so the digest the container is pinned to ` + `names an image nothing can pull: ${tags.out.trim()}`); } // 4. The temporary control plane is GONE. Two control planes is the half-finished state, and the - // name is the audit: a machine running mesh-control and not temp-mesh-control has pivoted. - const temp = await on(CONTROL, `docker inspect --format '{{.State.Status}}' temp-mesh-control`); - report.push(` temp-mesh-control ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`); + // name is the audit: a machine running mesh-controller and not temp-mesh-controller has pivoted. + const temp = await on(CONTROL, `docker inspect --format '{{.State.Status}}' temp-mesh-controller`); + report.push(` temp-mesh-controller ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`); if (temp.ok) { return stop("after step 10", - `temp-mesh-control is still ${temp.out.trim()}. Two control planes are consuming this ` + + `temp-mesh-controller is still ${temp.out.trim()}. Two control planes are consuming this ` + `mesh's broker queues; neither is wrong and the pivot is not finished.`); } // 5. And the mesh has heard from its one node. Everything the join phase does next depends on it. - const nodes = await on(CONTROL, `docker exec mesh-control /mesh-control node list`); + const nodes = await on(CONTROL, `docker exec mesh-controller /mesh-controller node list`); report.push(` node list ${nodes.out.trim().split("\n").join(" | ")}`); const line = nodes.out.split("\n").map((l) => l.trim()).find((l) => l.startsWith(`${CONTROL} `)); if (!line || !/^\S+\s+here\b/.test(line)) { @@ -715,7 +715,7 @@ async function genesis(images: HeldImage[]): Promise { // ================================================================================================== /** - * ace, shanks and g14 join. Host binary plus a token — no bootstrap, no substrate, no registry. + * ace, shanks and g14 join. Host binary plus a token — no bootstrap, no foundation, no registry. * * **novox is not in this loop.** It was enrolled by the installer, as part of becoming a mesh, and * enrolling it again would present the mesh with a second identity for a node it already knows — @@ -866,7 +866,7 @@ test("the full mesh forms across the access point and both server sets converge" const known = added.get(name); if (known !== undefined) return known; const { manifest, broker } = loadManifest(name); - await must(CONTROL, `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must(CONTROL, `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); added.set(name, broker); return broker; @@ -892,7 +892,7 @@ test("the full mesh forms across the access point and both server sets converge" // Operator-provided app credentials (own-secrets), delivered as fake values through `secret accept`. const credentialDelivered = new Map(); for (const name of new Set(CREDENTIALS.map((c) => c.name))) { - await must(CONTROL, `printf %s ${quote(`fake-${name}-value`)} > /tmp/fake-${name} && docker cp /tmp/fake-${name} mesh-control:/fake-${name}`); + await must(CONTROL, `printf %s ${quote(`fake-${name}-value`)} > /tmp/fake-${name} && docker cp /tmp/fake-${name} mesh-controller:/fake-${name}`); } for (const c of CREDENTIALS) { if (!assigned[c.node]!.has(c.module)) { @@ -912,7 +912,7 @@ test("the full mesh forms across the access point and both server sets converge" if (!assigned[s.node]!.has(s.module)) continue; try { const inControl = `/secret-${s.module}-${s.name}`; - await must(CONTROL, `printf %s ${quote(s.value)} > /tmp${inControl} && docker cp /tmp${inControl} mesh-control:${inControl}`); + await must(CONTROL, `printf %s ${quote(s.value)} > /tmp${inControl} && docker cp /tmp${inControl} mesh-controller:${inControl}`); await mesh(`secret accept ${s.node} ${s.module} ${s.name} --from ${inControl}`); } catch (err) { console.log(`OPERATOR SECRET FAILED ${s.node}/${s.module}/${s.name}: ${(err as Error).message.split("\n").slice(0, 2).join(" | ")}`); @@ -1021,7 +1021,7 @@ test("the full mesh forms across the access point and both server sets converge" // and before this bed set a public domain it composed to nothing on every node, silently. // // What is NOT checked here is issuance: whether route-proxy actually obtains a certificate from - // step-ca over ACME. That path is being fixed in mesh-control as this is written, and a bed that + // step-ca over ACME. That path is being fixed in mesh-controller as this is written, and a bed that // gated on it would be reporting somebody else's in-flight work as this bed's failure. // ================================================================================================ const adr: string[] = ["================ ADR 0066: LABELLED ROUTES ================"]; diff --git a/test/integration/whole-mesh-novox.test.ts b/test/integration/whole-mesh-novox.test.ts index edbf11c..85d1dd4 100644 --- a/test/integration/whole-mesh-novox.test.ts +++ b/test/integration/whole-mesh-novox.test.ts @@ -1,11 +1,11 @@ /** * The whole `novox` server's converted service set, installed together on ONE node behind the - * substrate — the whole-catalogue install this rebuild has never actually run. First stage of a + * foundation — the whole-catalogue install this rebuild has never actually run. First stage of a * whole-mesh rehearsal (novox/hq). * - * Topology (proven by assigned-two-node-db.test.ts): the substrate (store, broker, control) rides + * Topology (proven by assigned-two-node-db.test.ts): the foundation (store, broker, control) rides * `anchor` and nothing else; ALL of novox's services ride the `novox` node. novox's own postgres - * provider owns 5432 there, so it cannot co-locate with the substrate store. An overlay is placed so + * provider owns 5432 there, so it cannot co-locate with the foundation store. An overlay is placed so * each consumer's binding `at` resolves to novox's private address and reaches the providers * co-located with it. * @@ -28,7 +28,7 @@ * host ports here (container ports unchanged); the provider ports the consumers actually connect to * (postgres 5432, minio 9000, mongodb 27017, mssql 1433) are left as-is. See REMAP below. * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock + * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock * scripts/build-module-runtime.sh builds one runtime per module that has code; the route-proxy image * is built by scripts/build-route-proxy-image.sh; scenarios/whole-mesh-novox.yml stocks them all * alongside every server image. @@ -42,7 +42,7 @@ import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -55,13 +55,13 @@ const skip = !capability.usable : !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)" + ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" : false; const SCENARIO = "whole-mesh-novox"; const NODE = "novox"; -/** Where the committed module.json files live: the mesh-catalog beside mesh-control. */ +/** Where the committed module.json files live: the mesh-catalog beside mesh-controller. */ const catalogDir = process.env["MESH_LAB_CATALOG"] ?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "") ?? resolve(process.cwd(), "..", "mesh-catalog", "modules"); @@ -103,7 +103,7 @@ const MODULES: { name: string; containers: string[]; node?: boolean }[] = [ * fail2ban declares `capabilities: ["intrusion-prevention"]`, but mesh-host advertises no such * capability: profile/detectors.go defines container-runtime, package-manager, service-manager, * firewall, overlay, seat, privileged and graphical-session — nothing for intrusion-prevention. So - * NO node can ever host fail2ban. Worse, `mesh-control assign` records the assignment even while + * NO node can ever host fail2ban. Worse, `mesh-controller assign` records the assignment even while * reporting it "cannot be applied", and the whole-node `push` then refuses to resolve the ENTIRE node * ("nothing was sent") over that one un-hostable assignment — one bad module blocks every other. It * is therefore left unassigned here so the rest of the set can be proven. (novox/hq — escalated.) @@ -118,8 +118,8 @@ const DROPPED: { name: string; why: string }[] = [ /** * The provable CORE: modules that converge WHOLE on this node (every container up and stable) once - * the substrate resolves and applies the set. This bed gates green on the CORE — a regression in any - * of these turns it red. It is the substrate + all five providers + the four consumers that reach + * the foundation resolves and applies the set. This bed gates green on the CORE — a regression in any + * of these turns it red. It is the foundation + all five providers + the four consumers that reach * their providers and stay up + the four standalone apps. */ const CORE = new Set([ @@ -132,7 +132,7 @@ const CORE = new Set([ * KNOWN GAPS: modules that RESOLVE and are placed, but whose containers do not stay up because the * committed catalog manifest is incomplete (or, for firewall, a node-service expectation is unmet). * They are reported every run with the exact failure, and escalated (novox/hq) — but they do not gate - * green, because the gap is in the catalog/host, not in this bed or the mesh substrate. + * green, because the gap is in the catalog/host, not in this bed or the mesh foundation. * * umami — the mesh-umami provisioner needs the umami server URL and admin password in its * provisioner.env; the manifest wires neither, so it dies "UMAMI url or admin password @@ -185,7 +185,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane, a container on the first node. */ async function mesh(command: string, timeoutMs?: number): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); } /** The pinned reference this scenario's registry serves for a repository. */ @@ -194,9 +194,9 @@ function pinned(reference: string): string { return onTheMachine(reference, held); } -/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */ +/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { - return substrateBundle(bundle, images); + return foundationBundle(bundle, images); } /** @@ -236,7 +236,7 @@ interface NodeState { /** Ask the mesh, in its own terms, what a node has done with what it was sent. Never throws. */ async function nodeState(node: string): Promise { - const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`); + const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`); if (!asked.ok) return { reached: false, applied: false, current: false, waiting: false, raw: asked.out }; let state: { wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[]; @@ -270,12 +270,12 @@ before(async () => { instanceId = raised.instanceId; held = raised.images; - // anchor raises the substrate from its bundle, digests rewritten to the scenario registry's. - await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000); + // anchor raises the foundation from its bundle, digests rewritten to the scenario registry's. + await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); + await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 900_000); const up = await must("anchor", `docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { - assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); + for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { + assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); } // Both machines join the one mesh and run a host so they apply what they are pushed. @@ -316,7 +316,7 @@ test("the whole novox service set resolves, installs and converges on one node i for (const { name } of MODULES) { try { const { manifest, broker } = loadManifest(name); - await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`); + await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); if (broker) { await mesh(`module issue ${name} --node ${NODE}`); diff --git a/test/lastrun.test.ts b/test/lastrun.test.ts index 3e6d0b9..2b19ae4 100644 --- a/test/lastrun.test.ts +++ b/test/lastrun.test.ts @@ -23,13 +23,13 @@ test("a machine that has never run the suite is told so", () => { // The one that matters: it passed, and against code nobody runs any more. test("a run against code that has since changed is not current", () => { const said = judge( - passing("2026-08-31T11:00:00Z", { "mesh-lab": "aaa", "mesh-control": "bbb" }), + passing("2026-08-31T11:00:00Z", { "mesh-lab": "aaa", "mesh-controller": "bbb" }), now, - { "mesh-lab": "aaa", "mesh-control": "ccc" }, + { "mesh-lab": "aaa", "mesh-controller": "ccc" }, ); assert.equal(said.current, false, "a run against changed code was reported as current"); const text = said.lines.join("\n"); - assert.match(text, /mesh-control\s+at bbb, now at ccc/, text); + assert.match(text, /mesh-controller\s+at bbb, now at ccc/, text); assert.match(text, /code that has since changed/, text); }); diff --git a/test/pinning.test.ts b/test/pinning.test.ts index 65a943a..e03769a 100644 --- a/test/pinning.test.ts +++ b/test/pinning.test.ts @@ -16,8 +16,8 @@ import { */ const HELD: HeldImage[] = [ { - requested: "mesh-control:development", - repository: "mesh-control", + requested: "mesh-controller:development", + repository: "mesh-controller", reference: "sha256:" + "a".repeat(64), }, { @@ -51,7 +51,7 @@ test("the repository is the reference without its tag", () => { */ test("only what is built here and published nowhere counts as ours", () => { for (const ours of [ - "mesh-control:development", "mesh-runtime-plex:development", "mesh-route-proxy:development", + "mesh-controller:development", "mesh-runtime-plex:development", "mesh-route-proxy:development", "mesh-provision-postgres@sha256:" + "0".repeat(64), ]) { assert.ok(isMeshBuilt(ours), `${ours} is one of ours and was not recognised`); @@ -61,7 +61,7 @@ test("only what is built here and published nowhere counts as ours", () => { "registry.example:5000/novox/www:latest", // A registry host in front of one of our names does NOT make it ours: it says somebody // published it, so the machine can fetch it from there like anything else. - "registry.example:5000/mesh-control:development", + "registry.example:5000/mesh-controller:development", ]) { assert.ok(!isMeshBuilt(theirs), `${theirs} is not ours and was claimed`); } @@ -96,16 +96,16 @@ test("a third-party image is left exactly as the manifest wrote it", () => { }); test("a reference of ours that already carries a registry is still redirected", () => { - // What the committed substrate bundle looks like: written for a target that had a registry. - const before = `"image": "192.0.2.250:5000/mesh-control@sha256:${"e".repeat(64)}"`; + // What the committed foundation bundle looks like: written for a target that had a registry. + const before = `"image": "192.0.2.250:5000/mesh-controller@sha256:${"e".repeat(64)}"`; assert.equal(pinnedInto(before, HELD), `"image": "sha256:${"a".repeat(64)}"`); }); // A longer repository ending in a shorter one must not be half-replaced. test("a repository that ends in another one is not partly rewritten", () => { - const before = `"image": "our-mesh-control@sha256:${"7".repeat(64)}"`; + const before = `"image": "our-mesh-controller@sha256:${"7".repeat(64)}"`; assert.equal(pinnedInto(before, HELD), before, - "'our-mesh-control' was rewritten because it ends in 'mesh-control'"); + "'our-mesh-controller' was rewritten because it ends in 'mesh-controller'"); }); test("every image in a whole manifest is settled at once", () => { @@ -127,7 +127,7 @@ test("every image in a whole manifest is settled at once", () => { }); test("what a repository is held under can be asked for, and absence is not an empty string", () => { - assert.equal(referenceFor(HELD, "mesh-control"), `sha256:${"a".repeat(64)}`); + assert.equal(referenceFor(HELD, "mesh-controller"), `sha256:${"a".repeat(64)}`); assert.equal(referenceFor(HELD, "mesh-runtime-plex"), undefined); }); @@ -144,7 +144,7 @@ test("what is still a placeholder can be named", () => { */ test("an image the machine cannot fetch by itself is handed over", () => { for (const handed of [ - "mesh-control:development", + "mesh-controller:development", "mesh-runtime-plex:development", `registry.example/novox/www@sha256:${"a".repeat(64)}`, "registry.example:5000/novox/photos-server:latest", diff --git a/test/place.test.ts b/test/place.test.ts index ebb7da1..e8dddbb 100644 --- a/test/place.test.ts +++ b/test/place.test.ts @@ -38,10 +38,10 @@ test("a per-machine entry OVERRIDES `all:`, it does not add to it", () => { // Worth being exact about: a scenario naming one artifact for one machine gets that // artifact, not that artifact plus everything in `all:`. The opposite reading would place // things nobody asked for, which is the shape of fault this lab exists to catch. - const placements = planPlacements(scenario("place:\n all: [host]\n anchor: [substrate]")); + const placements = planPlacements(scenario("place:\n all: [host]\n anchor: [foundation]")); const byMachine = new Map(placements.map((p) => [p.machine, p.artifacts])); - assert.deepEqual(byMachine.get("anchor"), ["substrate"], "anchor should have ONLY substrate"); + assert.deepEqual(byMachine.get("anchor"), ["foundation"], "anchor should have ONLY foundation"); assert.deepEqual(byMachine.get("peer"), ["host"]); }); @@ -74,11 +74,11 @@ scenario: s segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } } machines: anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true } -images: [mesh-control:development, mesh-runtime-redis:development] +images: [mesh-controller:development, mesh-runtime-redis:development] place: { all: [host, runtime] } `); assert.deepEqual(planHeldImages(s), [ - { machine: "anchor", images: ["mesh-control:development", "mesh-runtime-redis:development"] }, + { machine: "anchor", images: ["mesh-controller:development", "mesh-runtime-redis:development"] }, ]); }); @@ -90,16 +90,16 @@ machines: anchor: at: { segment: hosting, address: [192.0.2.10] } egress: true - images: [mesh-control:development] + images: [mesh-controller:development] laptop: at: { segment: hosting, address: [192.0.2.20] } egress: true images: [mesh-runtime-redis:development] -images: [mesh-control:development, mesh-runtime-redis:development] +images: [mesh-controller:development, mesh-runtime-redis:development] place: { all: [host, runtime] } `); assert.deepEqual(planHeldImages(s), [ - { machine: "anchor", images: ["mesh-control:development"] }, + { machine: "anchor", images: ["mesh-controller:development"] }, { machine: "laptop", images: ["mesh-runtime-redis:development"] }, ]); }); @@ -113,7 +113,7 @@ segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } } machines: anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true } bare: { at: { segment: hosting, address: [192.0.2.20] }, egress: true, images: [] } -images: [mesh-control:development] +images: [mesh-controller:development] place: { all: [host, runtime] } `); assert.deepEqual(planHeldImages(s).map((p) => p.machine), ["anchor"]); @@ -132,15 +132,15 @@ place: { all: [host] } test("a tier that does not exist is refused BY NAME", () => { // Named individually rather than refused as a whole, so a scenario placing a host and a - // substrate is told exactly which half the lab cannot do — rather than being told `place:` + // foundation is told exactly which half the lab cannot do — rather than being told `place:` // is unsupported when half of it now works. try { - assertSupported(scenario("place:\n all: [host, substrate]\n peer: [control]")); + assertSupported(scenario("place:\n all: [host, foundation]\n peer: [control]")); assert.fail("expected a refusal"); } catch (err) { assert.ok(err instanceof UnsupportedError); const missing = err.missing.join("\n"); - assert.match(missing, /substrate/, "the substrate was not named"); + assert.match(missing, /foundation/, "the foundation was not named"); assert.match(missing, /control/, "the control plane was not named"); assert.doesNotMatch(missing, /place: host/, "the host is placeable and was refused anyway"); } @@ -172,18 +172,18 @@ test("an image reference is placeable, and a bare 'image:' is not", () => { test("the placeables are host, runtime and an image", () => { assert.ok(isPlaceable("host")); assert.ok(isPlaceable("runtime")); - assert.ok(!isPlaceable("substrate"), "the tiers above tier 0 do not exist yet"); + assert.ok(!isPlaceable("foundation"), "the tiers above tier 0 do not exist yet"); assert.ok(!isPlaceable("control-plane")); }); test("an unplaceable artifact is named, not refused as a whole", () => { - // A scenario placing a host and a substrate is told which half the lab cannot do — refusing + // A scenario placing a host and a foundation is told which half the lab cannot do — refusing // wholesale would send somebody looking for the wrong problem. const scenario = { name: "s", segments: {}, machines: { a: {} as never }, - place: { a: ["host", "runtime", "image:alpine@sha256:x", "substrate"] }, + place: { a: ["host", "runtime", "image:alpine@sha256:x", "foundation"] }, } as unknown as Parameters[0]; assert.throws( @@ -192,7 +192,7 @@ test("an unplaceable artifact is named, not refused as a whole", () => { // Checked as `place: —`, which is how an artifact is REPORTED as // unplaceable. Searching for the bare word matched the message's own list of what CAN // be placed, which mentions runtime — so the test failed on a correct message. - assert.ok(err.message.includes("place: substrate —"), "the unplaceable one is named"); + assert.ok(err.message.includes("place: foundation —"), "the unplaceable one is named"); assert.ok(!err.message.includes("place: image:alpine"), "a placeable one is not"); assert.ok(!err.message.includes("place: runtime —"), "nor is runtime"); assert.ok(!err.message.includes("place: host —"), "nor is host"); diff --git a/test/rebuild.test.ts b/test/rebuild.test.ts index f452700..32af5f5 100644 --- a/test/rebuild.test.ts +++ b/test/rebuild.test.ts @@ -79,9 +79,9 @@ test("the whole-mesh bed hands its anchor no control-plane image", () => { ...Object.values(scenario.machines).flatMap((m) => m.images ?? []), ]; assert.deepEqual( - named.filter((i) => i.startsWith("mesh-control")), + named.filter((i) => i.startsWith("mesh-controller")), [], - "the anchor is handed mesh-control, so genesis would never find out whether the installer " + + "the anchor is handed mesh-controller, so genesis would never find out whether the installer " + "really carries it", ); }); diff --git a/test/supported.test.ts b/test/supported.test.ts index 6a57d20..5154ec3 100644 --- a/test/supported.test.ts +++ b/test/supported.test.ts @@ -60,20 +60,20 @@ place: { all: [host] }`); }); test("a tier above 0 is still refused, and named", () => { - // The refusal narrowed rather than disappearing. A scenario placing a host AND a substrate + // The refusal narrowed rather than disappearing. A scenario placing a host AND a foundation // must be told which half is missing — not that `place:` is unsupported, when half of it // now works. const scenario = parseScenario(`scenario: x segments: { net: { kind: public, cidr: [192.0.2.0/24] } } machines: { a: { at: { segment: net, address: [192.0.2.1] } } } -place: { all: [host, substrate] }`); +place: { all: [host, foundation] }`); try { assertSupported(scenario); assert.fail("should have refused"); } catch (err) { assert.ok(err instanceof UnsupportedError); - assert.equal(err.missing.length, 1, `expected only the substrate: ${err.missing.join(", ")}`); - assert.match(err.missing[0] ?? "", /substrate/); + assert.equal(err.missing.length, 1, `expected only the foundation: ${err.missing.join(", ")}`); + assert.match(err.missing[0] ?? "", /foundation/); assert.match(err instanceof Error ? err.message : "", /silently lacks them/); } }); diff --git a/test/validate.test.ts b/test/validate.test.ts index 065b70e..41d2716 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -280,7 +280,7 @@ test("one of ours in images: is accepted", () => { assert.doesNotThrow(() => parseScenario(`scenario: x segments: { net: { kind: public, cidr: [192.0.2.0/24] } } machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } } -images: [mesh-control:development, mesh-route-proxy:development] +images: [mesh-controller:development, mesh-route-proxy:development] place: { all: [runtime] }`)); }); @@ -288,7 +288,7 @@ test("images: is named by tag — an image ID is not knowable until the image is refuses(`scenario: x segments: { net: { kind: public, cidr: [192.0.2.0/24] } } machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } } -images: ["mesh-control@sha256:${"0".repeat(64)}"] +images: ["mesh-controller@sha256:${"0".repeat(64)}"] place: { all: [runtime] }`, /is pinned by digest/); }); @@ -303,7 +303,7 @@ machines: at: { segment: net, address: [192.0.2.1] } egress: true images: [mesh-runtime-redis:development] -images: [mesh-control:development] +images: [mesh-controller:development] place: { all: [runtime] }`, /is not in this scenario's images/); }); diff --git a/test/warm.test.ts b/test/warm.test.ts index adf2207..3f9be20 100644 --- a/test/warm.test.ts +++ b/test/warm.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { judge, type Warm } from "../src/warm.ts"; const at = "2026-08-31T20:00:00Z"; -const built = { "mesh-lab": "aaa", "mesh-host": "bbb", "mesh-control": "ccc" }; +const built = { "mesh-lab": "aaa", "mesh-host": "bbb", "mesh-controller": "ccc" }; const warm = (over: Partial = {}): Warm => ({ scenario: "two-nodes", instanceId: "mlab-two-nodes-1", images: [], against: built, at, ...over });