Merge pull request 'One-node bed: SDK-by-version, rename, and the store/broker upgrade proofs' (#27) from feat/a-bed-that-hands-over-nothing into main

This commit was merged in pull request #27.
This commit is contained in:
2026-09-16 23:25:34 +02:00
89 changed files with 3030 additions and 737 deletions
+7 -7
View File
@@ -17,7 +17,7 @@ test.
| | **Bootstrap** | **Full** | | | **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 | | 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 | | Exercises | tiers 0 and 1 | tier 2 and above, and modules |
| Exists to | **develop the mesh** | **test what runs on it** | | 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 ```sh
export MESH_LAB_HOST_BINARY=<mesh-host>/mesh-host export MESH_LAB_HOST_BINARY=<mesh-host>/mesh-host
export MESH_LAB_BUNDLE=<mesh-host>/examples/substrate-first-node.lock export MESH_LAB_BUNDLE=<mesh-host>/examples/foundation-first-node.lock
export MESH_LAB_MODULES=<mesh-control>/examples/modules export MESH_LAB_MODULES=<mesh-controller>/examples/modules
export MESH_LAB_BUILDER=<mesh-control>/build/mesh-builder # build/, which is git-ignored export MESH_LAB_BUILDER=<mesh-controller>/build/mesh-builder # build/, which is git-ignored
# The installer. `suite` builds it with mesh-host's `make bootstrap`, which embeds a `docker save` # 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 # 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 # 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-host>/mesh-bootstrap export MESH_LAB_BOOTSTRAP_BINARY=<mesh-host>/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 # 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 # 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. # machine and — until the registry is up — nothing serving anything.
export MESH_LAB_CATALOG=<mesh-catalog>/modules export MESH_LAB_CATALOG=<mesh-catalog>/modules
# Built with `go build -o <path> ./examples/<name>` in mesh-control. # Built with `go build -o <path> ./examples/<name>` in mesh-controller.
export MESH_LAB_PROVISIONER=<somewhere>/postgres-provisioner export MESH_LAB_PROVISIONER=<somewhere>/postgres-provisioner
export MESH_LAB_OBJECTSTORE_PROVISIONER=<somewhere>/objectstore-provisioner export MESH_LAB_OBJECTSTORE_PROVISIONER=<somewhere>/objectstore-provisioner
export MESH_LAB_ROUTE_PROXY=<somewhere>/route-proxy export MESH_LAB_ROUTE_PROXY=<somewhere>/route-proxy
+161
View File
@@ -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": ""
}
]
}
+1 -1
View File
@@ -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 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. 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 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 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. a login that works, a rotation that takes effect, and a revocation that bites.
+3 -3
View File
@@ -8,10 +8,10 @@
# The flow the test drives (OAuth stubbed, so it is the FLOW that is proven, not the vendor): # 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 # 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 -> # 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. # 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 # 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): # digest, which is where the host pulls them from):
# scripts/build-module-runtime.sh anthropic-manager /tmp/anthropic-manager.tar # scripts/build-module-runtime.sh anthropic-manager /tmp/anthropic-manager.tar
@@ -35,7 +35,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# The two model-access runtimes, built by scripts/build-module-runtime.sh into the local daemon and # 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. # loaded onto the machine, which holds them by their own image IDs.
- mesh-runtime-anthropic-manager:development - mesh-runtime-anthropic-manager:development
+2 -2
View File
@@ -1,6 +1,6 @@
# One machine that becomes a mesh and then assigns itself the audit logger. # 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 # 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 # mesh assigns it the audit logger, so its events account is one the mesh delivered, not the
# broker's own (novox/hq ADR 0048). # broker's own (novox/hq ADR 0048).
@@ -20,7 +20,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# The tool runtime with the audit-logger, built by scripts/build-runtime-image.sh into the local # 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. # daemon and loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-audit:development - mesh-runtime-audit:development
+4 -4
View File
@@ -1,6 +1,6 @@
# One machine that becomes a mesh and is then assigned a SECOND wave of modules at once — the ones # 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 # 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. # same one-push co-residence, a different (and heavier) set of modules.
# #
# The four exercise the three converted shapes: # 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 # 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. # 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 # 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 # 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. # (mongo, unifi-controller, marytts, postgres) must be in the local daemon to be stocked.
@@ -31,13 +31,13 @@ machines:
egress: true egress: true
inbound: allow inbound: allow
# Sized up past catalogue-small's 6GiB: this wave carries two heavy JVM/embedded-DB service # 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. # three node runtimes — a dozen containers, two of them memory-hungry at startup.
memory: 8GiB memory: 8GiB
cpus: 4 cpus: 4
images: images:
- mesh-control:development - mesh-controller:development
# The runtimes built by scripts/build-module-runtime.sh and stocked here. marrytts needs none. # The runtimes built by scripts/build-module-runtime.sh and stocked here. marrytts needs none.
- mesh-runtime-mongodb:development - mesh-runtime-mongodb:development
- mesh-runtime-unifi:development - mesh-runtime-unifi:development
+3 -3
View File
@@ -16,7 +16,7 @@
# after enrol and before the push. Each module additionally OWNS its own config directory # after enrol and before the push. Each module additionally OWNS its own config directory
# (/services/{sonarr,radarr}/config), which the mesh does create. # (/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 # 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} # (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. # must be in the local daemon to be stocked.
@@ -32,14 +32,14 @@ machines:
at: { segment: hosting, address: [192.0.2.10] } at: { segment: hosting, address: [192.0.2.10] }
egress: true egress: true
inbound: allow 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 # The Servarr images are lighter than catalogue-apps' JVM pair, so catalogue-small's 6GiB is
# ample headroom. # ample headroom.
memory: 6GiB memory: 6GiB
cpus: 4 cpus: 4
images: images:
- mesh-control:development - mesh-controller:development
# The two runtimes built by scripts/build-module-runtime.sh and stocked here. # The two runtimes built by scripts/build-module-runtime.sh and stocked here.
- mesh-runtime-sonarr:development - mesh-runtime-sonarr:development
- mesh-runtime-radarr:development - mesh-runtime-radarr:development
+2 -2
View File
@@ -11,7 +11,7 @@
# the broker — so "the broker came up" is itself the proof the seed ran, because an unseeded store # the broker — so "the broker came up" is itself the proof the seed ran, because an unseeded store
# crash-loops the broker. # 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 # 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 # 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 # pulls from the internet over its uplink. eclipse-mosquitto:2 must be in the local
@@ -32,7 +32,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
- mesh-runtime-mosquitto:development - mesh-runtime-mosquitto:development
place: place:
+4 -4
View File
@@ -3,14 +3,14 @@
# #
# The per-backend beds each assign one module: postgres (a database provider), minio (an object-store # 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 # 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 # resolve and come up TOGETHER on one node — nothing new about any single module, everything new about
# their co-residence. # their co-residence.
# #
# The four are chosen because none of them share a directory, so the shared-workspace refusal # 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. # (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 # 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 # 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. # 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] } at: { segment: hosting, address: [192.0.2.10] }
egress: true egress: true
inbound: allow 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 # 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. # single-module beds run at 3GiB; co-residence needs the headroom.
memory: 6GiB memory: 6GiB
cpus: 4 cpus: 4
images: images:
- mesh-control:development - mesh-controller:development
# The four per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. plex # 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. # needs no server image in the lab — its runtime serves tools with no Plex to reach.
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
+2 -2
View File
@@ -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 # 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 # 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 # 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, and the machine pulls them from the internet like anything else.
images: images:
- mesh-control:development - mesh-controller:development
place: place:
all: [host, runtime] all: [host, runtime]
+106
View File
@@ -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=<forge url> MESH_LAB_SOURCE_REF=<commit>
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]
+5 -5
View File
@@ -2,10 +2,10 @@
# #
# The smallest thing that proves a mesh can be raised. One machine on a public segment with a way # 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 # 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. # 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 # 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. # must BE it, or the mesh would hand out an endpoint nothing answers on.
scenario: genesis-single scenario: genesis-single
@@ -23,7 +23,7 @@ machines:
# bare machine. A scenario that needs no images can omit this; genesis cannot. # bare machine. A scenario that needs no images can omit this; genesis cannot.
egress: true egress: true
inbound: allow 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. # pivot. Smaller than the four-node bed's anchor, which also carries a whole service set.
memory: 8GiB memory: 8GiB
cpus: 4 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 # 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 # 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: # 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: place:
all: [host, runtime] all: [host, runtime]
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
- mesh-runtime-grafana:development - mesh-runtime-grafana:development
place: place:
+1 -1
View File
@@ -28,7 +28,7 @@ machines:
inbound: allow inbound: allow
images: images:
- mesh-control:development - mesh-controller:development
place: place:
all: [host, runtime] all: [host, runtime]
+8 -8
View File
@@ -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 # 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-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 # 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 # 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 # 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. # 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 # 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 # 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. # 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): # 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 lavinmq /tmp/lavinmq.tar
# scripts/build-module-runtime.sh amqp-ping /tmp/amqp-ping.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 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. # uplink.
scenario: lavinmq-bed scenario: lavinmq-bed
@@ -46,7 +46,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# The lavinmq provider's runtime (reused for its run-once bootstrap and its provisioner) and the # 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 # 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. # and loaded onto the machines, which hold them by their own image IDs.
+3 -3
View File
@@ -11,9 +11,9 @@
# openai.env carries OPENAI_BASE_URL=http://<at>:<port>/v1. The test asserts that URL and that a # openai.env carries OPENAI_BASE_URL=http://<at>:<port>/v1. The test asserts that URL and that a
# request to it reaches the running model server. # 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 # 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 scenario: local-model-bed
segments: segments:
@@ -31,7 +31,7 @@ machines:
disk: 40GiB disk: 40GiB
images: images:
- mesh-control:development - mesh-controller:development
place: place:
all: [host, runtime] all: [host, runtime]
+1 -1
View File
@@ -21,7 +21,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# minio's runtime, built by scripts/build-module-runtime.sh minio (it carries mc), loaded onto # minio's runtime, built by scripts/build-module-runtime.sh minio (it carries mc), loaded onto
# the machine. # the machine.
- mesh-runtime-minio:development - mesh-runtime-minio:development
+8 -8
View File
@@ -1,7 +1,7 @@
# The usage context store, proved end to end (novox/hq ADR 0054). A postgres PROVIDER and the # 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 # 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 substrate store both want host port 5432, # 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 substrate lives on `anchor` and NOTHING else, and `laptop` # 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 # 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. # 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`), # 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. # 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 # 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): # digest, which is where the host pulls them from):
# scripts/build-module-runtime.sh postgres /tmp/postgres.tar # scripts/build-module-runtime.sh postgres /tmp/postgres.tar
@@ -22,7 +22,7 @@ segments:
cidr: [192.0.2.0/24] cidr: [192.0.2.0/24]
machines: machines:
# The substrate ONLY: store, broker, control — three containers. # The foundation ONLY: store, broker, control — three containers.
anchor: anchor:
at: { segment: hosting, address: [192.0.2.10] } at: { segment: hosting, address: [192.0.2.10] }
egress: true egress: true
@@ -30,8 +30,8 @@ machines:
memory: 4GiB memory: 4GiB
cpus: 4 cpus: 4
# The postgres PROVIDER (server + broker-bound runtime) and the model-usage CONSUMER (its run-once # 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 # migrate and its long-lived event runtime). The 5432-vs-foundation conflict is gone because the
# substrate store is on the OTHER node. The runtime images plus postgres:17-alpine are pulled from # 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. # the internet over the uplink; forty gigabytes holds them with room to spare.
laptop: laptop:
at: { segment: hosting, address: [192.0.2.20] } at: { segment: hosting, address: [192.0.2.20] }
@@ -42,7 +42,7 @@ machines:
disk: 40GiB disk: 40GiB
images: images:
- mesh-control:development - mesh-controller:development
# The per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. Each carries # 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. # its module's code — postgres its provisioner, model-usage its consumer, tools and run-once migrate.
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
+56
View File
@@ -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=<forge url> MESH_LAB_SOURCE_REF=<commit>
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]
+2 -2
View File
@@ -11,7 +11,7 @@
# OPENAI_API_KEY (env file + the Codex auth.json). The test asserts the written key equals the one # 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. # 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): # 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 # scripts/build-module-runtime.sh openai-consumer /tmp/openai-consumer.tar
scenario: openai-bed scenario: openai-bed
@@ -30,7 +30,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# The static-key consumer runtime, built by scripts/build-module-runtime.sh into the local daemon and # 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. # loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-openai-consumer:development - mesh-runtime-openai-consumer:development
+2 -2
View File
@@ -1,7 +1,7 @@
# One machine that becomes a mesh and then assigns itself plex's tool runtime. # 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 # 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 # 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 # 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. # mesh — proof the module runs its own code as its own process under its own scoped account.
@@ -21,7 +21,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# Plex's tool runtime, built by scripts/build-module-runtime.sh plex into the local daemon and # 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. # loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-plex:development - mesh-runtime-plex:development
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# postgres's runtime, built by scripts/build-module-runtime.sh postgres (it carries psql), loaded # postgres's runtime, built by scripts/build-module-runtime.sh postgres (it carries psql), loaded
# onto the machine. # onto the machine.
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# Redis's tool+provisioner runtime, built by scripts/build-module-runtime.sh redis into the local # 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. # daemon and loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-redis:development - mesh-runtime-redis:development
+4 -4
View File
@@ -14,9 +14,9 @@
# publicly-trusted certificate and answering an HTTP-01 challenge at the name — is proven separately # 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. # 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 # 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 # 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. # fixed page over a busybox nc loop. Both images are stocked and served by digest.
scenario: route-forwarding scenario: route-forwarding
@@ -35,8 +35,8 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# The route-proxy's image, built from the canonical Go proxy in mesh-control by # 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. # scripts/build-route-proxy-image.sh, and hello-web's backend, a bare alpine nc loop.
- mesh-route-proxy:development - mesh-route-proxy:development
+2 -2
View File
@@ -14,7 +14,7 @@
# - the container fires when the cron is due (top of the next minute); # - 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. # - 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 # 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. # 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 # There is no runtime image: schedtest carries no code of its own — the scheduled container is a
@@ -35,7 +35,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
place: place:
# Only the host — schedtest has no mesh-runtime to place. The tick image is pulled from the # Only the host — schedtest has no mesh-runtime to place. The tick image is pulled from the
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
- mesh-runtime-sonarr:development - mesh-runtime-sonarr:development
place: place:
+2 -2
View File
@@ -9,7 +9,7 @@
# built lazily and never throws at registration, so the runtime logs `[mesh-tools] serving 3 tool(s)` # 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. # 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 # 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 # local daemon, which the machine pulls from the internet over its uplink. confluence
# needs no service image — it is tools-only. # needs no service image — it is tools-only.
@@ -29,7 +29,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# confluence's runtime, built by scripts/build-module-runtime.sh confluence into the local daemon # 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 # 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. # service image: confluence is tools-only and outbound-only.
+2 -2
View File
@@ -10,7 +10,7 @@
# throws at registration, so the runtime logs `[mesh-tools] serving 23 tool(s)` and binds its serve # 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. # 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 # 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 # daemon, which the machine pulls from the internet over its uplink. gitlab needs no
# service image — it is tools-only. # service image — it is tools-only.
@@ -30,7 +30,7 @@ machines:
cpus: 2 cpus: 2
images: images:
- mesh-control:development - mesh-controller:development
# gitlab's runtime, built by scripts/build-module-runtime.sh gitlab into the local daemon and # 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 # loaded onto the machine, which holds it by its own image ID. There is no
# service image: gitlab is tools-only and outbound-only. # service image: gitlab is tools-only and outbound-only.
+6 -6
View File
@@ -1,13 +1,13 @@
# The DB-consumer chain a single node cannot host, proved across two machines. # 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 # 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 substrate # 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 — # (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 # 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, # 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 # 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 # no cross-node module comms and no overlay are needed — and the 5432-vs-foundation conflict is gone
# because the substrate store is on the OTHER node. # because the foundation store is on the OTHER node.
scenario: two-node-db scenario: two-node-db
segments: segments:
@@ -16,7 +16,7 @@ segments:
cidr: [192.0.2.0/24] cidr: [192.0.2.0/24]
machines: 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 # 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. # containers on a node, which this one never does.
anchor: anchor:
@@ -43,7 +43,7 @@ machines:
disk: 60GiB disk: 60GiB
images: images:
- mesh-control:development - mesh-controller:development
# The per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. Each carries # 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 # its module's provisioner, so no separate mesh-provision-* image is listed — the runtime is the
# provisioner (ADR 0048). # provisioner (ADR 0048).
+2 -2
View File
@@ -17,7 +17,7 @@ machines:
at: { segment: hosting, address: [192.0.2.10] } at: { segment: hosting, address: [192.0.2.10] }
egress: true egress: true
inbound: allow 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 # 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 # 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. # and the forge test fails on a status poll that is merely queued behind page-outs.
@@ -30,7 +30,7 @@ machines:
memory: 2GiB memory: 2GiB
images: images:
- mesh-control:development - mesh-controller:development
# And the builder, because it is a module the mesh assigns rather than a program somebody # 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. # starts by hand — which is the only way its credential can be one the mesh delivered.
- mesh-builder:development - mesh-builder:development
+7 -7
View File
@@ -1,15 +1,15 @@
# The whole `ace` server's converted service set, installed together on ONE node behind the mesh # 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. # 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 # `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/ # 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 # 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 — # 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. # 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 # 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 # 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 # 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 memory: 4GiB
cpus: 4 cpus: 4
disk: 20GiB 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. # 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, # 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. # Letta ~1.8GiB, Baserow ~1.5GiB, the UniFi controller's JVM, mssql ~2GiB). Sized past novox.
ace: ace:
@@ -42,7 +42,7 @@ machines:
memory: 18GiB memory: 18GiB
cpus: 8 cpus: 8
disk: 120GiB 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: images:
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
- mesh-runtime-redis:development - mesh-runtime-redis:development
@@ -70,7 +70,7 @@ machines:
- mesh-runtime-unifi:development - mesh-runtime-unifi:development
images: images:
- mesh-control:development - mesh-controller:development
# The per-module runtimes (built by scripts/build-module-runtime.sh). # The per-module runtimes (built by scripts/build-module-runtime.sh).
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
- mesh-runtime-redis:development - mesh-runtime-redis:development
+12 -12
View File
@@ -1,19 +1,19 @@
# The FULL mesh in its REAL production shape: two segments, one access point, one overlay. # 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 # 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, # 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 # 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. # — the access point — reachable from the outside only through what they dial out to.
# #
# hosting (public, routable) home (private, behind the access point) # 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 # 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) # overlay hub, ingress g14 10.99.1.30 workstation (light)
# #
# The `home` gateway masquerades v4 outbound and forwards inbound (an ordinary household router). # 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 # 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 # (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 # 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 # 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. # 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 # 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 substrate broker binds # 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 # 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. # 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 # 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 # 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 # 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 # 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 # 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. # already did it.
# #
# The images: are the UNION of the novox set (feat/novox-conversions @ 431310f: the slug + roundcube # The images: are the UNION of the novox set (feat/novox-conversions @ 431310f: the slug + roundcube
@@ -76,8 +76,8 @@ segments:
mapping_ttl: 120s mapping_ttl: 120s
machines: machines:
# The anchor: substrate (store, broker, control) + the whole novox service set + overlay hub + # 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 substrate too. # public ingress. Bigger than the flat bed's novox, because it now carries the foundation too.
novox: novox:
at: { segment: hosting, address: [192.0.2.20] } at: { segment: hosting, address: [192.0.2.20] }
egress: true 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 # because they carry no runtime image of their own — what they run is third-party or is the
# node itself. # 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 # 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 # 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 # a mesh. Handing it over from the workstation as well would mean the bed never found out
+9 -9
View File
@@ -1,10 +1,10 @@
# The whole `novox` server's converted service set, installed together on ONE node behind the mesh # 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). # 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 # 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 # 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 # consumer's binding `at` resolves to novox's private address and every consumer reaches the
# providers co-located with it. # providers co-located with it.
@@ -15,7 +15,7 @@
# apps portainer verdaccio registry route-proxy mailu # apps portainer verdaccio registry route-proxy mailu
# node-level firewall fail2ban # 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 # 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, # 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 # 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] cidr: [192.0.2.0/24]
machines: machines:
# The substrate ONLY: store, broker, control. Nothing else lands here. # The foundation ONLY: store, broker, control. Nothing else lands here.
anchor: anchor:
at: { segment: hosting, address: [192.0.2.10] } at: { segment: hosting, address: [192.0.2.10] }
egress: true egress: true
@@ -37,9 +37,9 @@ machines:
memory: 4GiB memory: 4GiB
cpus: 4 cpus: 4
disk: 20GiB 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. # 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 # 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 # 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 # 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 # layers and the runtimes. A hundred gigabytes holds the whole set without exhausting the disk
# mid-apply. # mid-apply.
disk: 100GiB 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: images:
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
- mesh-runtime-redis:development - mesh-runtime-redis:development
@@ -73,7 +73,7 @@ machines:
- mesh-route-proxy:development - mesh-route-proxy:development
images: images:
- mesh-control:development - mesh-controller:development
# The per-module runtimes (built by scripts/build-module-runtime.sh). registry, route-proxy, # 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. # invoicing, firewall and fail2ban carry no mesh-runtime image; route-proxy ships its own.
- mesh-runtime-postgres:development - mesh-runtime-postgres:development
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build the route-proxy module's runtime image: the reference reverse proxy (novox/hq # 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. # lab can stock and serve by digest.
# #
# Unlike the TypeScript modules (built by build-module-runtime.sh into a node tool runtime), the # 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 # 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. # docker daemon, which a scenario's registry then stocks and serves by digest.
# #
# build-route-proxy-image.sh # build-route-proxy-image.sh
@@ -13,7 +13,7 @@
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"; ROOT="$(cd "$HERE/.." && pwd)" 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}" MESH_CATALOG="${MESH_CATALOG:-$ROOT/mesh-catalog}"
TAG="${ROUTE_PROXY_TAG:-mesh-route-proxy:development}" TAG="${ROUTE_PROXY_TAG:-mesh-route-proxy:development}"
DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile" 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" ] || { [ -f "$MESH_CONTROL/examples/route-proxy/main.go" ] || {
echo "no proxy source at $MESH_CONTROL/examples/route-proxy" >&2; exit 1; } 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. # its examples/route-proxy package.
docker build -f "$DOCKERFILE" -t "$TAG" "$MESH_CONTROL" docker build -f "$DOCKERFILE" -t "$TAG" "$MESH_CONTROL"
echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)" echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)"
+2 -2
View File
@@ -103,7 +103,7 @@ export interface Machine {
* and a handful of containers. * and a handful of containers.
* *
* Declared, because it is a fact about the machine the scenario describes — the node that runs * 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 * 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 * 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). * (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 * **The mesh's own images** — the ones that exist in no registry and are put onto a machine by
* whoever built them. * 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 * 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 * 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 * their own image configuration. Written as tags, because a tag is what `docker save` can
+49 -6
View File
@@ -7,7 +7,7 @@
* from the internet, and that is now the thing worth proving before a raise says it is finished. * 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 * 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. * 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 * Two things are checked, in this order, because they fail differently and the difference is the
@@ -89,7 +89,38 @@ export async function confirmEgress(
// the retry is tightened so a silent server costs seconds rather than the step. A broken uplink // 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. // still fails the check above, before any of this.
await resilientResolver(name); 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<number> {
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<boolean> { async function resolves(name: string, waitSeconds: number): Promise<boolean> {
@@ -140,11 +171,22 @@ async function reaches(name: string, waitSeconds: number): Promise<string | null
* The shape of the problem is visible in `resolvectl status`: the machine has sensible global * The shape of the problem is visible in `resolvectl status`: the machine has sensible global
* fallbacks, and the *link* carrying the default route has exactly one server — the uplink gateway. * fallbacks, and the *link* carrying the default route has exactly one server — the uplink gateway.
* resolved will not reach for a global fallback while the link it is using has a server of its own, * resolved will not reach for a global fallback while the link it is using has a server of its own,
* so one unanswered packet is one failed lookup. Under four machines pulling images at once that * so one unanswered packet is one failed lookup.
* happens, and it has ended three runs long after the egress check passed.
* *
* The uplink stays first, so the modelled path is still the one used and still proven by the check * **The uplink goes last, and this is a preference, not a fix.** The uplink's resolver is a single
* above. The others answer only when it does not. * dnsmasq with no unique knowledge any scenario machine needs — machines here address each other by
* address, and the mesh writes its own names into /etc/hosts — so there is no reason for anything
* to wait on it. It is kept, last, so DHCP-supplied names still answer.
*
* **It is written down as a preference because it was once mistaken for the cure.** Lookups were
* timing out two times in three; the uplink was first in the list; reordering it made five in five;
* the conclusion drew itself and was wrong. The machines were sharing ONE DHCP lease (see
* `distinguishMachines` in raise.ts), so which machine could resolve anything depended on which had
* last won an ARP race — and reordering resolvers on a machine that has just won looks exactly like
* a fix. Nothing below this line will save a bed whose machines share an address.
*
* The caches are flushed afterwards, because resolved remembers the failures it collected while
* the bad server was in front.
*/ */
async function resilientResolver(name: string): Promise<void> { async function resilientResolver(name: string): Promise<void> {
await incus([ await incus([
@@ -152,7 +194,8 @@ async function resilientResolver(name: string): Promise<void> {
`link=$(ip -4 route show default | awk '{print $5}' | head -n1); ` + `link=$(ip -4 route show default | awk '{print $5}' | head -n1); ` +
`via=$(ip -4 route show default | awk '{print $3}' | head -n1); ` + `via=$(ip -4 route show default | awk '{print $3}' | head -n1); ` +
`if [ -n "$link" ] && command -v resolvectl >/dev/null 2>&1; then ` + `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); ], 30_000);
} }
+25 -2
View File
@@ -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 * 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. * 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 * 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 * that declares something and raises without it is the fault this lab was built to catch
* (novox/hq 04-ISSUES/003). * (novox/hq 04-ISSUES/003).
@@ -515,7 +515,7 @@ export async function loadHeldImages(
throw new Error( throw new Error(
`${requested} is not on this workstation, so there is nothing to hand the machines.\n` + `${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` + ` 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; 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<void> {
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}`);
}
+92 -1
View File
@@ -307,9 +307,18 @@ export async function raise(
enter("waiting for machines to become usable"); enter("waiting for machines to become usable");
await waitUntilAllUsable(created, readyTimeout, log); 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"); enter("applying declared addresses");
await applyAddresses(scenario, instanceId, byMachine, log); 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. // Transit first: a gateway's default route points at it, so it has to exist.
enter("wiring the public networks together"); enter("wiring the public networks together");
const transit = await raiseTransit(scenario, instanceId, log); 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 // **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. // 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 // 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"); enter("confirming egress reaches the internet");
await confirmEgress(scenario, byMachine, log); await confirmEgress(scenario, byMachine, log);
@@ -356,3 +365,85 @@ export async function raise(
throw new RaiseError(instanceId, step, cause); 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<void> {
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<string, string>,
log: (m: string) => void,
): Promise<void> {
const held = new Map<string, string[]>();
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.`,
);
}
+1 -1
View File
@@ -36,7 +36,7 @@ export function assertSupported(scenario: Scenario): void {
// `host`, `runtime` and `image:<reference>` work. Everything else in the vocabulary is named // `host`, `runtime` and `image:<reference>` work. Everything else in the vocabulary is named
// individually rather than refused as a whole, so a scenario that places a host and a // 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<string>(); const unplaceable = new Set<string>();
for (const { artifacts } of planPlacements(scenario)) { for (const { artifacts } of planPlacements(scenario)) {
for (const artifact of artifacts) { for (const artifact of artifacts) {
+2 -2
View File
@@ -3,7 +3,7 @@
* *
* **A digest is not knowable until something is built** (novox/hq 04-ISSUES/025). A manifest in a * **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 * 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 * 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, * digest to write down. The catalogue ships sixty-four zeros for them, which parses, resolves,
* composes — and stops on the machine. * 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 * **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 * 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-<something>` * mesh-controller and `scripts/build-module-runtime.sh` here both tag their output `mesh-<something>`
* with no registry host and no upstream organisation — that is what "built here, published * 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. * nowhere" looks like, and a hardcoded list would go stale the first time a module is added.
* *
+3 -3
View File
@@ -45,7 +45,7 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] {
env: { CGO_ENABLED: "0" }, env: { CGO_ENABLED: "0" },
}); });
} }
const control = where["mesh-control"]; const control = where["mesh-controller"];
if (control) { if (control) {
// **Every image the lab runs, not only the control plane's.** // **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 // 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 // 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. // was stale would be a bed proving something about last week's procedure.
const installer = env["MESH_LAB_BOOTSTRAP_BINARY"]; const installer = env["MESH_LAB_BOOTSTRAP_BINARY"];
if (installer && where["mesh-host"]) { 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 * 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. * 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 * one place. Overridable because a release installer carries a release image, and nothing about
* that is the lab's business. * that is the lab's business.
*/ */
+1 -1
View File
@@ -23,6 +23,6 @@ export function repositories(env: NodeJS.ProcessEnv = process.env): Repositories
const host = env["MESH_LAB_HOST_BINARY"]; const host = env["MESH_LAB_HOST_BINARY"];
if (host) found["mesh-host"] = dirname(host); if (host) found["mesh-host"] = dirname(host);
const modules = env["MESH_LAB_MODULES"]; const modules = env["MESH_LAB_MODULES"];
if (modules) found["mesh-control"] = dirname(dirname(modules)); if (modules) found["mesh-controller"] = dirname(dirname(modules));
return found; return found;
} }
+17 -17
View File
@@ -10,7 +10,7 @@
* **What changed from the earlier cut, and why this is simpler.** The refresh token no longer rides a * **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 * 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 * 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 * 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. * 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 * 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. * ~/.claude/.credentials.json, access-token-only.
* *
* STUBBED, and flagged in the report: (a) the vendor OAuth endpoint (a node stub); (b) the submit * 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 * 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. * 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: * 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-manager /tmp/anthropic-manager.tar
* scripts/build-module-runtime.sh anthropic-consumer /tmp/anthropic-consumer.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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -61,7 +61,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "anthropic-bed"; const SCENARIO = "anthropic-bed";
@@ -96,22 +96,22 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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 // 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 // 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 // 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 // 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. // operator who ran adopt owns the file and this does not arise.
async function stageIntoControl(hostPath: string, dest: string): Promise<void> { async function stageIntoControl(hostPath: string, dest: string): Promise<void> {
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 }> { 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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -173,11 +173,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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 add /anthropic-manager.json`);
await mesh(`module issue anthropic-manager --node ${MACHINE}`); await mesh(`module issue anthropic-manager --node ${MACHINE}`);
await mesh(`assign ${MACHINE} anthropic-manager`); 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 add /anthropic-consumer.json`);
await mesh(`module issue anthropic-consumer --node ${MACHINE}`); await mesh(`module issue anthropic-consumer --node ${MACHINE}`);
await mesh(`assign ${MACHINE} anthropic-consumer`); await mesh(`assign ${MACHINE} anthropic-consumer`);
+15 -15
View File
@@ -7,10 +7,10 @@
* container that connects over amqps with that account — never the broker's own. The trail filling * 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. * 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_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, * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -34,7 +34,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "audit-node"; const SCENARIO = "audit-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -89,7 +89,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -121,12 +121,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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"); await mesh("module add /audit.json");
// The mesh issues its scoped account and seals it to this machine, then assigns and pushes it. // 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 // 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 // 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`); 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}`); assert.match(users, /anchor-audit-logger/, `the scoped account is not on the broker:\n${users}`);
}); });
@@ -17,7 +17,7 @@
* *
* All are assigned to the one anchor, pushed ONCE, and the node converges ONCE with every one up. * 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 * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -42,7 +42,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "catalogue-apps"; const SCENARIO = "catalogue-apps";
@@ -73,7 +73,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -97,7 +97,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -129,12 +129,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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 ---------------------------------- // --- add, issue (those that serve/emit), assign, then ONE push ----------------------------------
async function add(name: string, manifest: string): Promise<void> { async function add(name: string, manifest: string): Promise<void> {
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 add /${name}.json`);
} }
@@ -23,7 +23,7 @@
* *
* All is assigned to the one anchor, pushed ONCE, and the node converges ONCE with both modules up. * 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; * 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 * 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 * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -50,7 +50,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "catalogue-media"; const SCENARIO = "catalogue-media";
@@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -105,7 +105,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -137,12 +137,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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 ------------------ // --- add, issue (both emit events → each gets a scoped broker account), assign ------------------
async function add(name: string, manifest: string): Promise<void> { async function add(name: string, manifest: string): Promise<void> {
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 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 // 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 // sent in a single declaration. A refusal here (nonzero, or "could not be resolved") is the
// regression this bed exists to catch. // 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, assert.ok(pushed.ok,
`the co-resident push was REFUSED — the shared-access collision ADR 0051 removed is back:\n${pushed.out}`); `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/, assert.doesNotMatch(pushed.out, /could not be resolved|both declare the path|shared data is the operator/,
@@ -22,7 +22,7 @@
* the seed), and mosquitto's provisioner — running in the assigned runtime — creates a scoped client * 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. * 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 * scripts/build-module-runtime.sh mosquitto builds mesh-runtime-mosquitto:development (carrying
* mosquitto_ctrl and the compiled bootstrap entrypoint) into the local daemon, which * 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 * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -48,7 +48,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "catalogue-mqtt"; const SCENARIO = "catalogue-mqtt";
@@ -78,7 +78,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -102,7 +102,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -134,12 +134,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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. // 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": {} }, serves: { "mqtt-topic": {} },
emits: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"], emits: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
// The events entrypoint subscribes to its own lifecycle events (an audit log), so it consumes // 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"], consumes: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
receives: { "mqtt-topic": "/var/lib/mosquitto-module/grants/mesh.json" }, receives: { "mqtt-topic": "/var/lib/mosquitto-module/grants/mesh.json" },
grants: { "mqtt-topic": "/var/lib/mosquitto-module/grants" }, 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"); await mesh("module add /mosquitto.json");
const issued = await mesh(`module issue mosquitto --node ${MACHINE}`); const issued = await mesh(`module issue mosquitto --node ${MACHINE}`);
assert.match(issued, /scoped to what it emits and consumes/, issued); 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, // 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 // 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 // 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. // — a provisioner that invented its own would refuse the connection.
const consumerPw = "mesh-minted-mqtt-7b2e1a"; const consumerPw = "mesh-minted-mqtt-7b2e1a";
await must(`printf %s ${quote(consumerPw)} > /var/lib/mosquitto-module/grants/app.secret`); await must(`printf %s ${quote(consumerPw)} > /var/lib/mosquitto-module/grants/app.secret`);
@@ -18,7 +18,7 @@
* path is fulfilled by creating the consumer's login with the mesh-minted password — it seals nothing * 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). * 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 * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -43,7 +43,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "catalogue-small"; const SCENARIO = "catalogue-small";
@@ -74,7 +74,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -104,7 +104,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -136,12 +136,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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 ------------------------------- // --- add, issue (the four that serve/emit), assign, then ONE push -------------------------------
async function add(name: string, manifest: string): Promise<void> { async function add(name: string, manifest: string): Promise<void> {
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 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 // 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 // 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 // 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 // the consumer with the mesh's password (PONG) — a provisioner that invented its own would answer
// WRONGPASS. // WRONGPASS.
const redisPassword = "mesh-minted-9f3c2a"; const redisPassword = "mesh-minted-9f3c2a";
+12 -12
View File
@@ -10,7 +10,7 @@
* Grafana — that the serve queue is bound is the proof the settings reached the runtime and its * Grafana — that the serve queue is bound is the proof the settings reached the runtime and its
* tools loaded from them. * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -34,7 +34,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "grafana-node"; const SCENARIO = "grafana-node";
@@ -63,7 +63,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -85,7 +85,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -117,11 +117,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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"); await mesh("module add /grafana.json");
// The operator states grafana's URL and API token as settings for this node — the config the // 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. // 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" }); 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}`); await mesh(`settings set grafana /grafana-settings.json --node ${MACHINE}`);
const issued = await mesh(`module issue grafana --node ${MACHINE}`); const issued = await mesh(`module issue grafana --node ${MACHINE}`);
+19 -19
View File
@@ -1,7 +1,7 @@
/** /**
* The usage context store, proved end to end (novox/hq ADR 0054). * 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 * 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 * 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 * 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; * 3. IN THE CLEAR — the value and its raw payload are ordinary columns an ordinary select returns;
* nothing about usage is sealed. * nothing about usage is sealed.
* *
* The topology mirrors two-node-db: the app-postgres provider and the mesh's own substrate store both * The topology mirrors two-node-db: the app-postgres provider and the mesh's own foundation store both
* want host port 5432, so the substrate (store, broker, control) owns `anchor` and NOTHING else, and * 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 * `laptop` runs the postgres PROVIDER and the model-usage CONSUMER co-located. Only enrolment crosses
* to anchor, over the underlay both machines share. * 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_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): * 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 * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -50,12 +50,12 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "model-usage-bed"; const SCENARIO = "model-usage-bed";
/** The node that carries the postgres provider and the model-usage consumer. anchor carries only the /** The node that carries the postgres provider and the model-usage consumer. anchor carries only the
* substrate. */ * foundation. */
const NODE = "laptop"; const NODE = "laptop";
let instanceId = ""; 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. */ /** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -120,7 +120,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise<void> {
} | undefined; } | undefined;
let said = ""; let said = "";
try { 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; said = asked.out;
if (asked.ok) state = JSON.parse(said); if (asked.ok) state = JSON.parse(said);
} catch (err) { } catch (err) {
@@ -160,11 +160,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`); const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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) { 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<void> { async function addIssueAssign(name: string, manifest: string): Promise<void> {
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 add /${name}.json`);
await mesh(`module issue ${name} --node ${NODE}`); await mesh(`module issue ${name} --node ${NODE}`);
await mesh(`assign ${NODE} ${name}`); 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 mesh(`push ${NODE}`);
await settled(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}}'`); const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`);
for (const name of ["postgres", "mesh-postgres", "mesh-model-usage"]) { for (const name of ["postgres", "mesh-postgres", "mesh-model-usage"]) {
assert.match(onLaptop, new RegExp(`(^|\\n)${name}(\\n|$)`), 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}`); 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 // 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 // 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 // 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 // `-e MESH_MODULE=...`. Write permission is per-exchange, not per-key, so postgres may carry any
+15 -15
View File
@@ -10,10 +10,10 @@
* proof the invocation routed to the assigned runtime, ran plex's real code, and replied, all under * 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. * 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_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, * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -37,7 +37,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "plex-node"; const SCENARIO = "plex-node";
@@ -68,7 +68,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -92,7 +92,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -124,12 +124,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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"); await mesh("module add /plex.json");
// The mesh issues plex's scoped account and seals it to this machine, then assigns and pushes it. // 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}`); `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 // 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 // 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 // (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. // to the assigned runtime and ran plex's real code under its scoped account.
+13 -13
View File
@@ -12,9 +12,9 @@
* has no way yet to deliver one to a provider's runtime (04-ISSUES). The manifest here sets a * 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. * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -38,7 +38,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "redis-node"; const SCENARIO = "redis-node";
@@ -67,7 +67,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -89,7 +89,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -121,11 +121,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); await mesh(`node add ${MACHINE}`);
@@ -151,7 +151,7 @@ test("the mesh assigns redis, and its runtime serves tools and provisions grants
version: "1", version: "1",
emits: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], 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 // 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). // crashes on start with a 404 (novox/hq ADR 0046: a consume is declared).
consumes: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], consumes: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"],
"own-secrets": { default: "/var/lib/redis-module/default.secret", broker: "/var/lib/mesh/redis/broker" }, "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"); await mesh("module add /redis.json");
const issued = await mesh(`module issue redis --node ${MACHINE}`); const issued = await mesh(`module issue redis --node ${MACHINE}`);
+14 -14
View File
@@ -25,8 +25,8 @@
* registry by digest; the host pulls and runs it on the cadence. * 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_HOST_BINARY=.../mesh-host (feat/apply-schedule — the scheduler that fires the step)
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock * MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* MESH_LAB_MODULES=.../mesh-control/examples/modules (feat/schedule-container — the parser that * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -50,7 +50,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "schedule-tick"; const SCENARIO = "schedule-tick";
@@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -119,7 +119,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -151,12 +151,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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"); await mesh("module add /schedtest.json");
// No `module issue`: schedtest serves/consumes nothing and carries no runtime, so it needs no // 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. // broker account. `assign` resolves its plan (no own-secret to fill) and ONE push converges it.
+11 -11
View File
@@ -8,7 +8,7 @@
* Sonarr here), registers its tools, and serves them under a scoped account. There is no live Sonarr * 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. * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -32,7 +32,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "sonarr-node"; const SCENARIO = "sonarr-node";
@@ -61,7 +61,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -83,7 +83,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -115,11 +115,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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"); await mesh("module add /sonarr.json");
const issued = await mesh(`module issue sonarr --node ${MACHINE}`); const issued = await mesh(`module issue sonarr --node ${MACHINE}`);
@@ -17,7 +17,7 @@
* A tool would only fail if it were actually invoked without real creds — which this bed does not do, * 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. * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -41,7 +41,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "tools-confluence"; const SCENARIO = "tools-confluence";
@@ -72,7 +72,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -96,7 +96,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -128,12 +128,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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"); await mesh("module add /confluence.json");
// confluence serves tools, so it is issued a scoped broker account (it emits/consumes nothing else). // 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}`); const issued = await mesh(`module issue confluence --node ${MACHINE}`);
+13 -13
View File
@@ -16,7 +16,7 @@
* A tool would only fail if it were actually invoked without real creds — which this bed does not do, * 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. * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -40,7 +40,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "tools-gitlab"; const SCENARIO = "tools-gitlab";
@@ -71,7 +71,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -95,7 +95,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -127,12 +127,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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"); await mesh("module add /gitlab.json");
// gitlab serves tools, so it is issued a scoped broker account (it emits/consumes nothing else). // 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}`); const issued = await mesh(`module issue gitlab --node ${MACHINE}`);
+30 -30
View File
@@ -1,18 +1,18 @@
/** /**
* The DB-consumer chain a single node cannot host, proved across two machines. * 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 * 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 substrate and got * 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 * 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 * 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 * 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 * 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, * 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. * provider owns laptop's 5432 uncontested.
* *
* The four manifests are the committed catalogue shapes (novox/hq ADR 0039/0047/0048), verbatim * 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 * 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. * 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_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): * 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 * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -56,11 +56,11 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "two-node-db"; 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"; const NODE = "laptop";
let instanceId = ""; 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. */ /** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -128,7 +128,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise<void> {
} | undefined; } | undefined;
let said = ""; let said = "";
try { 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; said = asked.out;
if (asked.ok) state = JSON.parse(said); if (asked.ok) state = JSON.parse(said);
} catch (err) { } catch (err) {
@@ -169,13 +169,13 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; 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. // 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", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`); const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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}-`); await destroyAll(`${SCENARIO}-`);
}, { timeout: 600_000 }); }, { 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, skip, timeout: 1_500_000,
}, async () => { }, 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 ------------------------- // --- add, issue a scoped broker account, assign to laptop, then ONE push -------------------------
async function addIssueAssign(name: string, manifest: string): Promise<void> { async function addIssueAssign(name: string, manifest: string): Promise<void> {
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 add /${name}.json`);
const issued = await mesh(`module issue ${name} --node ${NODE}`); const issued = await mesh(`module issue ${name} --node ${NODE}`);
assert.match(issued, /scoped to what it emits and consumes/, issued); 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 // 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 // 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 // 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. // 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 // 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 // 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("postgres", postgresManifest);
await addIssueAssign("redis", redisManifest); await addIssueAssign("redis", redisManifest);
await addIssueAssign("baserow", baserowManifest); 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); 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 onAnchor = await must("anchor", `docker ps --format '{{.Names}}'`);
const onLaptop = await must(NODE, `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|$)/, assert.doesNotMatch(onAnchor, /(^|\n)postgres(\n|$)/,
"the postgres provider landed on the substrate node — the 5432 collision this bed exists to avoid"); "the postgres provider landed on the foundation node — the 5432 collision this bed exists to avoid");
assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the substrate store leaked onto the second node"); 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"); 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 // 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 // ${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 // 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 // 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. // 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. // 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`); const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`);
+8 -8
View File
@@ -9,7 +9,7 @@
* agree over a wire. Everything either side of the wire is already asserted in its own suite. * 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_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 * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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 type { HeldImage } from "../../src/pinning.ts";
import { incus } from "../../src/incus/client.ts"; import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts"; import { machineName } from "../../src/lifecycle/names.ts";
@@ -35,7 +35,7 @@ const skip = !capability.usable
: !host || !existsSync(host) : !host || !existsSync(host)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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) : !builder || !existsSync(builder)
? "MESH_LAB_BUILDER is not set to a built mesh-builder" ? "MESH_LAB_BUILDER is not set to a built mesh-builder"
: false; : false;
@@ -79,12 +79,12 @@ async function must(command: string): Promise<string> {
} }
async function mesh(command: string): Promise<string> { async function mesh(command: string): Promise<string> {
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. */ /** The bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
before(async () => { before(async () => {
@@ -92,8 +92,8 @@ before(async () => {
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {}); const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {});
instanceId = raised.instanceId; instanceId = raised.instanceId;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`); await must(`${HOST_PATH} apply /tmp/foundation.lock`);
// The mesh's artifact store, standing where the `registry` module would. Read back rather than // 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, // 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 // 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. // is not running, and those want completely different responses.
const { out, ok } = await on( 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.equal(ok, false, "a build of nothing reported success");
assert.match(out, /could not build/, out); assert.match(out, /could not build/, out);
+6 -6
View File
@@ -35,7 +35,7 @@ const skip = !capability.usable
: !host || !existsSync(host) : !host || !existsSync(host)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle" ? "MESH_LAB_BUNDLE is not set to a foundation bundle"
: false; : false;
const SCENARIO = "first-node"; const SCENARIO = "first-node";
@@ -62,7 +62,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
const mesh = (command: string, timeoutMs?: number) => 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 () => { before(async () => {
if (skip) return; if (skip) return;
@@ -70,9 +70,9 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`), onProgress: (m) => console.log(`raise: ${m}`),
}); });
instanceId = raised.instanceId; 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`); 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 // **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, // 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" }, { id: "note", type: "file", path: "/var/lib/canary/it-arrived", content: "yes\n", mode: "0644" },
], ],
}))} > /tmp/canary.json`); }))} > /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("module add /canary.json");
await mesh(`assign ${MACHINE} canary`); await mesh(`assign ${MACHINE} canary`);
await mesh(`push ${MACHINE}`, 300_000); 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`); }))} > /tmp/canary-app.json`);
for (const name of ["canary-store", "canary-app"]) { 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(`module add /${name}.json`);
await mesh(`assign ${MACHINE} ${name}`); await mesh(`assign ${MACHINE} ${name}`);
} }
+1 -1
View File
@@ -25,7 +25,7 @@ const proxy = process.env["MESH_LAB_ROUTE_PROXY"] ?? "";
const skip = !capability.usable const skip = !capability.usable
? `lab not usable: ${capability.why}` ? `lab not usable: ${capability.why}`
: !proxy : !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; : false;
const SCENARIO = "a-public-name"; const SCENARIO = "a-public-name";
+14 -14
View File
@@ -5,8 +5,8 @@
* credential is delivered over it. This proves the other half of the bus (novox/hq ADR 0046): the * 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 `#` * 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 * 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" * from the foundation bundle — not a broker a test stood up — because "the mesh hosts the broker"
* (tier-1 substrate) is the thing being relied on. * (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 * 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 `<node>.<module>.events`, and a * payload, a consumer gets a durable per-consumer queue `<node>.<module>.events`, and a
@@ -14,10 +14,10 @@
* on the raised broker is the check that the runtime provisioned the contract, not just that a * on the raised broker is the check that the runtime provisioned the contract, not just that a
* message happened to arrive. * 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_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; * MESH_LAB_RUNTIME=.../mesh-runtime-audit.tar (docker save of the runtime+audit-logger image;
* built by scripts/build-runtime-image.sh) * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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 type { HeldImage } from "../../src/pinning.ts";
import { incus } from "../../src/incus/client.ts"; import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts"; import { machineName } from "../../src/lifecycle/names.ts";
@@ -48,7 +48,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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) : !runtime || !existsSync(runtime)
? "MESH_LAB_RUNTIME is not set to a runtime image tar (scripts/build-runtime-image.sh)" ? "MESH_LAB_RUNTIME is not set to a runtime image tar (scripts/build-runtime-image.sh)"
: false; : false;
@@ -58,7 +58,7 @@ const MACHINE = "anchor";
/** Where the audit-logger container writes its trail, on the machine — mounted from a host dir. */ /** 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_DIR = "/var/lib/mesh-audit";
const TRAIL = `${TRAIL_DIR}/audit.log`; 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/"; const BROKER = "amqp://guest:guest@127.0.0.1:5672/";
let instanceId = ""; 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. * 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 * `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). * than swallowing the marker (the fault mesh.test.ts documents).
*/ */
async function on(command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> { async function on(command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> {
@@ -92,14 +92,14 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
/** /**
* 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 * 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 * 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). * what makes it applicable (the same rewrite mesh.test.ts does).
*/ */
function bundleFor(images: HeldImage[]): string { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
/** Read the trail back as parsed JSON lines. */ /** Read the trail back as parsed JSON lines. */
@@ -120,14 +120,14 @@ before(async () => {
}); });
instanceId = raised.instanceId; 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, // 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. // which is not knowable until it has been handed over.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const running = await must(`docker ps --format '{{.Names}}'`); 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 // 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 // route out (novox/hq the lab is a closed address space), so the image arrives as a tar the way
+514
View File
@@ -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=<forge>/mesh-controller.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_KEEP=1 to leave it standing afterwards
*/
import { test, before, after } from "node:test";
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<string> {
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<string> {
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<string> {
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<string> {
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<string> {
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<string, Step>();
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<string>): Promise<void> {
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));
});
}
+8 -5
View File
@@ -12,7 +12,7 @@
* MESH_LAB_INCUS='sudo -n incus' * MESH_LAB_INCUS='sudo -n incus'
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host * MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap * 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_CATALOG=.../mesh-catalog/modules
* MESH_LAB_KEEP=1 to leave it standing afterwards * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy } from "../../src/lifecycle/operate.ts"; import { destroy } from "../../src/lifecycle/operate.ts";
import { bootstrapBinaryPath, hostBinaryPath } from "../../src/lifecycle/place.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"; import { genesis, type GenesisResult } from "./genesis.ts";
const SCENARIO = "genesis-single"; const SCENARIO = "genesis-single";
@@ -49,7 +49,7 @@ const skip =
"bootstrap IMAGE=mesh-builder:development`)" : "bootstrap IMAGE=mesh-builder:development`)" :
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" : !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" : !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" : !catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
false; false;
@@ -75,12 +75,15 @@ before(async () => {
installer: installer as string, installer: installer as string,
catalogDir, catalogDir,
// The template, not a bundle. The store and broker become the references mesh-catalog pins, // 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 // naming a registry that does not exist — the installer overwrites it, and leaving it proves
// that it does. // that it does.
bundleTemplate: substrateBundle(bundle, []), bundleTemplate: foundationBundle(bundle, []),
source, source,
sourceRef, 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), log: (m) => console.log(m),
}); });
} catch (err) { } catch (err) {
+92 -29
View File
@@ -12,10 +12,12 @@
* description of genesis, and both the single-node bed and the four-node bed call it. * description of genesis, and both the single-node bed and the four-node bed call it.
*/ */
import { existsSync, writeFileSync } from "node:fs"; import { existsSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join, resolve } from "node:path"; import { join, resolve } from "node:path";
import { exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts"; 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. */ /** What genesis did, or where it stopped. */
export interface GenesisResult { export interface GenesisResult {
@@ -35,7 +37,7 @@ export interface GenesisOptions {
/** A checkout of mesh-catalog's `modules/` on this workstation. */ /** A checkout of mesh-catalog's `modules/` on this workstation. */
catalogDir: string; 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. * replacing the control plane's image with the id of the image it carries.
*/ */
bundleTemplate: string; bundleTemplate: string;
@@ -53,6 +55,31 @@ export interface GenesisOptions {
*/ */
source: string; source: string;
sourceRef: 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; log?: (m: string) => void;
} }
@@ -64,7 +91,7 @@ export function stepIn(said: string): string {
export async function genesis(o: GenesisOptions): Promise<GenesisResult> { export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
const node = o.node; const node = o.node;
const registry = o.registry ?? "127.0.0.1:5000"; 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 catalogueOnMachine = o.catalogueOnMachine ?? "/opt/mesh-catalog";
const log = o.log ?? (() => {}); const log = o.log ?? (() => {});
@@ -98,30 +125,66 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
// The catalogue. `mesh-bootstrap --catalog` reads manifests from a CHECKOUT on the machine, // 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 // 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. // 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) { for (const module of modules) {
const from = resolve(o.catalogDir, module, "module.json"); const at = `${catalogueOnMachine}/modules/${module}/module.json`;
if (!existsSync(from)) return stop("preparing the catalogue", `the catalogue has no ${module}/module.json at ${from}`); if (!(await on(`test -f ${at}`)).ok) {
await push(o.instanceId, node, from, `${catalogueOnMachine}/modules/${module}/module.json`); 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); 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 = [ const command = [
BOOTSTRAP_PATH, BOOTSTRAP_PATH,
`--source ${o.source}`, `--source ${o.source}`,
`--source-ref ${o.sourceRef}`, `--source-ref ${o.sourceRef}`,
`--bundle /tmp/substrate-template.lock`, `--bundle /tmp/foundation-template.lock`,
`--catalog ${catalogueOnMachine}`, `--catalog ${catalogueOnMachine}`,
`--node ${node}`, `--node ${node}`,
`--registry ${registry}`, `--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}`, `--host ${HOST_PATH}`,
// The lab has no unit to supervise the host with, and the installer refuses to invent one — a // A service when the packaging was installed above (survives a reboot); otherwise the
// unit file is a packaging decision. A host started this way does not survive a reboot. // background process, which does not — the installer refuses to invent a unit either way.
`--host-in-background`, ...(o.hostService ? [] as string[] : [`--host-in-background`]),
].join(" "); ].join(" ");
// Run up to three times. Not to paper over a failure — every attempt's failing step is printed — // 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<GenesisResult> {
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
// 1. The control plane answers, asked of the PERMANENT container by name. // 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"}`); 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. // 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/`); 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<GenesisResult> {
// 3. THE PIVOT COMPLETED — the running control plane is pinned by a digest THIS MESH'S REGISTRY // 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). // 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)"}`); report.push(` pinned to ${pinnedTo || "(nothing)"}`);
if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) { if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) {
return stop("after the last step", 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 ` + `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").`); `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", 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}.`); `digest assigned by ${registry}.`);
} }
@@ -186,34 +249,34 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
// installer that quietly built something else would satisfy a weaker check and raise a mesh // installer that quietly built something else would satisfy a weaker check and raise a mesh
// nobody asked for. // nobody asked for.
const wanted = o.sourceRef.slice(0, 8); 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", 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. ` + `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)"}`); `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. // 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"}`); report.push(` registry holds ${tags.out.trim() || "nothing"}`);
if (!tags.out.includes("genesis")) { if (!tags.out.includes("genesis")) {
return stop("after the last step", 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()}`); `image nothing can pull: ${tags.out.trim()}`);
} }
// 4. The temporary control plane is GONE. The name is the audit. // 4. The temporary control plane is GONE. The name is the audit.
const temp = await on(`docker inspect --format '{{.State.Status}}' temp-mesh-control`); const temp = await on(`docker inspect --format '{{.State.Status}}' temp-mesh-controller`);
report.push(` temp-mesh-control ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`); report.push(` temp-mesh-controller ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`);
if (temp.ok) { if (temp.ok) {
return stop("after the last step", 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.`); `broker queues; neither is wrong and the pivot is not finished.`);
} }
// 5. And the mesh has heard from its one node. // 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(" | ")}`); 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} `)); const line = nodes.out.split("\n").map((l) => l.trim()).find((l) => l.startsWith(`${node} `));
if (!line || !/^\S+\s+here\b/.test(line)) { if (!line || !/^\S+\s+here\b/.test(line)) {
+6 -6
View File
@@ -17,19 +17,19 @@ import { duplicateAddresses, describeConflicts, type Held } from "../../src/life
import type { Scenario } from "../../src/declaration/types.ts"; import type { Scenario } from "../../src/declaration/types.ts";
import { mustBeHandedOver, pinnedInto, referenceFor, repositoryOf, type HeldImage } from "../../src/pinning.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. * 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. * 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. * 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 * 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` * 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 * and `lavinmq` — so the foundation'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. * 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 * **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 * 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"; "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 * 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 * 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. * 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"); let text = readFileSync(path, "utf8");
text = text.replaceAll(/[A-Za-z0-9_.:-]+\/postgres@sha256:[0-9a-f]{64}/g, UPSTREAM_STORE); text = text.replaceAll(/[A-Za-z0-9_.:-]+\/postgres@sha256:[0-9a-f]{64}/g, UPSTREAM_STORE);
text = text.replaceAll( text = text.replaceAll(
+24 -24
View File
@@ -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. * 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 * 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 * `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 * 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. * runs the provider AND the amqp-ping consumer, co-located, and owns laptop's 5672 uncontested.
* *
* The proof is layered: * 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 * 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); * gone from `docker ps -a` (a step, not a service);
* - the lavinmq service and BOTH runtimes (bootstrap done, provisioner running) are up and stable; * - 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; * node that runs it;
* - the provisioner (in mesh-lavinmq on laptop) created the consumer's vhost AND user, both named * - 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; * 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 * 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}`. * `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): * 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 lavinmq /tmp/lavinmq.tar
* scripts/build-module-runtime.sh amqp-ping /tmp/amqp-ping.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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -53,11 +53,11 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "lavinmq-bed"; 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"; const NODE = "laptop";
/** The login the mesh derives for the consumer: mesh_<node>_<slug> (slug "ping"; ADR 0049). */ /** The login the mesh derives for the consumer: mesh_<node>_<slug> (slug "ping"; ADR 0049). */
const CONSUMER_LOGIN = "mesh_laptop_ping"; 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. */ /** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -118,7 +118,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise<void> {
} | undefined; } | undefined;
let said = ""; let said = "";
try { 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; said = asked.out;
if (asked.ok) state = JSON.parse(said); if (asked.ok) state = JSON.parse(said);
} catch (err) { } catch (err) {
@@ -157,11 +157,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`); const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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) { for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) {
@@ -178,7 +178,7 @@ after(async () => {
await destroyAll(`${SCENARIO}-`); await destroyAll(`${SCENARIO}-`);
}, { timeout: 600_000 }); }, { 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, skip, timeout: 1_500_000,
}, async () => { }, 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 -------------------------------------- // --- add, issue a scoped broker account, assign to laptop --------------------------------------
async function addIssueAssign(name: string, manifest: string): Promise<void> { async function addIssueAssign(name: string, manifest: string): Promise<void> {
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 add /${name}.json`);
const issued = await mesh(`module issue ${name} --node ${NODE}`); const issued = await mesh(`module issue ${name} --node ${NODE}`);
assert.match(issued, /broker account/, issued); 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); 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 onAnchor = await must("anchor", `docker ps --format '{{.Names}}'`);
const onLaptop = await must(NODE, `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|$)/, assert.doesNotMatch(onAnchor, /(^|\n)lavinmq(\n|$)/,
"the lavinmq provider landed on the substrate node — the 5672 collision this bed exists to avoid"); "the lavinmq provider landed on the foundation node — the 5672 collision this bed exists to avoid");
assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the substrate store leaked onto the second node"); 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"); 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. // node that runs it and the module.
// ================================================================================================ // ================================================================================================
const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`); const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`);
+11 -11
View File
@@ -15,7 +15,7 @@
* asserts the templated URL and that a request to it reaches the running server (ollama answers * 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). * /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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -38,7 +38,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "local-model-bed"; const SCENARIO = "local-model-bed";
@@ -67,11 +67,11 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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 }> { 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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -117,7 +117,7 @@ async function settled(withinMs = 600_000): Promise<void> {
} }
async function addAssign(name: string, manifest: string): Promise<void> { async function addAssign(name: string, manifest: string): Promise<void> {
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 add /${name}.json`);
await mesh(`module issue ${name} --node ${MACHINE}`); await mesh(`module issue ${name} --node ${MACHINE}`);
await mesh(`assign ${MACHINE} ${name}`); await mesh(`assign ${MACHINE} ${name}`);
@@ -132,11 +132,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); await mesh(`node add ${MACHINE}`);
+12 -12
View File
@@ -11,7 +11,7 @@
* Mint on one side and create on the other agreeing, with no shared key and nothing placed by the * 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. * 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 * scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which
* scenarios/redis-node.yml stocks. * scenarios/redis-node.yml stocks.
*/ */
@@ -23,7 +23,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts"; import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -35,7 +35,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "redis-node"; const SCENARIO = "redis-node";
@@ -64,7 +64,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -86,7 +86,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -118,11 +118,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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" }], 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 add /redis.json");
await mesh(`module issue redis --node ${MACHINE}`); await mesh(`module issue redis --node ${MACHINE}`);
await mesh(`assign ${MACHINE} redis`); 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("module add /cacheuser.json");
await mesh(`assign ${MACHINE} cacheuser`); await mesh(`assign ${MACHINE} cacheuser`);
+47 -47
View File
@@ -6,10 +6,10 @@
* project keeps saying cannot be checked any other way (novox/hq ADR 0001: every fault of * 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). * 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_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. * 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 * 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 { pinnedInto, stillUnpinned, type HeldImage } from "../../src/pinning.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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 { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts"; import { machineName } from "../../src/lifecycle/names.ts";
import { ready, returnTo, keep, rememberStock, warmStock } from "../../src/warm.ts"; import { ready, returnTo, keep, rememberStock, warmStock } from "../../src/warm.ts";
@@ -36,7 +36,7 @@ const capability = await labIsUsable();
const binary = hostBinaryPath(); const binary = hostBinaryPath();
const bundle = process.env["MESH_LAB_BUNDLE"] ?? ""; const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
const builder = process.env["MESH_LAB_BUILDER"] ?? ""; 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 moduleExamples = process.env["MESH_LAB_MODULES"] ?? "";
const skip = !capability.usable const skip = !capability.usable
@@ -44,7 +44,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "two-nodes"; 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. */ /** The control plane, which runs in a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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<void> {
let said = ""; let said = "";
try { try {
const asked = await on("anchor", const asked = await on("anchor",
`docker exec mesh-control /mesh-control status --json`); `docker exec mesh-controller /mesh-controller status --json`);
said = asked.out; said = asked.out;
// Parsed inside the try on purpose: a truncated answer from a struggling machine is the // 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 // 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<void> {
* The bundle, as a machine should receive it. * 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 * 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. * exists in no registry, becomes the ID this machine was handed.
*/ */
function bundleFor(images: HeldImage[]): string { 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. */ /** 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 // 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. // knowable until it has been handed over.
held = raised.images; held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`); 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 // 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. // 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 () => { test("a bare machine becomes a mesh", { skip, timeout: 600_000 }, async () => {
const running = await must("anchor", `docker ps --format '{{.Names}}'`); 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`); 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 — // 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` + `"content":"PGHOST=$\{bound:postgres-database:at\}\\nPGPORT=$\{bound:postgres-database:port\}\\n` +
`PGUSER=$\{bound:postgres-database:as\}\\nPGPASSWORD=$\{secret:postgres-database\}\\n"}]}' ` + `PGUSER=$\{bound:postgres-database:as\}\\nPGPASSWORD=$\{secret:postgres-database\}\\n"}]}' ` +
`> /tmp/app.json`); `> /tmp/app.json`);
await must("anchor", `docker cp /tmp/pg.json mesh-control:/pg.json`); await must("anchor", `docker cp /tmp/pg.json mesh-controller:/pg.json`);
await must("anchor", `docker cp /tmp/app.json mesh-control:/app.json`); await must("anchor", `docker cp /tmp/app.json mesh-controller:/app.json`);
await mesh("module add /pg.json"); await mesh("module add /pg.json");
await mesh("module add /app.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. // is the situation `status` exists to distinguish from a machine that refused everything.
await must("anchor", `printf %s '{"module":"impossible","version":"1","resources":[` + await must("anchor", `printf %s '{"module":"impossible","version":"1","resources":[` +
`{"id":"nothing","type":"package","package":"a-package-that-does-not-exist"}]}' > /tmp/imp.json`); `{"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("module add /imp.json");
await mesh("assign laptop impossible"); await mesh("assign laptop impossible");
await mesh("push laptop"); 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":[` + 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`); `{"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("module add /away.json");
await mesh("assign laptop while-away"); await mesh("assign laptop while-away");
await mesh("push laptop"); 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 () => { 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, // 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 // 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". // 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) { 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":[` + await must("anchor", `printf %s '{"module":"${name}","version":"1","resources":[` +
`{"id":"note","type":"file","path":"${path}","content":"${name}"}]}' > /tmp/${name}.json`); `{"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(`module add /${name}.json`);
await mesh(`assign anchor ${name}`); 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, assert.ok((await on("anchor", `test -f /etc/mesh-kept`)).ok,
"unassigning one module took another one's file with it"); "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 // 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. // away, which is exactly what happened before origins existed.
const running = await must("anchor", `docker ps --format '{{.Names}}'`); 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), assert.match(running, new RegExp(container),
`${container} was removed by a declaration that never declared it`); `${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":[` + await must("anchor", `printf %s '{"module":"fixable","version":"1","resources":[` +
`{"id":"pkg","type":"package","package":"a-package-that-does-not-exist-yet"},` + `{"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`); `{"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("module add /fix.json");
await mesh("assign laptop fixable"); await mesh("assign laptop fixable");
await mesh("push laptop"); 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. // 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":[` + await must("anchor", `printf %s '{"module":"fixable","version":"1","resources":[` +
`{"id":"note","type":"file","path":"/etc/mesh-fixable","content":"here"}]}' > /tmp/fix.json`); `{"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("module add /fix.json");
// And nobody names the machine. // 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 // 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 // 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. // 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("module add /registry.json");
await mesh("assign anchor registry"); await mesh("assign anchor registry");
await mesh("push anchor"); 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"},` + `"certificate":{"into":"/etc/mesh/serving.crt","authority":"/etc/mesh/authority.crt"},` +
`"resources":[{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"}]}' ` + `"resources":[{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"}]}' ` +
`> /tmp/served.json`); `> /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("module add /served.json");
await mesh("assign anchor served"); await mesh("assign anchor served");
await mesh("push anchor"); 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",` + `{"id":"filter","type":"service","unit":"mesh-filter.service","state":"running",` +
`"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`); `"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`);
for (const f of ["talker", "firewall"]) { 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(`module add /${f}.json`);
} }
await mesh("assign laptop talker"); 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"}]}' ` + `"resources":[{"id":"dir","type":"directory","path":"/etc/realapp","mode":"0755"}]}' ` +
`> /tmp/realapp.json`); `> /tmp/realapp.json`);
for (const f of ["realstore", "realapp"]) { 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(`module add /${f}.json`);
} }
await mesh("assign anchor realstore"); 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",` + `{"id":"app","type":"container","name":"storefront",` +
`"image":"${ARTIFACT_STORE}","ports":["8088:5000"]}]}' > /tmp/storefront.json`); `"image":"${ARTIFACT_STORE}","ports":["8088:5000"]}]}' > /tmp/storefront.json`);
for (const f of ["frontdoor", "storefront"]) { 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(`module add /${f}.json`);
} }
await mesh("assign anchor frontdoor"); 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, assert.ok(withdrawn,
`the route outlived the module that asked for it:\n${after}\n\n` + `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` + `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; let gone = false;
for (let i = 0; i < 15 && !gone; i++) { 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"},` + `"secrets":{"model-access":"/etc/assistant/key"},` +
`"resources":[{"id":"dir","type":"directory","path":"/etc/assistant","mode":"0755"}]}' ` + `"resources":[{"id":"dir","type":"directory","path":"/etc/assistant","mode":"0755"}]}' ` +
`> /tmp/assistant.json`); `> /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"); await mesh("module add /assistant.json");
// The licences first. With none recorded at all the honest answer is that nothing provides // 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 // 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 // command. ADR 0024 warns this will be felt — which is correct, and correct is not the same as
// usable. // 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, assert.ok(!refused.ok,
`a consumer was given model access without anybody saying which:\n${refused.out}`); `a consumer was given model access without anybody saying which:\n${refused.out}`);
for (const want of ["personal", "the-organisation", "licence use"]) { 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"); 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. // 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.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); 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. // 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 secret = "sk-test-" + "0123456789abcdef".repeat(2);
const accepted = await must("anchor", 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.match(accepted, /sealed to 1 holder/, accepted);
assert.doesNotMatch(accepted, new RegExp(secret), assert.doesNotMatch(accepted, new RegExp(secret),
"the key was echoed back, so the one copy that matters is on a terminal"); "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",` + await must("anchor", `printf %s '{"module":"board","version":"1",` +
`"listens":[{"port":8090,"from":"mesh","why":"the board"}],` + `"listens":[{"port":8090,"from":"mesh","why":"the board"}],` +
`"resources":[]}' > /tmp/board.json`); `"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"); await mesh("module add /board.json");
// Served from the control plane's own container, reading the mesh on every request. // 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)); await new Promise((r) => setTimeout(r, 3000));
const read = async (path: string) => 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",` + await must("anchor", `printf %s '{"module":"impossible","version":"1",` +
`"resources":[{"id":"nowhere","type":"service","unit":"nothing-like-this.service",` + `"resources":[{"id":"nowhere","type":"service","unit":"nothing-like-this.service",` +
`"state":"running"}]}' > /tmp/impossible.json`); `"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("module add /impossible.json");
await mesh("assign laptop impossible"); await mesh("assign laptop impossible");
await mesh("push laptop"); 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"},` + `ExecStart=/usr/bin/nft -f ${rules}\\n[Install]\\nWantedBy=multi-user.target\\n"},` +
`{"id":"filter","type":"service","unit":"hub-filter.service","state":"running",` + `{"id":"filter","type":"service","unit":"hub-filter.service","state":"running",` +
`"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/hubfilter.json`); `"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("module add /hubfilter.json");
await mesh("assign anchor hubfilter"); await mesh("assign anchor hubfilter");
await mesh("push anchor"); 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",` + await must("anchor", `printf %s '{"module":"stillworks","version":"1",` +
`"resources":[{"id":"marker","type":"file","path":"/etc/mesh-still-works",` + `"resources":[{"id":"marker","type":"file","path":"/etc/mesh-still-works",` +
`"content":"yes","mode":"0644"}]}' > /tmp/stillworks.json`); `"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("module add /stillworks.json");
await mesh("assign laptop stillworks"); await mesh("assign laptop stillworks");
await mesh("push laptop"); await mesh("push laptop");
@@ -1450,7 +1450,7 @@ test("a container reaches another machine by the name the mesh gave it", {
`"capabilities":["container-runtime"],` + `"capabilities":["container-runtime"],` +
`"resources":[{"id":"idle","type":"container","name":"resolves",` + `"resources":[{"id":"idle","type":"container","name":"resolves",` +
`"image":"${ARTIFACT_STORE}"}]}' > /tmp/resolves.json`); `"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("module add /resolves.json");
await mesh("assign laptop resolves"); await mesh("assign laptop resolves");
await mesh("push laptop"); 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", { 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, timeout: 900_000,
}, async () => { }, async () => {
// postgres.novox.internal, plex.ace.internal — the first label is the service and the rest is // 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"]) { for (const name of ["dnsmasq", "resolved-split-dns"]) {
const manifest = readFileSync(`${moduleExamples}/${name}.json`, "utf8"); const manifest = readFileSync(`${moduleExamples}/${name}.json`, "utf8");
await must("anchor", `cat > /tmp/${name}.json <<'MANIFEST'\n${manifest}\nMANIFEST`); 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`); 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`); }))} > /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"); await mesh("module add /umami.json");
// **Accepted, not generated.** The value is what the database already answers to; the mesh // **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. // seals it and cannot read it again. Given whole, as the environment lines the containers read.
await must("anchor", await must("anchor",
`printf %s ${quote(`POSTGRES_PASSWORD=${password}`)} | ` + `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", await must("anchor",
`printf %s ${quote( `printf %s ${quote(
`DATABASE_URL=postgresql://umami:${password}@umami-db:5432/umami`)} | ` + `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("assign anchor umami");
await mesh("push anchor", 300_000); 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); planned.push(name);
await must("anchor", `printf %s ${quote(pinned)} > /${name}.json`); 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`); 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), [], assert.deepEqual(stillUnpinned(pinned), [],
`${name} still names an image nothing serves, so it could not start`); `${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", `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(`module add /run-${name}.json`);
await mesh(`assign anchor ${name}`); 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), [], assert.deepEqual(stillUnpinned(pinned), [],
"redis still names an image nothing serves, so it could not start"); "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", `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"); 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 // 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"},` + `"secrets":{"redis-cache":"/var/lib/cachetest/cache.secret"},` +
`"resources":[{"id":"state","type":"directory","path":"/var/lib/cachetest","mode":"0700"}]}' ` + `"resources":[{"id":"state","type":"directory","path":"/var/lib/cachetest","mode":"0700"}]}' ` +
`> /cachetest.json`); `> /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("module add /cachetest.json");
await mesh("assign anchor redis"); await mesh("assign anchor redis");
+12 -12
View File
@@ -9,7 +9,7 @@
* the consumer reaching its bucket with the access key and secret the mesh delivered it. Nothing is * the consumer reaching its bucket with the access key and secret the mesh delivered it. Nothing is
* placed by the test. * 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 * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "minio-node"; const SCENARIO = "minio-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -93,7 +93,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -125,11 +125,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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" }], 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 add /minio.json");
await mesh(`module issue minio --node ${MACHINE}`); await mesh(`module issue minio --node ${MACHINE}`);
await mesh(`assign ${MACHINE} minio`); 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("module add /bucketuser.json");
await mesh(`assign ${MACHINE} bucketuser`); await mesh(`assign ${MACHINE} bucketuser`);
+1 -1
View File
@@ -33,7 +33,7 @@ const skip = !capability.usable
? `lab not usable: ${capability.why}` ? `lab not usable: ${capability.why}`
: !provisioner : !provisioner
? "set MESH_LAB_OBJECTSTORE_PROVISIONER to a built 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; : false;
const SCENARIO = "an-object-store"; const SCENARIO = "an-object-store";
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -12,7 +12,7 @@
* ordinary sealed-delivery path — with NO manager, NO refresh, NO access/refresh split, NO usage. The * 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). * 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: * Build the consumer runtime image into the local daemon first:
* scripts/build-module-runtime.sh openai-consumer /tmp/openai-consumer.tar * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "openai-bed"; const SCENARIO = "openai-bed";
@@ -69,11 +69,11 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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 }> { 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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -135,11 +135,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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"); const consumerImage = pinned("mesh-runtime-openai-consumer");
// --- the licence, a record with vendor openai (static-key) ------------------------------------- // --- 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 // 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. // 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"}'`); 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 // 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 // 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). // 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-control:/openai-key`); 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`); await mesh(`licence key personal --file /openai-key`);
// --- deploy the consumer ----------------------------------------------------------------------- // --- 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 add /openai-consumer.json`);
await mesh(`module issue openai-consumer --node ${MACHINE}`); await mesh(`module issue openai-consumer --node ${MACHINE}`);
await mesh(`assign ${MACHINE} openai-consumer`); await mesh(`assign ${MACHINE} openai-consumer`);
@@ -9,7 +9,7 @@
* the mesh minted. The proof is the consumer connecting to its database with the credential the mesh * 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. * 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), * scripts/build-module-runtime.sh postgres builds mesh-runtime-postgres:development (with psql),
* which scenarios/postgres-node.yml stocks. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -33,7 +33,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "postgres-node"; const SCENARIO = "postgres-node";
@@ -62,7 +62,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -84,7 +84,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -116,11 +116,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
{ id: "net", type: "network", name: "postgres" }, { 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 // 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. // committed manifest publishes it for cross-node consumers, which is a different node.
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres", 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" }], 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 add /postgres.json");
await mesh(`module issue postgres --node ${MACHINE}`); await mesh(`module issue postgres --node ${MACHINE}`);
await mesh(`assign ${MACHINE} postgres`); 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("module add /dbuser.json");
await mesh(`assign ${MACHINE} dbuser`); await mesh(`assign ${MACHINE} dbuser`);
@@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts"; import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -32,7 +32,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "redis-node"; const SCENARIO = "redis-node";
@@ -61,7 +61,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -83,7 +83,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -115,11 +115,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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 add /redis.json");
await mesh(`module issue redis --node ${MACHINE}`); await mesh(`module issue redis --node ${MACHINE}`);
await mesh(`assign ${MACHINE} redis`); await mesh(`assign ${MACHINE} redis`);
@@ -9,10 +9,10 @@
* password gets PONG — where a provisioner that invented its own password would answer WRONGPASS. * 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 * 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. * `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 * scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which
* scenarios/redis-node.yml stocks. * scenarios/redis-node.yml stocks.
*/ */
@@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts"; import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "redis-node"; const SCENARIO = "redis-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -87,7 +87,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -119,11 +119,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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 add /redis.json");
await mesh(`module issue redis --node ${MACHINE}`); await mesh(`module issue redis --node ${MACHINE}`);
await mesh(`assign ${MACHINE} redis`); await mesh(`assign ${MACHINE} redis`);
+1 -1
View File
@@ -25,7 +25,7 @@ const provisioner = process.env["MESH_LAB_PROVISIONER"] ?? "";
const skip = !capability.usable const skip = !capability.usable
? `lab not usable: ${capability.why}` ? `lab not usable: ${capability.why}`
: !provisioner : !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; : false;
const SCENARIO = "a-provider"; const SCENARIO = "a-provider";
+14 -14
View File
@@ -25,7 +25,7 @@
* publicly-trusted certificate and answering an HTTP-01 challenge at the name — is proven separately * 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). * 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; * 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. * 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -49,7 +49,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "route-forwarding"; const SCENARIO = "route-forwarding";
@@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */ /** The control plane, a container on the node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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); 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -105,7 +105,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -143,12 +143,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// Raise the substrate — store, broker, control — from the bundle. // Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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 // 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"); await mesh("module add /route-proxy.json");
// No `module issue`: route-proxy has no broker account and no own-secret to mint. `assign` resolves // 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. // its plan and the provider is matchable by a consumer's route from that alone.
await mesh(`assign ${MACHINE} route-proxy`); 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("module add /hello-web.json");
await mesh(`assign ${MACHINE} hello-web`); await mesh(`assign ${MACHINE} hello-web`);
@@ -12,7 +12,7 @@
* the container was replaced (a new container id) and the config on disk carries the new value. * 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. * 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 * scripts/build-module-runtime.sh grafana builds mesh-runtime-grafana:development, which
* scenarios/grafana-node.yml stocks. * scenarios/grafana-node.yml stocks.
*/ */
@@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts"; import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "grafana-node"; const SCENARIO = "grafana-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
} }
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
@@ -87,7 +87,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs; const until = Date.now() + withinMs;
let last = ""; let last = "";
while (Date.now() < until) { 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) { if (asked.ok) {
try { try {
const state = JSON.parse(asked.out) as { const state = JSON.parse(asked.out) as {
@@ -112,7 +112,7 @@ async function settled(withinMs = 480_000): Promise<void> {
async function setToken(token: string): Promise<void> { async function setToken(token: string): Promise<void> {
const settings = JSON.stringify({ url: "http://127.0.0.1:3000", token }); 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}`); await mesh(`settings set grafana /s.json --node ${MACHINE}`);
} }
@@ -129,11 +129,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000); await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`); const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
await mesh(`node add ${MACHINE}`); 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 mesh("module add /grafana.json");
await setToken("token-alpha"); await setToken("token-alpha");
+13 -13
View File
@@ -1,9 +1,9 @@
/** /**
* The whole `ace` server's converted service set, installed together on ONE node behind the * 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. * 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 * `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 * 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 * 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 * 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). * 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"; 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 { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -44,7 +44,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "whole-mesh-ace"; 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<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function loadManifest(name: string): { manifest: string; broker: boolean } { function loadManifest(name: string): { manifest: string; broker: boolean } {
@@ -214,7 +214,7 @@ interface NodeState {
} }
async function nodeState(node: string): Promise<NodeState> { async function nodeState(node: string): Promise<NodeState> {
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 }; if (!asked.ok) return { reached: false, applied: false, current: false, waiting: false, raw: asked.out };
let state: { let state: {
wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[]; wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[];
@@ -248,11 +248,11 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000); await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 900_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`); const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
} }
for (const machine of ["anchor", NODE]) { 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; if (DROPPED.some((d) => d.name === name)) continue;
try { try {
const { manifest, broker } = loadManifest(name); 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`); await mesh(`module add /${name}.json`);
if (broker) { if (broker) {
await mesh(`module issue ${name} --node ${NODE}`); await mesh(`module issue ${name} --node ${NODE}`);
+55 -55
View File
@@ -5,12 +5,12 @@
* *
* hosting (public) home (private, behind a NAT access point) * 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 * 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) * control) + the whole novox g14 10.99.1.30 workstation (light: portainer only)
* set + overlay hub + ingress * set + overlay hub + ingress
* *
* There is NO separate anchor: novox IS the anchor. The substrate runs on novox, and novox also * 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 substrate host and a service node at once. * 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). * 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. * 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, * 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 * 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 * 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 * 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 * 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. * 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 * FOUNDATION-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 * separate-anchor beds never hit): the foundation 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 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 substrate's ports * 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 * (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. * 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 * (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(). * 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 * 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 { import {
bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH, bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH,
} from "../../src/lifecycle/place.ts"; } 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"; import { referenceFor, type HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -84,7 +84,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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) : !installer || !existsSync(installer)
? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap (mesh-host `make " + ? "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, " + "bootstrap IMAGE=mesh-builder:development`). The anchor is raised BY the installer now, " +
@@ -96,7 +96,7 @@ const skip = !capability.usable
: false; : false;
const SCENARIO = "whole-mesh-full"; 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"; const CONTROL = "novox";
/** Every node in the mesh. novox is on hosting; the rest are behind the home gateway. */ /** Every node in the mesh. novox is on hosting; the rest are behind the home gateway. */
const NODES = ["novox", "ace", "shanks", "g14"]; 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 * `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. * 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. * 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([ const GAPS_NOVOX = new Set([
"umami", "mailu", "firewall", "fail2ban", "only-office", "de-spiegel", "amqp-email-forwarder", "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 // 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. // gated is the half that is decided and cheap — see the ADR 0066 section at the end.
"step-ca", "step-ca",
]); ]);
@@ -274,13 +274,13 @@ const PLAN: { node: string; mods: Mod[]; core: Set<string>; gaps: Set<string> }[
/** /**
* Host-port remaps (per module; host ports are per-VM so novox's and ace's never clash across nodes). * 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 * The two FOUNDATION 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 * 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). * runs. The rest break the novox web/app host-port collisions (route-proxy fronts 80/443).
*/ */
const REMAP: Record<string, Record<string, string>> = { const REMAP: Record<string, Record<string, string>> = {
// Moved, NOT bound to loopback. These two carried `127.0.0.1:` and it broke a consumer on a node // 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 `<node>.internal`, that // with no foundation at all: a module is told to reach its provider at `<node>.internal`, that
// name resolves to the node's overlay address, and a provider listening only on loopback refuses // 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 // 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. // 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). */ /** The control plane, a container on novox (the anchor). */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** 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 { function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images); return foundationBundle(bundle, images);
} }
function loadManifest(name: string): { manifest: string; broker: boolean } { function loadManifest(name: string): { manifest: string; broker: boolean } {
@@ -405,7 +405,7 @@ interface NodeState {
} }
async function nodeState(node: string): Promise<NodeState> { async function nodeState(node: string): Promise<NodeState> {
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 }; if (!asked.ok) return { reached: false, applied: false, current: false, waiting: false, raw: asked.out };
let state: { let state: {
wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[]; wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[];
@@ -467,9 +467,9 @@ async function deliverCaRoot(): Promise<boolean> {
// Safe here and nowhere else: these three exist for the seconds between being written and // 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. // 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", "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.crt mesh-controller:/ca-root-cert",
"docker cp /tmp/ca/root.key mesh-control:/ca-root-key", "docker cp /tmp/ca/root.key mesh-controller:/ca-root-key",
"docker cp /tmp/ca/key-password mesh-control:/ca-root-key-password", "docker cp /tmp/ca/key-password mesh-controller:/ca-root-key-password",
].join("\n"), 180_000); ].join("\n"), 180_000);
if (!made.ok) { if (!made.ok) {
console.log(`CA ROOT NOT MADE on ${CONTROL}:\n${made.out.split("\n").slice(-8).join("\n")}`); 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. * 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 * **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 * 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 * 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 * 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<GenesisResult> {
} }
report.push(` catalogue ${CATALOGUE_MODULES.join(", ")} at ${CATALOGUE_ON_MACHINE}`); 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 // 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. // 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 // 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 // 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. // 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 // 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 // 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 // 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. // 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 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); writeFileSync(local, template);
await push(instanceId, CONTROL, local, "/tmp/substrate-template.lock"); await push(instanceId, CONTROL, local, "/tmp/foundation-template.lock");
const command = [ const command = [
BOOTSTRAP_PATH, BOOTSTRAP_PATH,
`--source ${source}`, `--source ${source}`,
`--source-ref ${sourceRef}`, `--source-ref ${sourceRef}`,
`--bundle /tmp/substrate-template.lock`, `--bundle /tmp/foundation-template.lock`,
`--catalog ${CATALOGUE_ON_MACHINE}`, `--catalog ${CATALOGUE_ON_MACHINE}`,
`--node ${CONTROL}`, `--node ${CONTROL}`,
`--registry ${MESH_REGISTRY}`, `--registry ${MESH_REGISTRY}`,
@@ -628,10 +628,10 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
// 1. The control plane answers. Asked of the PERMANENT container by name — `status` opens all // 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. // three stores, so a reply proves the connections it was given are the foundation's own.
const answered = await on(CONTROL, `docker exec mesh-control /mesh-control status`, 60_000); 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"}`); 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 // 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. // container that is up is not a registry that serves.
@@ -643,22 +643,22 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// 3. THE PIVOT COMPLETED. ADR 0067 states this check in as many words: after installing, the // 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 // 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. // 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(); .out.trim();
report.push(` pinned to ${pinnedTo || "(nothing)"}`); report.push(` pinned to ${pinnedTo || "(nothing)"}`);
if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) { if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) {
return stop("after step 10", 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 ` + `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 ` + `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").`); `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)) { .test(pinnedTo)) {
return stop("after step 10", 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}.`); `digest assigned by ${MESH_REGISTRY}.`);
} }
@@ -669,36 +669,36 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// outside to one that can — same container, same digest, same registry. The difference is // 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. // whether a build happened, and the only place that is visible is the installer saying so.
const wanted = sourceRef.slice(0, 8); 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", 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.`); `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 // 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. // reference is a claim; a tag list is the registry agreeing.
const tags = await on(CONTROL, 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"}`); report.push(` registry holds ${tags.out.trim() || "nothing"}`);
if (!tags.out.includes("genesis")) { if (!tags.out.includes("genesis")) {
return stop("after step 10", 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()}`); `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 // 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. // 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-control`); const temp = await on(CONTROL, `docker inspect --format '{{.State.Status}}' temp-mesh-controller`);
report.push(` temp-mesh-control ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`); report.push(` temp-mesh-controller ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`);
if (temp.ok) { if (temp.ok) {
return stop("after step 10", 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.`); `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. // 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(" | ")}`); 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} `)); const line = nodes.out.split("\n").map((l) => l.trim()).find((l) => l.startsWith(`${CONTROL} `));
if (!line || !/^\S+\s+here\b/.test(line)) { if (!line || !/^\S+\s+here\b/.test(line)) {
@@ -715,7 +715,7 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// ================================================================================================== // ==================================================================================================
/** /**
* 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 * **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 — * 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); const known = added.get(name);
if (known !== undefined) return known; if (known !== undefined) return known;
const { manifest, broker } = loadManifest(name); 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`); await mesh(`module add /${name}.json`);
added.set(name, broker); added.set(name, broker);
return 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`. // Operator-provided app credentials (own-secrets), delivered as fake values through `secret accept`.
const credentialDelivered = new Map<string, boolean>(); const credentialDelivered = new Map<string, boolean>();
for (const name of new Set(CREDENTIALS.map((c) => c.name))) { 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) { for (const c of CREDENTIALS) {
if (!assigned[c.node]!.has(c.module)) { 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; if (!assigned[s.node]!.has(s.module)) continue;
try { try {
const inControl = `/secret-${s.module}-${s.name}`; 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}`); await mesh(`secret accept ${s.node} ${s.module} ${s.name} --from ${inControl}`);
} catch (err) { } catch (err) {
console.log(`OPERATOR SECRET FAILED ${s.node}/${s.module}/${s.name}: ${(err as Error).message.split("\n").slice(0, 2).join(" | ")}`); 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. // 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 // 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. // gated on it would be reporting somebody else's in-flight work as this bed's failure.
// ================================================================================================ // ================================================================================================
const adr: string[] = ["================ ADR 0066: LABELLED ROUTES ================"]; const adr: string[] = ["================ ADR 0066: LABELLED ROUTES ================"];
+21 -21
View File
@@ -1,11 +1,11 @@
/** /**
* The whole `novox` server's converted service set, installed together on ONE node behind the * 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). * 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 * `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 * each consumer's binding `at` resolves to novox's private address and reaches the providers
* co-located with it. * co-located with it.
* *
@@ -28,7 +28,7 @@
* host ports here (container ports unchanged); the provider ports the consumers actually connect to * 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. * (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 * 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 * is built by scripts/build-route-proxy-image.sh; scenarios/whole-mesh-novox.yml stocks them all
* alongside every server image. * alongside every server image.
@@ -42,7 +42,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts"; import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.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"; import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable(); const capability = await labIsUsable();
@@ -55,13 +55,13 @@ const skip = !capability.usable
: !binary || !existsSync(binary) : !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle) : !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; : false;
const SCENARIO = "whole-mesh-novox"; const SCENARIO = "whole-mesh-novox";
const NODE = "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"] const catalogDir = process.env["MESH_LAB_CATALOG"]
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "") ?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
?? resolve(process.cwd(), "..", "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 * fail2ban declares `capabilities: ["intrusion-prevention"]`, but mesh-host advertises no such
* capability: profile/detectors.go defines container-runtime, package-manager, service-manager, * capability: profile/detectors.go defines container-runtime, package-manager, service-manager,
* firewall, overlay, seat, privileged and graphical-session — nothing for intrusion-prevention. So * 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 * 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 * ("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.) * 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 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 * 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 substrate + all five providers + the four consumers that reach * 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. * their providers and stay up + the four standalone apps.
*/ */
const CORE = new Set([ 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 * 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). * 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 * 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 * 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 * 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. */ /** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<string> { async function mesh(command: string, timeoutMs?: number): Promise<string> {
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. */ /** The pinned reference this scenario's registry serves for a repository. */
@@ -194,9 +194,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held); 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 { 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. */ /** 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<NodeState> { async function nodeState(node: string): Promise<NodeState> {
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 }; if (!asked.ok) return { reached: false, applied: false, current: false, waiting: false, raw: asked.out };
let state: { let state: {
wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[]; wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[];
@@ -270,12 +270,12 @@ before(async () => {
instanceId = raised.instanceId; instanceId = raised.instanceId;
held = raised.images; held = raised.images;
// anchor raises the substrate from its bundle, digests rewritten to the scenario registry's. // anchor raises the foundation 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", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000); await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 900_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`); const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) { for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`); 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. // 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) { for (const { name } of MODULES) {
try { try {
const { manifest, broker } = loadManifest(name); 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`); await mesh(`module add /${name}.json`);
if (broker) { if (broker) {
await mesh(`module issue ${name} --node ${NODE}`); await mesh(`module issue ${name} --node ${NODE}`);
+3 -3
View File
@@ -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. // 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", () => { test("a run against code that has since changed is not current", () => {
const said = judge( 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, 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"); assert.equal(said.current, false, "a run against changed code was reported as current");
const text = said.lines.join("\n"); 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); assert.match(text, /code that has since changed/, text);
}); });
+10 -10
View File
@@ -16,8 +16,8 @@ import {
*/ */
const HELD: HeldImage[] = [ const HELD: HeldImage[] = [
{ {
requested: "mesh-control:development", requested: "mesh-controller:development",
repository: "mesh-control", repository: "mesh-controller",
reference: "sha256:" + "a".repeat(64), 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", () => { test("only what is built here and published nowhere counts as ours", () => {
for (const ours of [ 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), "mesh-provision-postgres@sha256:" + "0".repeat(64),
]) { ]) {
assert.ok(isMeshBuilt(ours), `${ours} is one of ours and was not recognised`); 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", "registry.example:5000/novox/www:latest",
// A registry host in front of one of our names does NOT make it ours: it says somebody // 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. // 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`); 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", () => { 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. // What the committed foundation 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)}"`; const before = `"image": "192.0.2.250:5000/mesh-controller@sha256:${"e".repeat(64)}"`;
assert.equal(pinnedInto(before, HELD), `"image": "sha256:${"a".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. // 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", () => { 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, 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", () => { 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", () => { 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); 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", () => { test("an image the machine cannot fetch by itself is handed over", () => {
for (const handed of [ for (const handed of [
"mesh-control:development", "mesh-controller:development",
"mesh-runtime-plex:development", "mesh-runtime-plex:development",
`registry.example/novox/www@sha256:${"a".repeat(64)}`, `registry.example/novox/www@sha256:${"a".repeat(64)}`,
"registry.example:5000/novox/photos-server:latest", "registry.example:5000/novox/photos-server:latest",
+15 -15
View File
@@ -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 // 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 // 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. // 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])); 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"]); assert.deepEqual(byMachine.get("peer"), ["host"]);
}); });
@@ -74,11 +74,11 @@ scenario: s
segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } } segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines: machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true } 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] } place: { all: [host, runtime] }
`); `);
assert.deepEqual(planHeldImages(s), [ 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: anchor:
at: { segment: hosting, address: [192.0.2.10] } at: { segment: hosting, address: [192.0.2.10] }
egress: true egress: true
images: [mesh-control:development] images: [mesh-controller:development]
laptop: laptop:
at: { segment: hosting, address: [192.0.2.20] } at: { segment: hosting, address: [192.0.2.20] }
egress: true egress: true
images: [mesh-runtime-redis:development] 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] } place: { all: [host, runtime] }
`); `);
assert.deepEqual(planHeldImages(s), [ assert.deepEqual(planHeldImages(s), [
{ machine: "anchor", images: ["mesh-control:development"] }, { machine: "anchor", images: ["mesh-controller:development"] },
{ machine: "laptop", images: ["mesh-runtime-redis:development"] }, { machine: "laptop", images: ["mesh-runtime-redis:development"] },
]); ]);
}); });
@@ -113,7 +113,7 @@ segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines: machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true } anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true }
bare: { at: { segment: hosting, address: [192.0.2.20] }, egress: true, images: [] } bare: { at: { segment: hosting, address: [192.0.2.20] }, egress: true, images: [] }
images: [mesh-control:development] images: [mesh-controller:development]
place: { all: [host, runtime] } place: { all: [host, runtime] }
`); `);
assert.deepEqual(planHeldImages(s).map((p) => p.machine), ["anchor"]); 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", () => { 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 // 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. // is unsupported when half of it now works.
try { 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"); assert.fail("expected a refusal");
} catch (err) { } catch (err) {
assert.ok(err instanceof UnsupportedError); assert.ok(err instanceof UnsupportedError);
const missing = err.missing.join("\n"); 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.match(missing, /control/, "the control plane was not named");
assert.doesNotMatch(missing, /place: host/, "the host is placeable and was refused anyway"); 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", () => { test("the placeables are host, runtime and an image", () => {
assert.ok(isPlaceable("host")); assert.ok(isPlaceable("host"));
assert.ok(isPlaceable("runtime")); 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")); assert.ok(!isPlaceable("control-plane"));
}); });
test("an unplaceable artifact is named, not refused as a whole", () => { 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. // wholesale would send somebody looking for the wrong problem.
const scenario = { const scenario = {
name: "s", name: "s",
segments: {}, segments: {},
machines: { a: {} as never }, 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<typeof assertSupported>[0]; } as unknown as Parameters<typeof assertSupported>[0];
assert.throws( assert.throws(
@@ -192,7 +192,7 @@ test("an unplaceable artifact is named, not refused as a whole", () => {
// Checked as `place: <artifact> —`, which is how an artifact is REPORTED as // Checked as `place: <artifact> —`, which is how an artifact is REPORTED as
// unplaceable. Searching for the bare word matched the message's own list of what CAN // 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. // 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: image:alpine"), "a placeable one is not");
assert.ok(!err.message.includes("place: runtime —"), "nor is runtime"); assert.ok(!err.message.includes("place: runtime —"), "nor is runtime");
assert.ok(!err.message.includes("place: host —"), "nor is host"); assert.ok(!err.message.includes("place: host —"), "nor is host");
+2 -2
View File
@@ -79,9 +79,9 @@ test("the whole-mesh bed hands its anchor no control-plane image", () => {
...Object.values(scenario.machines).flatMap((m) => m.images ?? []), ...Object.values(scenario.machines).flatMap((m) => m.images ?? []),
]; ];
assert.deepEqual( 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", "really carries it",
); );
}); });
+4 -4
View File
@@ -60,20 +60,20 @@ place: { all: [host] }`);
}); });
test("a tier above 0 is still refused, and named", () => { 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 // must be told which half is missing — not that `place:` is unsupported, when half of it
// now works. // now works.
const scenario = parseScenario(`scenario: x const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } } segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] } } } machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
place: { all: [host, substrate] }`); place: { all: [host, foundation] }`);
try { try {
assertSupported(scenario); assertSupported(scenario);
assert.fail("should have refused"); assert.fail("should have refused");
} catch (err) { } catch (err) {
assert.ok(err instanceof UnsupportedError); assert.ok(err instanceof UnsupportedError);
assert.equal(err.missing.length, 1, `expected only the substrate: ${err.missing.join(", ")}`); assert.equal(err.missing.length, 1, `expected only the foundation: ${err.missing.join(", ")}`);
assert.match(err.missing[0] ?? "", /substrate/); assert.match(err.missing[0] ?? "", /foundation/);
assert.match(err instanceof Error ? err.message : "", /silently lacks them/); assert.match(err instanceof Error ? err.message : "", /silently lacks them/);
} }
}); });
+3 -3
View File
@@ -280,7 +280,7 @@ test("one of ours in images: is accepted", () => {
assert.doesNotThrow(() => parseScenario(`scenario: x assert.doesNotThrow(() => parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } } segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } } 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] }`)); 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 refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } } segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } } 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] }`, place: { all: [runtime] }`,
/is pinned by digest/); /is pinned by digest/);
}); });
@@ -303,7 +303,7 @@ machines:
at: { segment: net, address: [192.0.2.1] } at: { segment: net, address: [192.0.2.1] }
egress: true egress: true
images: [mesh-runtime-redis:development] images: [mesh-runtime-redis:development]
images: [mesh-control:development] images: [mesh-controller:development]
place: { all: [runtime] }`, place: { all: [runtime] }`,
/is not in this scenario's images/); /is not in this scenario's images/);
}); });
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import { judge, type Warm } from "../src/warm.ts"; import { judge, type Warm } from "../src/warm.ts";
const at = "2026-08-31T20:00:00Z"; 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> = {}): Warm => const warm = (over: Partial<Warm> = {}): Warm =>
({ scenario: "two-nodes", instanceId: "mlab-two-nodes-1", images: [], against: built, at, ...over }); ({ scenario: "two-nodes", instanceId: "mlab-two-nodes-1", images: [], against: built, at, ...over });