One-node bed: SDK-by-version, rename, and the store/broker upgrade proofs #27

Merged
jschoubben merged 26 commits from feat/a-bed-that-hands-over-nothing into main 2026-09-16 21:25:34 +00:00
89 changed files with 785 additions and 785 deletions
Showing only changes of commit 5d6e8fbe7a - Show all commits
+7 -7
View File
@@ -17,7 +17,7 @@ test.
| | **Bootstrap** | **Full** |
|---|---|---|
| Contains | virtual machines, the node host, a pinned substrate bundle | a complete mesh: forge, control plane, delivery, modules |
| Contains | virtual machines, the node host, a pinned foundation bundle | a complete mesh: forge, control plane, delivery, modules |
| Verdict from | what the host reports about the state it reconciled | a pipeline result ending in verify |
| Exercises | tiers 0 and 1 | tier 2 and above, and modules |
| Exists to | **develop the mesh** | **test what runs on it** |
@@ -140,23 +140,23 @@ is written down here rather than reconstructed a third time.
```sh
export MESH_LAB_HOST_BINARY=<mesh-host>/mesh-host
export MESH_LAB_BUNDLE=<mesh-host>/examples/substrate-first-node.lock
export MESH_LAB_MODULES=<mesh-control>/examples/modules
export MESH_LAB_BUILDER=<mesh-control>/build/mesh-builder # build/, which is git-ignored
export MESH_LAB_BUNDLE=<mesh-host>/examples/foundation-first-node.lock
export MESH_LAB_MODULES=<mesh-controller>/examples/modules
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`
# of the control-plane image — so it is built AFTER that image, in the same run, or it carries a
# stale one sealed inside a binary where nothing would ever notice. The whole-mesh bed raises its
# anchor by RUNNING this, rather than by applying a substrate bundle itself (novox/hq ADR 0067).
# anchor by RUNNING this, rather than by applying a foundation bundle itself (novox/hq ADR 0067).
export MESH_LAB_BOOTSTRAP_BINARY=<mesh-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
# manifests from a copy of it ON THE MACHINE, because at genesis there is no forge, no build
# machine and — until the registry is up — nothing serving anything.
export MESH_LAB_CATALOG=<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_OBJECTSTORE_PROVISIONER=<somewhere>/objectstore-provisioner
export MESH_LAB_ROUTE_PROXY=<somewhere>/route-proxy
+17 -17
View File
@@ -1,18 +1,18 @@
{
"scenario": "one-node-mesh",
"established": 21,
"established": 22,
"of": 22,
"steps": [
{
"code": "R1",
"title": "a bare machine becomes a mesh of one, raised by the installer",
"status": "pass",
"seconds": 122,
"seconds": 316,
"why": ""
},
{
"code": "R2",
"title": "the substrate is up — a store and a broker of the mesh's own",
"title": "the foundation is up — a store and a broker of the mesh's own",
"status": "pass",
"seconds": 1,
"why": ""
@@ -56,63 +56,63 @@
"code": "P1",
"title": "the mesh builds the shared base from source",
"status": "pass",
"seconds": 85,
"seconds": 3,
"why": ""
},
{
"code": "P2",
"title": "the mesh builds and runs a store of its own",
"status": "pass",
"seconds": 38,
"seconds": 7,
"why": ""
},
{
"code": "P3",
"title": "the mesh builds and runs its own catalogue",
"status": "pass",
"seconds": 31,
"seconds": 4,
"why": ""
},
{
"code": "P4",
"title": "the mesh rebuilds its own control plane from source",
"status": "pass",
"seconds": 32,
"seconds": 30,
"why": ""
},
{
"code": "N1",
"title": "the mesh puts itself on a private network, and its machine has a name",
"status": "pass",
"seconds": 6,
"seconds": 4,
"why": ""
},
{
"code": "N2",
"title": "the machine has a packet filter, loaded from what modules declared",
"status": "pass",
"seconds": 7,
"seconds": 2,
"why": ""
},
{
"code": "U1",
"title": "the mesh builds a module standing on that base",
"status": "pass",
"seconds": 13,
"seconds": 14,
"why": ""
},
{
"code": "U2",
"title": "the mesh runs a broker for that module to talk to",
"status": "pass",
"seconds": 24,
"seconds": 26,
"why": ""
},
{
"code": "U3",
"title": "the anchor runs the module the mesh built",
"status": "pass",
"seconds": 6,
"seconds": 7,
"why": ""
},
{
@@ -140,22 +140,22 @@
"code": "V4",
"title": "every resource the mesh declared is true on the machine",
"status": "pass",
"seconds": 11,
"seconds": 13,
"why": ""
},
{
"code": "E1",
"title": "a change to a module's source reaches the machine on its own",
"status": "pass",
"seconds": 13,
"seconds": 12,
"why": ""
},
{
"code": "E2",
"title": "the mesh comes back after the machine reboots",
"status": "fail",
"seconds": 31,
"why": "every container came back and no host agent did, so the machine is running the right things and can no longer be told anything:\namqp-ping\tUp 3 seconds\nmesh-lavinmq\tUp 3 seconds\nlavinmq\tUp 4 seconds (health: starting)\nmesh-control\tUp 3 seconds\nmesh-catalog\tUp 1 second\nmesh-postgres\tUp 4 seconds\npostgres\tUp 3 seconds\nmesh-builder\tUp 2 seconds\nmesh-registry\tUp 3 seconds\nmesh-broker\tUp 3 seconds (health: starting)\nmesh-store\tUp 3 seconds\n"
"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
it declared and no longer declares.
The reference implementation lives in `mesh-control/examples/postgres-provisioner`, because that
The reference implementation lives in `mesh-controller/examples/postgres-provisioner`, because that
is where the contract is defined and where the language is already set up to read it. The lab's
job is the other half: raising a real PostgreSQL and proving that what the mesh delivered becomes
a login that works, a rotation that takes effect, and a revocation that bites.
+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 manager module seals the refresh token to the node's PUBLIC key -> the host unseals it and
# mounts the cleartext at the manager's bound path -> the manager calls the stub token endpoint ->
# submits back only { access token, re-sealed box } -> mesh-control seals the access token per
# submits back only { access token, re-sealed box } -> mesh-controller seals the access token per
# consumer holder -> the consumer runtime writes ~/.claude/.credentials.json, access-token-only.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# Build BOTH runtime images into the local daemon first (the scenario stocks and serves them by
# digest, which is where the host pulls them from):
# scripts/build-module-runtime.sh anthropic-manager /tmp/anthropic-manager.tar
@@ -35,7 +35,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# The two model-access runtimes, built by scripts/build-module-runtime.sh into the local daemon and
# loaded onto the machine, which holds them by their own image IDs.
- mesh-runtime-anthropic-manager:development
+2 -2
View File
@@ -1,6 +1,6 @@
# One machine that becomes a mesh and then assigns itself the audit logger.
#
# The substrate is first-node's — a store, a broker, the control plane — and one module image on
# The foundation is first-node's — a store, a broker, the control plane — and one module image on
# top: the tool runtime carrying the audit-logger (mesh-catalog). The node enrols itself and the
# mesh assigns it the audit logger, so its events account is one the mesh delivered, not the
# broker's own (novox/hq ADR 0048).
@@ -20,7 +20,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# The tool runtime with the audit-logger, built by scripts/build-runtime-image.sh into the local
# daemon and loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-audit:development
+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
# converted this session (novox/hq ADR 0039/0048/0052): mongodb, unifi and marrytts, with postgres
# carried along as a known-good control. This is catalogue-small's sibling: same first-node substrate,
# carried along as a known-good control. This is catalogue-small's sibling: same first-node foundation,
# same one-push co-residence, a different (and heavier) set of modules.
#
# The four exercise the three converted shapes:
@@ -14,7 +14,7 @@
# None of the four share a directory, so the shared-workspace refusal (novox/hq 04-ISSUES/012) does
# not bite; that class stays for a later bed.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-module-runtime.sh {mongodb,unifi,postgres} build the runtime images into the local
# daemon (mongodb carries mongosh; unifi and postgres carry their CLIs). The service images
# (mongo, unifi-controller, marytts, postgres) must be in the local daemon to be stocked.
@@ -31,13 +31,13 @@ machines:
egress: true
inbound: allow
# Sized up past catalogue-small's 6GiB: this wave carries two heavy JVM/embedded-DB service
# containers (the UniFi controller and MaryTTS) on top of the substrate, mongodb, postgres and
# containers (the UniFi controller and MaryTTS) on top of the foundation, mongodb, postgres and
# three node runtimes — a dozen containers, two of them memory-hungry at startup.
memory: 8GiB
cpus: 4
images:
- mesh-control:development
- mesh-controller:development
# The runtimes built by scripts/build-module-runtime.sh and stocked here. marrytts needs none.
- mesh-runtime-mongodb:development
- mesh-runtime-unifi:development
+3 -3
View File
@@ -16,7 +16,7 @@
# after enrol and before the push. Each module additionally OWNS its own config directory
# (/services/{sonarr,radarr}/config), which the mesh does create.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-module-runtime.sh {sonarr,radarr} build the two runtime images into the local daemon
# (they speak HTTP and need no CLI added). The service images lscr.io/linuxserver/{sonarr,radarr}
# must be in the local daemon to be stocked.
@@ -32,14 +32,14 @@ machines:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
inbound: allow
# Two *arr apps (server + runtime each) on top of the first-node substrate — seven containers.
# Two *arr apps (server + runtime each) on top of the first-node foundation — seven containers.
# The Servarr images are lighter than catalogue-apps' JVM pair, so catalogue-small's 6GiB is
# ample headroom.
memory: 6GiB
cpus: 4
images:
- mesh-control:development
- mesh-controller:development
# The two runtimes built by scripts/build-module-runtime.sh and stocked here.
- mesh-runtime-sonarr:development
- mesh-runtime-radarr:development
+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
# crash-loops the broker.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-module-runtime.sh mosquitto builds mesh-runtime-mosquitto:development (carrying
# mosquitto_ctrl and the compiled bootstrap entrypoint) into the local daemon, which this scenario
# pulls from the internet over its uplink. eclipse-mosquitto:2 must be in the local
@@ -32,7 +32,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
- mesh-runtime-mosquitto:development
place:
+4 -4
View File
@@ -3,14 +3,14 @@
#
# The per-backend beds each assign one module: postgres (a database provider), minio (an object-store
# provider), redis (a cache provider + tools), plex (a tools module). This raises the same first-node
# substrate and then assigns all four to the one anchor in a single push, so the proof is that they
# foundation and then assigns all four to the one anchor in a single push, so the proof is that they
# resolve and come up TOGETHER on one node — nothing new about any single module, everything new about
# their co-residence.
#
# The four are chosen because none of them share a directory, so the shared-workspace refusal
# (novox/hq 04-ISSUES/012 — the media stack) does not bite here; that class stays for a later bed.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-module-runtime.sh {postgres,redis,minio,plex} build the four runtime images into the
# local daemon (postgres carries psql, minio carries mc), which this scenario stocks and serves by
# the internet over its uplink. Only the mesh's own images come from the local daemon.
@@ -26,14 +26,14 @@ machines:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
inbound: allow
# Sized up: this anchor runs the substrate (store, broker, control) plus four modules — three of
# Sized up: this anchor runs the foundation (store, broker, control) plus four modules — three of
# which are a server container and a runtime container each — so a dozen containers at once. The
# single-module beds run at 3GiB; co-residence needs the headroom.
memory: 6GiB
cpus: 4
images:
- mesh-control:development
- mesh-controller:development
# The four per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. plex
# needs no server image in the lab — its runtime serves tools with no Plex to reach.
- mesh-runtime-postgres:development
+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
# delivery. It exists to develop the steps of raising a mesh on a machine that has nothing but a
@@ -24,7 +24,7 @@ machines:
# is what pinning asks for (novox/hq ADR 0006). The store and the broker are ordinary third-party
# images, and the machine pulls them from the internet like anything else.
images:
- mesh-control:development
- mesh-controller:development
place:
all: [host, runtime]
+4 -4
View File
@@ -11,7 +11,7 @@
# 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 substrate, the registry and the builder's own dependencies are PULLED from the internet,
# - 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;
@@ -26,7 +26,7 @@
#
# hosting (public, routable) home (private, behind the access point)
# anchor 192.0.2.20 ── anchor home-server 10.99.1.10 home server
# substrate, registry, workstation 10.99.1.20 workstation
# 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
@@ -35,7 +35,7 @@
# 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/substrate-first-node.lock
# 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
@@ -74,7 +74,7 @@ machines:
cpus: 6
disk: 60GiB
# Three machines that JOIN. Host binary and a token, nothing else — no bootstrap, no substrate,
# 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:
+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
# out to the internet, the host placed, and nothing else — the installer carries the control
# plane's image and the substrate's own images are pulled over the uplink, exactly as they are on
# plane's image and the foundation's own images are pulled over the uplink, exactly as they are on
# a bare machine.
#
# The address matters: the substrate template names the broker at 192.0.2.10, and a token carries
# The address matters: the foundation template names the broker at 192.0.2.10, and a token carries
# that address verbatim as the endpoint an enrolling node dials. With one machine, that machine
# must BE it, or the mesh would hand out an endpoint nothing answers on.
scenario: genesis-single
@@ -23,7 +23,7 @@ machines:
# bare machine. A scenario that needs no images can omit this; genesis cannot.
egress: true
inbound: allow
# Enough for the substrate (store, broker), the registry, and two control planes during the
# Enough for the foundation (store, broker), the registry, and two control planes during the
# pivot. Smaller than the four-node bed's anchor, which also carries a whole service set.
memory: 8GiB
cpus: 4
@@ -33,8 +33,8 @@ machines:
#
# The runtime is not a mesh tier — it is a prerequisite of the machine, and the installer's first
# step refuses to go on without one. Placing it here is the lab preparing a machine, not the lab
# describing an installation. Everything above tier 0 — the substrate, the registry, the control
# describing an installation. Everything above tier 0 — the foundation, the registry, the control
# plane — is the installer's, and the lab places none of it. That is the whole point of this bed:
# if the lab placed the substrate, it would be describing installing all over again.
# if the lab placed the foundation, it would be describing installing all over again.
place:
all: [host, runtime]
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
- mesh-runtime-grafana:development
place:
+1 -1
View File
@@ -28,7 +28,7 @@ machines:
inbound: allow
images:
- mesh-control:development
- mesh-controller:development
place:
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
# user-facing capability: a module that needs a message queue gets its OWN broker — a scoped vhost and
# user on a lavinmq PROVIDER — not an account on the control broker. That makes lavinmq the sharp
# two-node case, for the same reason two-node-db is: the substrate's broker publishes 5672 on the node
# two-node case, for the same reason two-node-db is: the foundation's broker publishes 5672 on the node
# it runs on, and a lavinmq provider must publish 5672 too for its consumers to reach it — so the two
# cannot share a machine. The moment a real amqp provider must own a node's 5672, it collides with the
# control broker already there, and the chain is blocked single-node.
#
# This is the split that unblocks it. `anchor` runs the substrate (store, broker, control) and NOTHING
# This is the split that unblocks it. `anchor` runs the foundation (store, broker, control) and NOTHING
# else. `laptop` runs the whole chain: the lavinmq PROVIDER and the amqp-ping CONSUMER that requires
# it. Provider and consumer are co-located on laptop, so the grant never crosses a node boundary; only
# enrolment crosses to anchor, over the underlay both machines share. And because the substrate broker
# enrolment crosses to anchor, over the underlay both machines share. And because the foundation broker
# is on the OTHER node, the provider owns laptop's 5672 uncontested.
#
# It needs a host binary and the substrate bundle:
# It needs a host binary and the foundation bundle:
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
#
# HELPER — stock the two runtimes into the local daemon before the run (some may already be there):
# scripts/build-module-runtime.sh lavinmq /tmp/lavinmq.tar
# scripts/build-module-runtime.sh amqp-ping /tmp/amqp-ping.tar
# The service image cloudamqp/lavinmq:latest must be in the local daemon too — it is already stocked as
# the substrate's own broker image; each node pulls what it runs from the internet, over its own
# the foundation's own broker image; each node pulls what it runs from the internet, over its own
# uplink.
scenario: lavinmq-bed
@@ -46,7 +46,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# The lavinmq provider's runtime (reused for its run-once bootstrap and its provisioner) and the
# amqp-ping consumer's runtime, both built by scripts/build-module-runtime.sh into the local daemon
# and loaded onto the machines, which hold them by their own image IDs.
+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
# request to it reaches the running model server.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# Both modules are pure declaration (a server container + a templated file) — no module runtime image
# is built; the bed only stocks the substrate and the ollama server image.
# is built; the bed only stocks the foundation and the ollama server image.
scenario: local-model-bed
segments:
@@ -31,7 +31,7 @@ machines:
disk: 40GiB
images:
- mesh-control:development
- mesh-controller:development
place:
all: [host, runtime]
+1 -1
View File
@@ -21,7 +21,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# minio's runtime, built by scripts/build-module-runtime.sh minio (it carries mc), loaded onto
# the machine.
- mesh-runtime-minio:development
+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
# model-usage CONSUMER ride one node; the substrate (store, broker, control) owns the other. As in
# two-node-db, the app-postgres provider and the mesh's own substrate store both want host port 5432,
# so they cannot share a machine — the substrate lives on `anchor` and NOTHING else, and `laptop`
# model-usage CONSUMER ride one node; the foundation (store, broker, control) owns the other. As in
# two-node-db, the app-postgres provider and the mesh's own foundation store both want host port 5432,
# so they cannot share a machine — the foundation lives on `anchor` and NOTHING else, and `laptop`
# runs postgres plus model-usage. Provider and consumer are co-located on laptop, so only enrolment
# crosses to anchor, over the underlay both machines already share.
#
@@ -9,7 +9,7 @@
# provisioned postgres store, at BOTH grains (licence and session, differing only in `consumer`),
# LATEST-per-key, and IN THE CLEAR — an ordinary select returns the numeric value and its raw payload.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# Build BOTH runtime images into the local daemon first (the scenario stocks and serves them by
# digest, which is where the host pulls them from):
# scripts/build-module-runtime.sh postgres /tmp/postgres.tar
@@ -22,7 +22,7 @@ segments:
cidr: [192.0.2.0/24]
machines:
# The substrate ONLY: store, broker, control — three containers.
# The foundation ONLY: store, broker, control — three containers.
anchor:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
@@ -30,8 +30,8 @@ machines:
memory: 4GiB
cpus: 4
# The postgres PROVIDER (server + broker-bound runtime) and the model-usage CONSUMER (its run-once
# migrate and its long-lived event runtime). The 5432-vs-substrate conflict is gone because the
# substrate store is on the OTHER node. The runtime images plus postgres:17-alpine are pulled from
# migrate and its long-lived event runtime). The 5432-vs-foundation conflict is gone because the
# foundation store is on the OTHER node. The runtime images plus postgres:17-alpine are pulled from
# the internet over the uplink; forty gigabytes holds them with room to spare.
laptop:
at: { segment: hosting, address: [192.0.2.20] }
@@ -42,7 +42,7 @@ machines:
disk: 40GiB
images:
- mesh-control:development
- mesh-controller:development
# The per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. Each carries
# its module's code — postgres its provisioner, model-usage its consumer, tools and run-once migrate.
- mesh-runtime-postgres:development
+5 -5
View File
@@ -2,7 +2,7 @@
#
# 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 substrate, a registry,
# 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.
@@ -10,11 +10,11 @@
# 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 substrate and a registry it pulled from the internet,
# - 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 substrate's is the control plane's own plumbing, not a provider,
# - 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.
#
@@ -26,7 +26,7 @@
# 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/substrate-first-node.lock
# 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
@@ -37,7 +37,7 @@ segments:
cidr: [192.0.2.0/24]
machines:
# The address matters: the substrate template names the broker at a fixed address, and a token
# 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.
#
+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
# the operator set — through the sealed delivery path, unchanged.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# Build the consumer runtime image into the local daemon first (raise() stocks it from there):
# scripts/build-module-runtime.sh openai-consumer /tmp/openai-consumer.tar
scenario: openai-bed
@@ -30,7 +30,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# The static-key consumer runtime, built by scripts/build-module-runtime.sh into the local daemon and
# loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-openai-consumer:development
+2 -2
View File
@@ -1,7 +1,7 @@
# One machine that becomes a mesh and then assigns itself plex's tool runtime.
#
# The audit-node bed proved an assigned *consumer* (novox/hq ADR 0048). This proves an assigned
# module that *serves tools* (ADR 0052): the same first-node substrate, plus plex's tool runtime on
# module that *serves tools* (ADR 0052): the same first-node foundation, plus plex's tool runtime on
# top. The node enrols itself, the mesh issues plex a broker account scoped to serve.plex.* and
# assigns it, the host runs the runtime container, and a caller invokes plex.plex_reachable over the
# mesh — proof the module runs its own code as its own process under its own scoped account.
@@ -21,7 +21,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# Plex's tool runtime, built by scripts/build-module-runtime.sh plex into the local daemon and
# loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-plex:development
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# postgres's runtime, built by scripts/build-module-runtime.sh postgres (it carries psql), loaded
# onto the machine.
- mesh-runtime-postgres:development
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# Redis's tool+provisioner runtime, built by scripts/build-module-runtime.sh redis into the local
# daemon and loaded onto the machine, which holds it by its own image ID.
- mesh-runtime-redis:development
+4 -4
View File
@@ -14,9 +14,9 @@
# publicly-trusted certificate and answering an HTTP-01 challenge at the name — is proven separately
# by certificates.test.ts against a real ACME server (Pebble), driving the same proxy binary.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-route-proxy-image.sh builds mesh-route-proxy:development into the local daemon
# (from mesh-control/examples/route-proxy, via mesh-catalog/modules/route-proxy/Dockerfile).
# (from mesh-controller/examples/route-proxy, via mesh-catalog/modules/route-proxy/Dockerfile).
# alpine:latest must be in the local daemon — hello-web's backend is a bare alpine that serves a
# fixed page over a busybox nc loop. Both images are stocked and served by digest.
scenario: route-forwarding
@@ -35,8 +35,8 @@ machines:
cpus: 2
images:
- mesh-control:development
# The route-proxy's image, built from the canonical Go proxy in mesh-control by
- mesh-controller:development
# The route-proxy's image, built from the canonical Go proxy in mesh-controller by
# scripts/build-route-proxy-image.sh, and hello-web's backend, a bare alpine nc loop.
- mesh-route-proxy:development
+2 -2
View File
@@ -14,7 +14,7 @@
# - the container fires when the cron is due (top of the next minute);
# - it fires AGAIN on the following minute — recurrence, not a one-shot.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_MODULES=.../mesh-control/examples/modules
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_MODULES=.../mesh-controller/examples/modules
# alpine:latest must be in the local daemon; the machine pulls it from the internet over its
# uplink, and the scheduled container declares it exactly as the catalogue writes it.
# There is no runtime image: schedtest carries no code of its own — the scheduled container is a
@@ -35,7 +35,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
place:
# Only the host — schedtest has no mesh-runtime to place. The tick image is pulled from the
+1 -1
View File
@@ -20,7 +20,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
- mesh-runtime-sonarr:development
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)`
# and binds its serve queues regardless; a tool would only fail if it were actually invoked.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-module-runtime.sh confluence builds mesh-runtime-confluence:development into the
# local daemon, which the machine pulls from the internet over its uplink. confluence
# needs no service image — it is tools-only.
@@ -29,7 +29,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# confluence's runtime, built by scripts/build-module-runtime.sh confluence into the local daemon
# and loaded onto the machine, which holds it by its own image ID. There is no
# service image: confluence is tools-only and outbound-only.
+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
# queues regardless; a tool would only fail if it were actually invoked without real creds.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# scripts/build-module-runtime.sh gitlab builds mesh-runtime-gitlab:development into the local
# daemon, which the machine pulls from the internet over its uplink. gitlab needs no
# service image — it is tools-only.
@@ -30,7 +30,7 @@ machines:
cpus: 2
images:
- mesh-control:development
- mesh-controller:development
# gitlab's runtime, built by scripts/build-module-runtime.sh gitlab into the local daemon and
# loaded onto the machine, which holds it by its own image ID. There is no
# service image: gitlab is tools-only and outbound-only.
+6 -6
View File
@@ -1,13 +1,13 @@
# The DB-consumer chain a single node cannot host, proved across two machines.
#
# The app-postgres provider and the mesh's own substrate store both want host port 5432, so they
# cannot share a machine — the collision that blocked this chain single-node. Here the substrate
# The app-postgres provider and the mesh's own foundation store both want host port 5432, so they
# cannot share a machine — the collision that blocked this chain single-node. Here the foundation
# (store, broker, control) lives on `anchor` and NOTHING else; `laptop` runs the whole chain —
# postgres and redis PROVIDERS plus the baserow and letta CONSUMERS that require them. Both
# machines sit on one shared segment and enrol into the one mesh; only enrolment crosses to anchor,
# over the underlay both machines already share. Provider and consumers are co-located on laptop, so
# no cross-node module comms and no overlay are needed — and the 5432-vs-substrate conflict is gone
# because the substrate store is on the OTHER node.
# no cross-node module comms and no overlay are needed — and the 5432-vs-foundation conflict is gone
# because the foundation store is on the OTHER node.
scenario: two-node-db
segments:
@@ -16,7 +16,7 @@ segments:
cidr: [192.0.2.0/24]
machines:
# The substrate ONLY: store, broker, control — three containers. Four gigabytes is plenty for a
# The foundation ONLY: store, broker, control — three containers. Four gigabytes is plenty for a
# node that hosts no modules; the thrash the two-nodes bed warns of comes from stacking eleven
# containers on a node, which this one never does.
anchor:
@@ -43,7 +43,7 @@ machines:
disk: 60GiB
images:
- mesh-control:development
- mesh-controller:development
# The per-module runtimes, built by scripts/build-module-runtime.sh and stocked here. Each carries
# its module's provisioner, so no separate mesh-provision-* image is listed — the runtime is the
# provisioner (ADR 0048).
+2 -2
View File
@@ -17,7 +17,7 @@ machines:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
inbound: allow
# The whole substrate, the registry, the builder, an adopted workload and the modules under
# The whole foundation, the registry, the builder, an adopted workload and the modules under
# test all land here — eleven containers before the forge arrives. At the 1GiB default this
# machine thrashes, and it presents as "the mesh hangs": every exec slows from 15s to 105s
# and the forge test fails on a status poll that is merely queued behind page-outs.
@@ -30,7 +30,7 @@ machines:
memory: 2GiB
images:
- mesh-control:development
- mesh-controller:development
# And the builder, because it is a module the mesh assigns rather than a program somebody
# starts by hand — which is the only way its credential can be one the mesh delivered.
- mesh-builder:development
+7 -7
View File
@@ -1,15 +1,15 @@
# The whole `ace` server's converted service set, installed together on ONE node behind the mesh
# substrate — the media/home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of
# foundation — the media/home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of
# scenarios/whole-mesh-novox.yml; same topology, a different (larger, media-heavy) module set.
#
# Substrate (store, broker, control) rides `anchor` and NOTHING else; ALL of ace's services ride the
# Foundation (store, broker, control) rides `anchor` and NOTHING else; ALL of ace's services ride the
# `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres/redis
# providers co-located with them. The media stack (sonarr/radarr/lidarr/plex/bazarr/nzbget/
# qbittorrent/bookshelf) shares the operator-owned library directories under /services/media (ADR
# 0051 `accesses`); the test pre-creates them on the node, as the operator would, before the push —
# the mesh confirms the paths exist and mounts them, but creates and chowns none of it.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# The runtimes are built by scripts/build-module-runtime.sh (one per module) and must be in the local
# daemon, because nothing serves them and nothing can. Every media/app image is pulled from the
# internet by the node itself, over its uplink, by the digest its module.json already pins. The test
@@ -30,9 +30,9 @@ machines:
memory: 4GiB
cpus: 4
disk: 20GiB
# The substrate only, so the control plane's image only. The runtimes belong on the node that
# The foundation only, so the control plane's image only. The runtimes belong on the node that
# runs the modules, and a 20GiB disk has no room for them anyway.
images: [mesh-control:development]
images: [mesh-controller:development]
# The whole ace service set — 24 modules, ~50 containers, several heavy (Plex, Home Assistant,
# Letta ~1.8GiB, Baserow ~1.5GiB, the UniFi controller's JVM, mssql ~2GiB). Sized past novox.
ace:
@@ -42,7 +42,7 @@ machines:
memory: 18GiB
cpus: 8
disk: 120GiB
# Every runtime. Not mesh-control: the control plane runs on the anchor.
# Every runtime. Not mesh-controller: the control plane runs on the anchor.
images:
- mesh-runtime-postgres:development
- mesh-runtime-redis:development
@@ -70,7 +70,7 @@ machines:
- mesh-runtime-unifi:development
images:
- mesh-control:development
- mesh-controller:development
# The per-module runtimes (built by scripts/build-module-runtime.sh).
- mesh-runtime-postgres:development
- mesh-runtime-redis:development
+12 -12
View File
@@ -1,19 +1,19 @@
# The FULL mesh in its REAL production shape: two segments, one access point, one overlay.
#
# This is the first multi-segment whole-mesh bed. The earlier flat whole-mesh-full sat every node
# on one public segment with a SEPARATE `anchor` carrying the substrate. Production is not flat, and
# on one public segment with a SEPARATE `anchor` carrying the foundation. Production is not flat, and
# there is no separate anchor: `novox` IS the anchor. It sits on the routable `hosting` segment,
# runs the substrate (store, broker, control) AND its own service set AND is the overlay hub and the
# runs the foundation (store, broker, control) AND its own service set AND is the overlay hub and the
# public ingress. `ace`, `shanks` and `g14` sit on the household `home` segment BEHIND a NAT gateway
# — the access point — reachable from the outside only through what they dial out to.
#
# hosting (public, routable) home (private, behind the access point)
# novox 192.0.2.20 ── anchor ace 10.99.1.10 home server, media/IoT set
# substrate + novox set shanks 10.99.1.20 workstation (light)
# foundation + novox set shanks 10.99.1.20 workstation (light)
# overlay hub, ingress g14 10.99.1.30 workstation (light)
#
# The `home` gateway masquerades v4 outbound and forwards inbound (an ordinary household router).
# Home nodes reach novox's public 192.0.2.20 by dialling OUT through it: the substrate broker (5671),
# Home nodes reach novox's public 192.0.2.20 by dialling OUT through it: the foundation broker (5671),
# the mesh's own artifact store, and — the thing this bed exists to prove — the WireGuard overlay hub
# (51820/udp). The hub keepalive holds the NAT hole open so the tunnel, once formed, stays up. novox
# cannot initiate to a home node at all; every home↔novox path is either the overlay or a forwarded
@@ -35,20 +35,20 @@
# the gateway's masquerade? The driving test verifies the WireGuard handshake and cross-segment
# reachability over the overlay explicitly, and reports form-vs-break as its headline.
#
# Substrate-on-novox collides on two host ports the separate-anchor beds never hit: the substrate
# store binds 127.0.0.1:5432 and novox's postgres provider publishes 5432; the substrate broker binds
# Foundation-on-novox collides on two host ports the separate-anchor beds never hit: the foundation
# store binds 127.0.0.1:5432 and novox's postgres provider publishes 5432; the foundation broker binds
# 5671 + 127.0.0.1:5672 and novox's lavinmq provider publishes 5672. The driving test REMAPS those two
# provider host publishes off the substrate's ports (consumers reach the providers over the mesh
# provider host publishes off the foundation's ports (consumers reach the providers over the mesh
# network on the container port, so the host side is free to move). Reported as a topology finding.
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap MESH_LAB_CATALOG=.../mesh-catalog/modules
#
# GENESIS AND JOINING ARE TWO DIFFERENT ACTS, and this bed distinguishes them. novox is brought
# into existence by `mesh-bootstrap` — the same program a bare machine runs — and is afterwards a
# working mesh of one, with a registry and a control plane that is an ordinary module pinned to an
# image that registry serves. ace, shanks and g14 then JOIN it: host binary, token, enrol, run. No
# bootstrap, no substrate, no registry. novox is never enrolled twice, because the installer
# bootstrap, no foundation, no registry. novox is never enrolled twice, because the installer
# already did it.
#
# The images: are the UNION of the novox set (feat/novox-conversions @ 431310f: the slug + roundcube
@@ -76,8 +76,8 @@ segments:
mapping_ttl: 120s
machines:
# The anchor: substrate (store, broker, control) + the whole novox service set + overlay hub +
# public ingress. Bigger than the flat bed's novox, because it now carries the substrate too.
# The anchor: foundation (store, broker, control) + the whole novox service set + overlay hub +
# public ingress. Bigger than the flat bed's novox, because it now carries the foundation too.
novox:
at: { segment: hosting, address: [192.0.2.20] }
egress: true
@@ -90,7 +90,7 @@ machines:
# because they carry no runtime image of their own — what they run is third-party or is the
# node itself.
#
# **mesh-control is NOT here, and its absence is the point** (novox/hq ADR 0067). The anchor is
# **mesh-controller is NOT here, and its absence is the point** (novox/hq ADR 0067). The anchor is
# brought into existence by the installer, and the installer carries the control plane's image
# inside itself — that is the whole reason a machine that can reach no registry can still raise
# a mesh. Handing it over from the workstation as well would mean the bed never found out
+9 -9
View File
@@ -1,10 +1,10 @@
# The whole `novox` server's converted service set, installed together on ONE node behind the mesh
# substrate — the whole-catalogue install the rebuild has never actually run. First stage of a
# foundation — the whole-catalogue install the rebuild has never actually run. First stage of a
# whole-mesh rehearsal (novox/hq).
#
# Topology, proven by test/integration/assigned-two-node-db.test.ts: the substrate (store, broker,
# Topology, proven by test/integration/assigned-two-node-db.test.ts: the foundation (store, broker,
# control) rides `anchor` and NOTHING else; ALL of novox's services ride the `novox` node — its own
# postgres provider owns 5432 there, so it cannot co-locate with the substrate store on 5432. Both
# postgres provider owns 5432 there, so it cannot co-locate with the foundation store on 5432. Both
# machines sit on one public segment and enrol into the one mesh; an overlay is placed so a
# consumer's binding `at` resolves to novox's private address and every consumer reaches the
# providers co-located with it.
@@ -15,7 +15,7 @@
# apps portainer verdaccio registry route-proxy mailu
# node-level firewall fail2ban
#
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
# The runtimes are built by scripts/build-module-runtime.sh (one per module that has code) and the
# route-proxy image by scripts/build-route-proxy-image.sh; those must be in the local daemon,
# because nothing serves them and nothing can. Every third-party image is pulled from the internet
@@ -29,7 +29,7 @@ segments:
cidr: [192.0.2.0/24]
machines:
# The substrate ONLY: store, broker, control. Nothing else lands here.
# The foundation ONLY: store, broker, control. Nothing else lands here.
anchor:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
@@ -37,9 +37,9 @@ machines:
memory: 4GiB
cpus: 4
disk: 20GiB
# The substrate only, so the control plane's image only. Handing this machine the whole set of
# The foundation only, so the control plane's image only. Handing this machine the whole set of
# runtimes would fill a 20GiB disk with images nothing on it will ever start.
images: [mesh-control:development]
images: [mesh-controller:development]
# The whole novox service set — ~38 containers (five providers with runtimes, six consumers with
# runtimes, portainer/verdaccio/registry/route-proxy, the nine-container Mailu stack and its
# runtime) plus two node-level modules. mssql alone wants ~2GiB; Mailu, Nextcloud and Keycloak are
@@ -55,7 +55,7 @@ machines:
# layers and the runtimes. A hundred gigabytes holds the whole set without exhausting the disk
# mid-apply.
disk: 100GiB
# Every runtime, and the proxy. Not mesh-control: the control plane runs on the anchor.
# Every runtime, and the proxy. Not mesh-controller: the control plane runs on the anchor.
images:
- mesh-runtime-postgres:development
- mesh-runtime-redis:development
@@ -73,7 +73,7 @@ machines:
- mesh-route-proxy:development
images:
- mesh-control:development
- mesh-controller:development
# The per-module runtimes (built by scripts/build-module-runtime.sh). registry, route-proxy,
# invoicing, firewall and fail2ban carry no mesh-runtime image; route-proxy ships its own.
- mesh-runtime-postgres:development
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Build the route-proxy module's runtime image: the reference reverse proxy (novox/hq
# 08-connectivity §3), compiled from its canonical Go source in mesh-control into a container the
# 08-connectivity §3), compiled from its canonical Go source in mesh-controller into a container the
# lab can stock and serve by digest.
#
# Unlike the TypeScript modules (built by build-module-runtime.sh into a node tool runtime), the
# proxy is a Go program. The module ships only the packaging — a Dockerfile in mesh-catalog whose
# build context is the mesh-control repository root — and this script runs that build into the local
# build context is the mesh-controller repository root — and this script runs that build into the local
# docker daemon, which a scenario's registry then stocks and serves by digest.
#
# build-route-proxy-image.sh
@@ -13,7 +13,7 @@
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"; ROOT="$(cd "$HERE/.." && pwd)"
MESH_CONTROL="${MESH_CONTROL:-$ROOT/mesh-control}"
MESH_CONTROL="${MESH_CONTROL:-$ROOT/mesh-controller}"
MESH_CATALOG="${MESH_CATALOG:-$ROOT/mesh-catalog}"
TAG="${ROUTE_PROXY_TAG:-mesh-route-proxy:development}"
DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile"
@@ -22,7 +22,7 @@ DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile"
[ -f "$MESH_CONTROL/examples/route-proxy/main.go" ] || {
echo "no proxy source at $MESH_CONTROL/examples/route-proxy" >&2; exit 1; }
# Context is the mesh-control repository root: the proxy compiles against that module's go.mod and
# Context is the mesh-controller repository root: the proxy compiles against that module's go.mod and
# its examples/route-proxy package.
docker build -f "$DOCKERFILE" -t "$TAG" "$MESH_CONTROL"
echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)"
+2 -2
View File
@@ -103,7 +103,7 @@ export interface Machine {
* and a handful of containers.
*
* Declared, because it is a fact about the machine the scenario describes — the node that runs
* the whole substrate is bigger than the laptop that joins it, and a test that starves its
* the whole foundation is bigger than the laptop that joins it, and a test that starves its
* anchor at the default answers questions about memory pressure, not about the mesh. The forge
* test failed three times as "status hangs" before anyone counted the containers in 1GiB
* (novox/hq 04-ISSUES/024 is the same lesson about a different resource).
@@ -158,7 +158,7 @@ export interface Scenario {
* **The mesh's own images** — the ones that exist in no registry and are put onto a machine by
* whoever built them.
*
* mesh-control, mesh-builder, mesh-route-proxy, the per-module runtimes and the provisioners are
* mesh-controller, mesh-builder, mesh-route-proxy, the per-module runtimes and the provisioners are
* built from source and published nowhere. A machine gets them the way an operator's machine
* does: they are built on the workstation, loaded onto the machine, and named by the digest of
* their own image configuration. Written as tags, because a tag is what `docker save` can
+1 -1
View File
@@ -7,7 +7,7 @@
* from the internet, and that is now the thing worth proving before a raise says it is finished.
*
* The failure it exists to stop is the same one, in the same shape: `raise` returns, the caller
* applies a substrate, the first pull fails, no node enrols, and the instance is left a bare
* applies a foundation, the first pull fails, no node enrols, and the instance is left a bare
* shell — with the cause several steps back and looking like a mesh fault rather than a lab one.
*
* Two things are checked, in this order, because they fail differently and the difference is the
+2 -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
* 0 now does, so this is the seam where the lab acquires a consumer.
*
* Only `host` is placeable. Everything else in the placement vocabulary — the substrate, a
* Only `host` is placeable. Everything else in the placement vocabulary — the foundation, a
* control plane, a forge — is still refused by name rather than ignored, because a scenario
* that declares something and raises without it is the fault this lab was built to catch
* (novox/hq 04-ISSUES/003).
@@ -515,7 +515,7 @@ export async function loadHeldImages(
throw new Error(
`${requested} is not on this workstation, so there is nothing to hand the machines.\n` +
` It is one of the mesh's own images and exists in no registry — nothing can pull it.\n` +
` Build it first (mesh-control's \`make image …\`, or scripts/build-module-runtime.sh).`,
` Build it first (mesh-controller's \`make image …\`, or scripts/build-module-runtime.sh).`,
);
}
}
+1 -1
View File
@@ -338,7 +338,7 @@ export async function raise(
// **Only now is "this machine can reach the outside" a true statement.** The route, the
// gateway and the machine's own filtering are all in place, so this is the path a pull takes.
// A raise that returned without checking would hand the next step a fact it depends on and
// has no way to test — which is how a substrate apply used to die on its first pull.
// has no way to test — which is how a foundation apply used to die on its first pull.
enter("confirming egress reaches the internet");
await confirmEgress(scenario, byMachine, log);
+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
// 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>();
for (const { artifacts } of planPlacements(scenario)) {
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
* repository can pin a third-party image, because somebody can ask a registry what a tag points
* at. It cannot pin an image the mesh builds itself: mesh-control, mesh-builder, mesh-route-proxy,
* at. It cannot pin an image the mesh builds itself: mesh-controller, mesh-builder, mesh-route-proxy,
* the per-module runtimes and the provisioners exist in no registry, so there is no manifest
* digest to write down. The catalogue ships sixty-four zeros for them, which parses, resolves,
* composes — and stops on the machine.
@@ -54,7 +54,7 @@ export function repositoryOf(reference: string): string {
*
* **Derived from the shape the build produces, not from a list of names.** `make image
* builder-image provisioner-image objectstore-image redis-provisioner-image proxy-image` in
* mesh-control and `scripts/build-module-runtime.sh` here both tag their output `mesh-<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
* 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" },
});
}
const control = where["mesh-control"];
const control = where["mesh-controller"];
if (control) {
// **Every image the lab runs, not only the control plane's.**
//
@@ -84,7 +84,7 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] {
//
// It is built here at all because the bed now bootstraps THROUGH it (novox/hq ADR 0067): the
// anchor is brought into existence by running the same program a bare machine runs, rather than
// by the bed applying a substrate bundle by hand and calling that an install. An installer that
// by the bed applying a foundation bundle by hand and calling that an install. An installer that
// was stale would be a bed proving something about last week's procedure.
const installer = env["MESH_LAB_BOOTSTRAP_BINARY"];
if (installer && where["mesh-host"]) {
@@ -104,7 +104,7 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] {
* thing it was going to run and now carries the thing that makes it, so a raised mesh holds a
* control plane it built from a repository and a commit rather than one it was handed.
*
* `mesh-builder:development` is what mesh-control's `make builder-image` tags — one tag, said in
* `mesh-builder:development` is what mesh-controller's `make builder-image` tags — one tag, said in
* one place. Overridable because a release installer carries a release image, and nothing about
* that is the lab's business.
*/
+1 -1
View File
@@ -23,6 +23,6 @@ export function repositories(env: NodeJS.ProcessEnv = process.env): Repositories
const host = env["MESH_LAB_HOST_BINARY"];
if (host) found["mesh-host"] = dirname(host);
const modules = env["MESH_LAB_MODULES"];
if (modules) found["mesh-control"] = dirname(dirname(modules));
if (modules) found["mesh-controller"] = dirname(dirname(modules));
return found;
}
+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
* bespoke at-rest envelope the module opens with a node private key the mesh must somehow place — a
* module is never given a node's private key, so that path could not exist. It rides the ORDINARY
* sealed-delivery path instead: mesh-control (via the manager module at adoption) seals it to the
* sealed-delivery path instead: mesh-controller (via the manager module at adoption) seals it to the
* manager node's PUBLIC key, and the HOST unseals it with that node's real private key and mounts the
* cleartext at the manager module's bound secret path — exactly as a consumer's db password arrives.
* So there is no fake node key pair mounted here any more; the host's own real sealing key does the
@@ -32,11 +32,11 @@
* ~/.claude/.credentials.json, access-token-only.
*
* STUBBED, and flagged in the report: (a) the vendor OAuth endpoint (a node stub); (b) the submit
* transport (the test invokes `mesh-control licence submit-refresh` on the manager's output, standing
* transport (the test invokes `mesh-controller licence submit-refresh` on the manager's output, standing
* in for the authenticated cross-node call a manager node would make). The node-private-key stub of
* the earlier cut is GONE — the host uses its own real key.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* Build both runtime images into the local daemon first:
* scripts/build-module-runtime.sh anthropic-manager /tmp/anthropic-manager.tar
* scripts/build-module-runtime.sh anthropic-consumer /tmp/anthropic-consumer.tar
@@ -49,7 +49,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -61,7 +61,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "anthropic-bed";
@@ -96,22 +96,22 @@ async function must(command: string, timeoutMs?: number): Promise<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
// one to `mesh-control` — whose process runs as a non-root user — the test relaxes the mode on the
// one to `mesh-controller` — whose process runs as a non-root user — the test relaxes the mode on the
// anchor host (where `must` is root) and then copies it in: `docker cp` preserves the source mode, so
// the file lands 0644 and mesh-control (a distroless image with no `chmod` of its own) can read it.
// the file lands 0644 and mesh-controller (a distroless image with no `chmod` of its own) can read it.
// What is staged this way is a sealed box or the access token, never a cleartext refresh token, so a
// world-readable copy discloses nothing the control plane does not already hold. In production the
// operator who ran adopt owns the file and this does not arise.
async function stageIntoControl(hostPath: string, dest: string): Promise<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 }> {
return on(`docker exec mesh-control /mesh-control ${command}`);
return on(`docker exec mesh-controller /mesh-controller ${command}`);
}
/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */
@@ -120,7 +120,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -173,11 +173,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -239,7 +239,7 @@ test("model access refreshes on the manager node and delivers only the access to
},
],
});
await must(`printf %s ${quote(managerManifest)} > /tmp/anthropic-manager.json && docker cp /tmp/anthropic-manager.json mesh-control:/anthropic-manager.json`);
await must(`printf %s ${quote(managerManifest)} > /tmp/anthropic-manager.json && docker cp /tmp/anthropic-manager.json mesh-controller:/anthropic-manager.json`);
await mesh(`module add /anthropic-manager.json`);
await mesh(`module issue anthropic-manager --node ${MACHINE}`);
await mesh(`assign ${MACHINE} anthropic-manager`);
@@ -355,7 +355,7 @@ test("model access refreshes on the manager node and delivers only the access to
},
],
});
await must(`printf %s ${quote(consumerManifest)} > /tmp/anthropic-consumer.json && docker cp /tmp/anthropic-consumer.json mesh-control:/anthropic-consumer.json`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/anthropic-consumer.json && docker cp /tmp/anthropic-consumer.json mesh-controller:/anthropic-consumer.json`);
await mesh(`module add /anthropic-consumer.json`);
await mesh(`module issue anthropic-consumer --node ${MACHINE}`);
await mesh(`assign ${MACHINE} anthropic-consumer`);
+15 -15
View File
@@ -7,10 +7,10 @@
* container that connects over amqps with that account — never the broker's own. The trail filling
* is the proof the delivered, scoped credential authenticated and the subscription bound.
*
* It needs the host binary, the substrate bundle, and the runtime image the scenario loads:
* It needs the host binary, the foundation bundle, and the runtime image the scenario loads:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-runtime-image.sh builds mesh-runtime-audit:development into the local daemon,
* which scenarios/audit-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it.
*/
@@ -22,7 +22,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -34,7 +34,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "audit-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -74,9 +74,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -89,7 +89,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -121,12 +121,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -165,7 +165,7 @@ test("the mesh assigns the audit logger, and it consumes over the account the me
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/audit.json && docker cp /tmp/audit.json mesh-control:/audit.json`);
await must(`printf %s ${quote(manifest)} > /tmp/audit.json && docker cp /tmp/audit.json mesh-controller:/audit.json`);
await mesh("module add /audit.json");
// The mesh issues its scoped account and seals it to this machine, then assigns and pushes it.
@@ -209,7 +209,7 @@ test("the mesh assigns the audit logger, and it consumes over the account the me
// And the account the mesh made for it is a real one on the broker — the trail above already
// proved it authenticated and read its queue. That it reaches no further than its own queue is
// the scope CreateModuleAccount applies, checked as patterns in mesh-control's own tests.
// the scope CreateModuleAccount applies, checked as patterns in mesh-controller's own tests.
const users = await must(`docker exec mesh-broker lavinmqctl list_users 2>&1`);
assert.match(users, /anchor-audit-logger/, `the scoped account is not on the broker:\n${users}`);
});
@@ -17,7 +17,7 @@
*
* All are assigned to the one anchor, pushed ONCE, and the node converges ONCE with every one up.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh {mongodb,unifi,postgres} build the runtime images into the local
* daemon; scenarios/catalogue-apps.yml stocks them. mongo:7, lscr.io/linuxserver/unifi-controller
* and synesthesiam/marytts must be in the local daemon to be stocked.
@@ -30,7 +30,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -42,7 +42,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "catalogue-apps";
@@ -73,7 +73,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -82,9 +82,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -97,7 +97,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -129,12 +129,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -310,7 +310,7 @@ test("the mesh assigns mongodb, unifi, marrytts and postgres to one node in one
// --- add, issue (those that serve/emit), assign, then ONE push ----------------------------------
async function add(name: string, manifest: string): Promise<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`);
}
@@ -23,7 +23,7 @@
*
* All is assigned to the one anchor, pushed ONCE, and the node converges ONCE with both modules up.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh {sonarr,radarr} build the runtime images into the local daemon;
* scenarios/catalogue-media.yml stocks them. lscr.io/linuxserver/{sonarr,radarr} must be in the
* local daemon; the service images are pulled from the internet. Each *arr runtime is given a lab
@@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -50,7 +50,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "catalogue-media";
@@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -90,9 +90,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -105,7 +105,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -137,12 +137,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -248,7 +248,7 @@ test("sonarr and radarr, both accessing one operator-owned directory, co-resolve
// --- add, issue (both emit events → each gets a scoped broker account), assign ------------------
async function add(name: string, manifest: string): Promise<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`);
}
@@ -274,7 +274,7 @@ test("sonarr and radarr, both accessing one operator-owned directory, co-resolve
// read as two modules owning one path. Now each ACCESSES it, so the pair co-resolves and both are
// sent in a single declaration. A refusal here (nonzero, or "could not be resolved") is the
// regression this bed exists to catch.
const pushed = await on(`docker exec mesh-control /mesh-control push ${MACHINE}`);
const pushed = await on(`docker exec mesh-controller /mesh-controller push ${MACHINE}`);
assert.ok(pushed.ok,
`the co-resident push was REFUSED — the shared-access collision ADR 0051 removed is back:\n${pushed.out}`);
assert.doesNotMatch(pushed.out, /could not be resolved|both declare the path|shared data is the operator/,
@@ -22,7 +22,7 @@
* the seed), and mosquitto's provisioner — running in the assigned runtime — creates a scoped client
* for a contribution the mesh delivered, which then authenticates with the password the mesh minted.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh mosquitto builds mesh-runtime-mosquitto:development (carrying
* mosquitto_ctrl and the compiled bootstrap entrypoint) into the local daemon, which
* scenarios/catalogue-mqtt.yml stocks. eclipse-mosquitto:2 must be in the local daemon to be
@@ -36,7 +36,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -48,7 +48,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "catalogue-mqtt";
@@ -78,7 +78,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -87,9 +87,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -102,7 +102,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -134,12 +134,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh and starts the host so it applies what it is pushed.
@@ -187,7 +187,7 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker
serves: { "mqtt-topic": {} },
emits: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
// The events entrypoint subscribes to its own lifecycle events (an audit log), so it consumes
// them too — declared, or the substrate never makes the queue the runtime binds (ADR 0046).
// them too — declared, or the foundation never makes the queue the runtime binds (ADR 0046).
consumes: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
receives: { "mqtt-topic": "/var/lib/mosquitto-module/grants/mesh.json" },
grants: { "mqtt-topic": "/var/lib/mosquitto-module/grants" },
@@ -254,7 +254,7 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker
],
});
await must(`printf %s ${quote(manifest)} > /tmp/mosquitto.json && docker cp /tmp/mosquitto.json mesh-control:/mosquitto.json`);
await must(`printf %s ${quote(manifest)} > /tmp/mosquitto.json && docker cp /tmp/mosquitto.json mesh-controller:/mosquitto.json`);
await mesh("module add /mosquitto.json");
const issued = await mesh(`module issue mosquitto --node ${MACHINE}`);
assert.match(issued, /scoped to what it emits and consumes/, issued);
@@ -324,7 +324,7 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker
// each consumer it reads the login the mesh derived and the mesh-minted password the host unsealed,
// and creates exactly that dynsec client, scoped to its own topic subtree (ADR 0048). A
// hand-written contributions file + secret stand in for the control plane's write; their SHAPE is
// what mesh-control produces. The proof is authentication as the consumer with the mesh's password
// what mesh-controller produces. The proof is authentication as the consumer with the mesh's password
// — a provisioner that invented its own would refuse the connection.
const consumerPw = "mesh-minted-mqtt-7b2e1a";
await must(`printf %s ${quote(consumerPw)} > /var/lib/mosquitto-module/grants/app.secret`);
@@ -18,7 +18,7 @@
* path is fulfilled by creating the consumer's login with the mesh-minted password — it seals nothing
* and needs no seal key (novox/hq ADR 0048, issue 032-provider-runtime-has-no-seal-key).
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh {postgres,redis,minio,plex} build the four runtime images into the
* local daemon; scenarios/catalogue-small.yml stocks them. postgres:17-alpine, redis:7-alpine and
* minio/minio:latest is pulled from the internet by the node itself.
@@ -31,7 +31,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -43,7 +43,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "catalogue-small";
@@ -74,7 +74,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -83,9 +83,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -104,7 +104,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -136,12 +136,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -340,7 +340,7 @@ test("the mesh assigns postgres, redis, minio and plex to one node in one push,
// --- add, issue (the four that serve/emit), assign, then ONE push -------------------------------
async function add(name: string, manifest: string): Promise<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`);
}
@@ -455,7 +455,7 @@ test("the mesh assigns postgres, redis, minio and plex to one node in one push,
// issue 032): for each consumer it reads the login the mesh derived and the mesh-minted password the
// host unsealed, and creates the ACL user under exactly that login and password — sealing nothing
// and writing no credential file. A hand-written contributions file and secret stand in for the
// control plane's write; their SHAPE is what mesh-control produces. The proof is authentication as
// control plane's write; their SHAPE is what mesh-controller produces. The proof is authentication as
// the consumer with the mesh's password (PONG) — a provisioner that invented its own would answer
// WRONGPASS.
const redisPassword = "mesh-minted-9f3c2a";
+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
* tools loaded from them.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh grafana builds mesh-runtime-grafana:development into the local
* daemon, which scenarios/grafana-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it.
*/
@@ -22,7 +22,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -34,7 +34,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "grafana-node";
@@ -63,7 +63,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -72,7 +72,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -85,7 +85,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -117,11 +117,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -163,13 +163,13 @@ test("the mesh assigns grafana's runtime, configured by settings, and it serves
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-control:/grafana.json`);
await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-controller:/grafana.json`);
await mesh("module add /grafana.json");
// The operator states grafana's URL and API token as settings for this node — the config the
// runtime will read. Nothing about them is in the manifest.
const settings = JSON.stringify({ url: "http://127.0.0.1:3000", token: "lab-grafana-token" });
await must(`printf %s ${quote(settings)} > /tmp/grafana-settings.json && docker cp /tmp/grafana-settings.json mesh-control:/grafana-settings.json`);
await must(`printf %s ${quote(settings)} > /tmp/grafana-settings.json && docker cp /tmp/grafana-settings.json mesh-controller:/grafana-settings.json`);
await mesh(`settings set grafana /grafana-settings.json --node ${MACHINE}`);
const issued = await mesh(`module issue grafana --node ${MACHINE}`);
+19 -19
View File
@@ -1,7 +1,7 @@
/**
* The usage context store, proved end to end (novox/hq ADR 0054).
*
* model-usage is a MODULE, not a control-plane feature, because mesh-control is a CLI and cannot
* model-usage is a MODULE, not a control-plane feature, because mesh-controller is a CLI and cannot
* consume events: the store that keeps the latest usage reading has to be something that subscribes
* to `module.*.usage.*` and upserts. This bed raises a real mesh, provisions model-usage its own
* postgres store, emits usage events into the mesh, and reads the store back to prove the three
@@ -14,15 +14,15 @@
* 3. IN THE CLEAR — the value and its raw payload are ordinary columns an ordinary select returns;
* nothing about usage is sealed.
*
* The topology mirrors two-node-db: the app-postgres provider and the mesh's own substrate store both
* want host port 5432, so the substrate (store, broker, control) owns `anchor` and NOTHING else, and
* The topology mirrors two-node-db: the app-postgres provider and the mesh's own foundation store both
* want host port 5432, so the foundation (store, broker, control) owns `anchor` and NOTHING else, and
* `laptop` runs the postgres PROVIDER and the model-usage CONSUMER co-located. Only enrolment crosses
* to anchor, over the underlay both machines share.
*
* It needs a host binary and the substrate bundle:
* It needs a host binary and the foundation bundle:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
*
* HELPER — stock the two runtimes into the local daemon before the run (some may already be there):
* scripts/build-module-runtime.sh postgres /tmp/postgres.tar
@@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -50,12 +50,12 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "model-usage-bed";
/** The node that carries the postgres provider and the model-usage consumer. anchor carries only the
* substrate. */
* foundation. */
const NODE = "laptop";
let instanceId = "";
@@ -83,7 +83,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
/** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<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. */
@@ -92,9 +92,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -120,7 +120,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise<void> {
} | undefined;
let said = "";
try {
const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`);
const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`);
said = asked.out;
if (asked.ok) state = JSON.parse(said);
} catch (err) {
@@ -160,11 +160,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) {
@@ -269,7 +269,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
});
async function addIssueAssign(name: string, manifest: string): Promise<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 issue ${name} --node ${NODE}`);
await mesh(`assign ${NODE} ${name}`);
@@ -289,7 +289,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
await mesh(`push ${NODE}`);
await settled(NODE);
// The provider owns 5432 on laptop; the substrate store owns it on anchor.
// The provider owns 5432 on laptop; the foundation store owns it on anchor.
const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`);
for (const name of ["postgres", "mesh-postgres", "mesh-model-usage"]) {
assert.match(onLaptop, new RegExp(`(^|\\n)${name}(\\n|$)`),
@@ -331,7 +331,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
assert.match(tableReady, /^t$/m, `the usage table was never created (the consumer did not migrate):\n${tableReady}`);
// Inject a usage event into the mesh. model-usage is a PURE CONSUMER, so its own broker account has
// no publish right (mesh-control grants write to mesh.events only to a module that declares `emits`).
// no publish right (mesh-controller grants write to mesh.events only to a module that declares `emits`).
// So publish from the postgres provider's container — a publisher already on the node — overriding
// the source header to the real producer the key names, exactly as events.test.ts injects with
// `-e MESH_MODULE=...`. Write permission is per-exchange, not per-key, so postgres may carry any
+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
* the scoped account and never the broker's own.
*
* It needs the host binary, the substrate bundle, and the runtime image the scenario loads:
* It needs the host binary, the foundation bundle, and the runtime image the scenario loads:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh plex builds mesh-runtime-plex:development into the local daemon,
* which scenarios/plex-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it.
*/
@@ -25,7 +25,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -37,7 +37,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "plex-node";
@@ -68,7 +68,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -77,9 +77,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -92,7 +92,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -124,12 +124,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -176,7 +176,7 @@ test("the mesh assigns plex's runtime, and it serves plex's tools over the accou
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/plex.json && docker cp /tmp/plex.json mesh-control:/plex.json`);
await must(`printf %s ${quote(manifest)} > /tmp/plex.json && docker cp /tmp/plex.json mesh-controller:/plex.json`);
await mesh("module add /plex.json");
// The mesh issues plex's scoped account and seals it to this machine, then assigns and pushes it.
@@ -210,7 +210,7 @@ test("the mesh assigns plex's runtime, and it serves plex's tools over the accou
`plex's runtime never bound its serve queue:\n${(await on(`docker logs mesh-plex 2>&1 | tail -20`)).out}\n---\n${served}`);
// A caller invokes plex.plex_reachable over the mesh, from the bootstrap account (a caller, like
// mesh-control's command API — plex's own account serves, it does not call). The reply is the
// mesh-controller's command API — plex's own account serves, it does not call). The reply is the
// tool's own answer: it ran in the assigned runtime and reported the Plex server is unreachable
// (there is none in the lab). A reply at all — not a timeout — is the proof the invocation routed
// to the assigned runtime and ran plex's real code under its scoped account.
+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
* lab-local key so the mechanism can be proven; the delivery is a separate, open design question.
*
* It needs the host binary, the substrate bundle, and the runtime image the scenario loads:
* It needs the host binary, the foundation bundle, and the runtime image the scenario loads:
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development into the local
* daemon, which scenarios/redis-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it.
*/
@@ -26,7 +26,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -38,7 +38,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "redis-node";
@@ -67,7 +67,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -76,7 +76,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -89,7 +89,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -121,11 +121,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -151,7 +151,7 @@ test("the mesh assigns redis, and its runtime serves tools and provisions grants
version: "1",
emits: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"],
// redis's events entrypoint subscribes to its own lifecycle events (an audit-trail log), so it
// consumes them too — declared, or the substrate never makes the queue the runtime binds and it
// consumes them too — declared, or the foundation never makes the queue the runtime binds and it
// crashes on start with a 404 (novox/hq ADR 0046: a consume is declared).
consumes: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"],
"own-secrets": { default: "/var/lib/redis-module/default.secret", broker: "/var/lib/mesh/redis/broker" },
@@ -190,7 +190,7 @@ test("the mesh assigns redis, and its runtime serves tools and provisions grants
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`);
await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`);
await mesh("module add /redis.json");
const issued = await mesh(`module issue redis --node ${MACHINE}`);
+14 -14
View File
@@ -25,8 +25,8 @@
* registry by digest; the host pulls and runs it on the cadence.
*
* MESH_LAB_HOST_BINARY=.../mesh-host (feat/apply-schedule — the scheduler that fires the step)
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_MODULES=.../mesh-control/examples/modules (feat/schedule-container — the parser that
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* MESH_LAB_MODULES=.../mesh-controller/examples/modules (feat/schedule-container — the parser that
* carries `schedule` through). scenarios/schedule-tick.yml stocks alpine:latest (which must be in
* the local daemon) and serves it by digest; there is no runtime image — schedtest is a bare tick.
*/
@@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -50,7 +50,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "schedule-tick";
@@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -90,9 +90,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -119,7 +119,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -151,12 +151,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -197,7 +197,7 @@ test("the mesh assigns schedtest: installing the schedule does not run it, and t
],
});
await must(`printf %s ${quote(manifest)} > /tmp/schedtest.json && docker cp /tmp/schedtest.json mesh-control:/schedtest.json`);
await must(`printf %s ${quote(manifest)} > /tmp/schedtest.json && docker cp /tmp/schedtest.json mesh-controller:/schedtest.json`);
await mesh("module add /schedtest.json");
// No `module issue`: schedtest serves/consumes nothing and carries no runtime, so it needs no
// broker account. `assign` resolves its plan (no own-secret to fill) and ONE push converges it.
+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
* to reach — that the serve queue is bound is the proof the key was detected and the tools loaded.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh sonarr builds mesh-runtime-sonarr:development into the local
* daemon, which scenarios/sonarr-node.yml stocks — so no MESH_LAB_RUNTIME here; the host pulls it.
*/
@@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -32,7 +32,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "sonarr-node";
@@ -61,7 +61,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -70,7 +70,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -83,7 +83,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -115,11 +115,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -167,7 +167,7 @@ test("the mesh assigns sonarr's runtime, and it detects its key and serves its t
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/sonarr.json && docker cp /tmp/sonarr.json mesh-control:/sonarr.json`);
await must(`printf %s ${quote(manifest)} > /tmp/sonarr.json && docker cp /tmp/sonarr.json mesh-controller:/sonarr.json`);
await mesh("module add /sonarr.json");
const issued = await mesh(`module issue sonarr --node ${MACHINE}`);
@@ -17,7 +17,7 @@
* A tool would only fail if it were actually invoked without real creds — which this bed does not do,
* because the point is exactly that serving does not require them.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh confluence builds mesh-runtime-confluence:development into the
* local daemon; scenarios/tools-confluence.yml stocks it. There is no service image.
*/
@@ -29,7 +29,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -41,7 +41,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "tools-confluence";
@@ -72,7 +72,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -81,9 +81,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -96,7 +96,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -128,12 +128,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -185,7 +185,7 @@ test("the mesh assigns confluence: its tools-only runtime comes up and serves th
],
});
await must(`printf %s ${quote(manifest)} > /tmp/confluence.json && docker cp /tmp/confluence.json mesh-control:/confluence.json`);
await must(`printf %s ${quote(manifest)} > /tmp/confluence.json && docker cp /tmp/confluence.json mesh-controller:/confluence.json`);
await mesh("module add /confluence.json");
// confluence serves tools, so it is issued a scoped broker account (it emits/consumes nothing else).
const issued = await mesh(`module issue confluence --node ${MACHINE}`);
+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,
* because the point is exactly that serving does not require them.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh gitlab builds mesh-runtime-gitlab:development into the local
* daemon; scenarios/tools-gitlab.yml stocks it. There is no service image — gitlab is tools-only.
*/
@@ -28,7 +28,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -40,7 +40,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "tools-gitlab";
@@ -71,7 +71,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -80,9 +80,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -95,7 +95,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -127,12 +127,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and start the host so it
@@ -184,7 +184,7 @@ test("the mesh assigns gitlab: its tools-only runtime comes up and serves the fu
],
});
await must(`printf %s ${quote(manifest)} > /tmp/gitlab.json && docker cp /tmp/gitlab.json mesh-control:/gitlab.json`);
await must(`printf %s ${quote(manifest)} > /tmp/gitlab.json && docker cp /tmp/gitlab.json mesh-controller:/gitlab.json`);
await mesh("module add /gitlab.json");
// gitlab serves tools, so it is issued a scoped broker account (it emits/consumes nothing else).
const issued = await mesh(`module issue gitlab --node ${MACHINE}`);
+30 -30
View File
@@ -1,18 +1,18 @@
/**
* The DB-consumer chain a single node cannot host, proved across two machines.
*
* app-postgres and the mesh's own substrate store both want host port 5432, so they cannot share a
* machine. Every earlier catalogue bed put the provider on the same node as the substrate and got
* app-postgres and the mesh's own foundation store both want host port 5432, so they cannot share a
* machine. Every earlier catalogue bed put the provider on the same node as the foundation and got
* away with it only because the provider published no 5432 a consumer ever reached, or because the
* substrate's store and the module's postgres were the same container. The moment a real
* foundation's store and the module's postgres were the same container. The moment a real
* postgres PROVIDER must publish 5432 for real consumers to connect, it collides with the store the
* substrate already has there — and the chain is blocked single-node.
* foundation already has there — and the chain is blocked single-node.
*
* This is the split that unblocks it. `anchor` runs the substrate (store, broker, control) and
* This is the split that unblocks it. `anchor` runs the foundation (store, broker, control) and
* NOTHING else. `laptop` runs the whole chain: the postgres and redis PROVIDERS, and the baserow
* and letta CONSUMERS that require them. Provider and consumers are co-located on laptop, so the
* grant never crosses a node boundary and no overlay is needed — only enrolment crosses to anchor,
* over the underlay both machines share. And because the substrate store is on the OTHER node, the
* over the underlay both machines share. And because the foundation store is on the OTHER node, the
* provider owns laptop's 5432 uncontested.
*
* The four manifests are the committed catalogue shapes (novox/hq ADR 0039/0047/0048), verbatim
@@ -22,10 +22,10 @@
* ONCE; laptop converges once with every one up, and the two consumers are provisioned against the
* database the provider on their own node gave them.
*
* It needs a host binary and the substrate bundle:
* It needs a host binary and the foundation bundle:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
*
* HELPER — stock the four runtimes into the local daemon before the run (some may already be there):
* scripts/build-module-runtime.sh postgres /tmp/postgres.tar
@@ -44,7 +44,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -56,11 +56,11 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "two-node-db";
/** The node that carries the whole DB-consumer chain. anchor carries only the substrate. */
/** The node that carries the whole DB-consumer chain. anchor carries only the foundation. */
const NODE = "laptop";
let instanceId = "";
@@ -88,7 +88,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
/** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<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. */
@@ -97,9 +97,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -128,7 +128,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise<void> {
} | undefined;
let said = "";
try {
const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`);
const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`);
said = asked.out;
if (asked.ok) state = JSON.parse(said);
} catch (err) {
@@ -169,13 +169,13 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// The first node raises the substrate — store, broker, control — from the bundle its host carries,
// The first node raises the foundation — store, broker, control — from the bundle its host carries,
// its digests rewritten to the ones this scenario's own registry serves.
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// Both machines join the one mesh, each with a token that says what the mesh calls it, and each
@@ -196,7 +196,7 @@ after(async () => {
await destroyAll(`${SCENARIO}-`);
}, { timeout: 600_000 });
test("the provider and its consumers ride the second node while the substrate owns 5432 on the first", {
test("the provider and its consumers ride the second node while the foundation owns 5432 on the first", {
skip, timeout: 1_500_000,
}, async () => {
// ================================================================================================
@@ -392,7 +392,7 @@ test("the provider and its consumers ride the second node while the substrate ow
// --- add, issue a scoped broker account, assign to laptop, then ONE push -------------------------
async function addIssueAssign(name: string, manifest: string): Promise<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`);
const issued = await mesh(`module issue ${name} --node ${NODE}`);
assert.match(issued, /scoped to what it emits and consumes/, issued);
@@ -400,7 +400,7 @@ test("the provider and its consumers ride the second node while the substrate ow
}
// The consumer connects to its provider by the provider's PRIVATE-NETWORK address — the binding's
// `at`, which mesh-control fills as "where the consuming machine is on the private network, empty
// `at`, which mesh-controller fills as "where the consuming machine is on the private network, empty
// if it is not on one" (declaration.go). So even though provider and consumer are co-located on
// laptop, the address baserow is handed is the mesh OVERLAY address, and it is empty unless the
// machine is on the overlay. The overlay networking is therefore assigned first, to both nodes.
@@ -411,7 +411,7 @@ test("the provider and its consumers ride the second node while the substrate ow
// Providers first, then the consumers that require them. The mesh resolves the whole set at push
// time regardless of order; this order simply reads like the dependency graph. Provider AND
// consumers all go to laptop; the 5432 conflict is gone because the substrate store is on anchor.
// consumers all go to laptop; the 5432 conflict is gone because the foundation store is on anchor.
await addIssueAssign("postgres", postgresManifest);
await addIssueAssign("redis", redisManifest);
await addIssueAssign("baserow", baserowManifest);
@@ -422,14 +422,14 @@ test("the provider and its consumers ride the second node while the substrate ow
await settled(NODE);
// ================================================================================================
// THE two-node split — the substrate owns 5432 on anchor, the provider owns it on laptop.
// THE two-node split — the foundation owns 5432 on anchor, the provider owns it on laptop.
// ================================================================================================
const onAnchor = await must("anchor", `docker ps --format '{{.Names}}'`);
const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`);
assert.match(onAnchor, /(^|\n)mesh-store(\n|$)/, "the substrate store is not on the first node");
assert.match(onAnchor, /(^|\n)mesh-store(\n|$)/, "the foundation store is not on the first node");
assert.doesNotMatch(onAnchor, /(^|\n)postgres(\n|$)/,
"the postgres provider landed on the substrate node — the 5432 collision this bed exists to avoid");
assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the substrate store leaked onto the second node");
"the postgres provider landed on the foundation node — the 5432 collision this bed exists to avoid");
assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the foundation store leaked onto the second node");
assert.match(onLaptop, /(^|\n)postgres(\n|$)/, "the postgres provider is not on the second node");
// ================================================================================================
@@ -453,7 +453,7 @@ test("the provider and its consumers ride the second node while the substrate ow
// REGRESSION (provider-seal-key): baserow's server.env DATABASE_PASSWORD is filled from the
// ${secret:postgres-database} placeholder; its database.secret file carries the same credential via
// the secrets: map. Both are baserow's one postgres password and MUST be equal. Before the
// mesh-control fix (secrets_into_files.go matched a need by provision name alone, not by consuming
// mesh-controller fix (secrets_into_files.go matched a need by provision name alone, not by consuming
// module) the placeholder path took whichever co-located consumer came last — letta's — so the two
// diverged and baserow authenticated with the wrong password. novox/hq 04-ISSUES/022.
{
@@ -496,7 +496,7 @@ test("the provider and its consumers ride the second node while the substrate ow
}
// ================================================================================================
// Each module got its own scoped broker account on the substrate's broker (which is on anchor,
// Each module got its own scoped broker account on the foundation's broker (which is on anchor,
// reached from laptop over the shared segment) — named for the node that runs it and the module.
// ================================================================================================
const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`);
+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.
*
* MESH_LAB_HOST_BINARY a built mesh-host
* MESH_LAB_BUNDLE the substrate bundle
* MESH_LAB_BUNDLE the foundation bundle
* MESH_LAB_BUILDER a built mesh-builder
*/
@@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
@@ -35,7 +35,7 @@ const skip = !capability.usable
: !host || !existsSync(host)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle"
? "MESH_LAB_BUNDLE is not set to a foundation bundle"
: !builder || !existsSync(builder)
? "MESH_LAB_BUILDER is not set to a built mesh-builder"
: false;
@@ -79,12 +79,12 @@ async function must(command: string): Promise<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. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
before(async () => {
@@ -92,8 +92,8 @@ before(async () => {
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {});
instanceId = raised.instanceId;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`);
// The mesh's artifact store, standing where the `registry` module would. Read back rather than
// assumed: a builder publishing into a registry that never came up fails several minutes later,
@@ -166,7 +166,7 @@ test("a build that cannot succeed says why, and records nothing", { skip, timeou
// A failure is a result. A build that fails silently is indistinguishable from a builder that
// is not running, and those want completely different responses.
const { out, ok } = await on(
`docker exec mesh-control /mesh-control build /root/does-not-exist --wait 120s`,
`docker exec mesh-controller /mesh-controller build /root/does-not-exist --wait 120s`,
);
assert.equal(ok, false, "a build of nothing reported success");
assert.match(out, /could not build/, out);
+6 -6
View File
@@ -35,7 +35,7 @@ const skip = !capability.usable
: !host || !existsSync(host)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle"
? "MESH_LAB_BUNDLE is not set to a foundation bundle"
: false;
const SCENARIO = "first-node";
@@ -62,7 +62,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
}
const mesh = (command: string, timeoutMs?: number) =>
must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs);
before(async () => {
if (skip) return;
@@ -70,9 +70,9 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${
pinnedInto(readFileSync(bundle, "utf8"), raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 300_000);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 300_000);
// **And the machine joins.** Applying the bundle raises a control plane; it does not tell that
// control plane a machine exists. Leaving this out is what the first run of this canary found,
@@ -111,7 +111,7 @@ test("a module reaches the machine", { skip, timeout: 600_000 }, async () => {
{ id: "note", type: "file", path: "/var/lib/canary/it-arrived", content: "yes\n", mode: "0644" },
],
}))} > /tmp/canary.json`);
await must(`docker cp /tmp/canary.json mesh-control:/canary.json`);
await must(`docker cp /tmp/canary.json mesh-controller:/canary.json`);
await mesh("module add /canary.json");
await mesh(`assign ${MACHINE} canary`);
await mesh(`push ${MACHINE}`, 300_000);
@@ -151,7 +151,7 @@ test("a consumer gets a credential it can use", { skip, timeout: 600_000 }, asyn
],
}))} > /tmp/canary-app.json`);
for (const name of ["canary-store", "canary-app"]) {
await must(`docker cp /tmp/${name}.json mesh-control:/${name}.json`);
await must(`docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
await mesh(`assign ${MACHINE} ${name}`);
}
+1 -1
View File
@@ -25,7 +25,7 @@ const proxy = process.env["MESH_LAB_ROUTE_PROXY"] ?? "";
const skip = !capability.usable
? `lab not usable: ${capability.why}`
: !proxy
? "set MESH_LAB_ROUTE_PROXY to a built proxy (mesh-control: go build ./examples/route-proxy)"
? "set MESH_LAB_ROUTE_PROXY to a built proxy (mesh-controller: go build ./examples/route-proxy)"
: false;
const SCENARIO = "a-public-name";
+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
* events exchange, where a module emits and any number listen, and the audit logger consumes `#`
* and writes down what happened. It runs against the `mesh-broker` this scenario's own host raised
* from the substrate bundle — not a broker a test stood up — because "the mesh hosts the broker"
* (tier-1 substrate) is the thing being relied on.
* from the foundation bundle — not a broker a test stood up — because "the mesh hosts the broker"
* (tier-1 foundation) is the thing being relied on.
*
* The wire shape it asserts is ADR 0047: metadata rides as headers so the body is only the
* payload, a consumer gets a durable per-consumer queue `<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
* message happened to arrive.
*
* It needs the host binary and the substrate bundle, like the mesh walk, plus a runtime image:
* It needs the host binary and the foundation bundle, like the mesh walk, plus a runtime image:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* MESH_LAB_RUNTIME=.../mesh-runtime-audit.tar (docker save of the runtime+audit-logger image;
* built by scripts/build-runtime-image.sh)
*
@@ -33,7 +33,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
@@ -48,7 +48,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: !runtime || !existsSync(runtime)
? "MESH_LAB_RUNTIME is not set to a runtime image tar (scripts/build-runtime-image.sh)"
: false;
@@ -58,7 +58,7 @@ const MACHINE = "anchor";
/** Where the audit-logger container writes its trail, on the machine — mounted from a host dir. */
const TRAIL_DIR = "/var/lib/mesh-audit";
const TRAIL = `${TRAIL_DIR}/audit.log`;
/** The broker the substrate raised, reachable on the node's loopback (novox/hq ADR 0001). */
/** The broker the foundation raised, reachable on the node's loopback (novox/hq ADR 0001). */
const BROKER = "amqp://guest:guest@127.0.0.1:5672/";
let instanceId = "";
@@ -73,7 +73,7 @@ function quote(s: string): string {
* A command on the machine, its exit read from a marker on its own line.
*
* `exec 2>&1` on its own first line and the marker on its own last line, so a command that carries
* a heredoc — the substrate bundle is written with one — terminates where it says it does rather
* a heredoc — the foundation bundle is written with one — terminates where it says it does rather
* than swallowing the marker (the fault mesh.test.ts documents).
*/
async function on(command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> {
@@ -92,14 +92,14 @@ async function must(command: string, timeoutMs?: number): Promise<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
* is not this one; matching by repository and rewriting to the digest this registry assigned is
* what makes it applicable (the same rewrite mesh.test.ts does).
*/
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
/** Read the trail back as parsed JSON lines. */
@@ -120,14 +120,14 @@ before(async () => {
});
instanceId = raised.instanceId;
// The node raises its substrate — store, broker and the rest — from the bundle, applied from a
// The node raises its foundation — store, broker and the rest — from the bundle, applied from a
// file because the control plane's image is named by the ID this machine holds it under,
// which is not knowable until it has been handed over.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const running = await must(`docker ps --format '{{.Names}}'`);
assert.match(running, /mesh-broker/, `the substrate did not raise a broker:\n${running}`);
assert.match(running, /mesh-broker/, `the foundation did not raise a broker:\n${running}`);
// Bring the runtime+audit-logger image onto the machine. Loaded, not pulled: the machine has no
// route out (novox/hq the lab is a closed address space), so the image arrives as a tar the way
+11 -11
View File
@@ -31,9 +31,9 @@
* MESH_LAB_INCUS='sudo -n incus'
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap
* MESH_LAB_BUNDLE=.../mesh-host/examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
* MESH_LAB_CATALOG=.../mesh-catalog/modules
* MESH_LAB_SOURCE=<forge>/mesh-control.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_SOURCE=<forge>/mesh-controller.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_KEEP=1 to leave it standing afterwards
*/
import { test, before, after } from "node:test";
@@ -44,7 +44,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec, push } from "../../src/lifecycle/operate.ts";
import { bootstrapBinaryPath, hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
import { genesis, type GenesisResult } from "./genesis.ts";
const SCENARIO = "fresh-mesh";
@@ -68,7 +68,7 @@ const REGISTRY = `${ANCHOR}:5000`;
*/
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
/**
* What `amqp-ping` requires, and what the substrate does not supply.
* What `amqp-ping` requires, and what the foundation does not supply.
*
* The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a
* record of, so it provides nothing to anything. A module asking for `amqp` is asking for a
@@ -86,7 +86,7 @@ const PROVIDER = { module: "lavinmq", repo: "mesh-catalog", path: "modules/lavin
const STORE = { module: "postgres", repo: "mesh-catalog", path: "modules/postgres", container: "mesh-postgres" };
const CATALOGUE = { module: "mesh-catalog", repo: "mesh-catalog", path: "modules/mesh-catalog", container: "mesh-catalog" };
/** The control plane, rebuilt from its own repository — the step that ends the installer's tenure. */
const CONTROL_PLANE = { module: "mesh-control", repo: "mesh-control", path: "", container: "mesh-control" };
const CONTROL_PLANE = { module: "mesh-controller", repo: "mesh-controller", path: "", container: "mesh-controller" };
/** 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";
@@ -154,7 +154,7 @@ const skip =
!installer ? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap" :
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" :
!sourceRef ? "MESH_LAB_SOURCE_REF is not set to the commit to build" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a substrate template" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation template" :
!catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
false;
@@ -180,7 +180,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
return out;
}
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);
}
/** A module the mesh has to make for itself: where its source is, and what it runs when it works. */
@@ -239,7 +239,7 @@ async function waitForContainer(node: string, container: string, seconds = 200):
*
* **The control plane runs in a container, so a file on the machine is not a file it can open.**
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
* inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is
* inside mesh-controller, which is correct and was briefly mistaken for a missing manifest. It is
* copied the last step of the way with `docker cp`.
*
* **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp`
@@ -250,7 +250,7 @@ async function registerModule(module: string, manifest: string): Promise<string>
const onMachine = `/tmp/${module}.json`;
const inContainer = `/${module}.json`;
await push(instanceId, CONTROL, manifest, onMachine);
await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`);
await must(CONTROL, `docker cp ${onMachine} mesh-controller:${inContainer}`);
return mesh(`module add ${inContainer}`);
}
function tokenFrom(said: string): string {
@@ -325,7 +325,7 @@ before(async () => {
// whose anchor is somewhere else every node, including this one, would enrol against an
// address nothing answers on. The installer refuses to guess it and says so, which is
// right: it does not know what this machine is called from outside.
bundleTemplate: substrateBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
bundleTemplate: foundationBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
registry: REGISTRY,
source,
sourceRef,
@@ -371,7 +371,7 @@ before(async () => {
return built;
});
// A store of its own. **Not the substrate's.** The installer raises a store for the control
// A store of its own. **Not the foundation's.** The installer raises a store for the control
// plane to keep its own records in, the way it raises a broker — plumbing, not a module the mesh
// has any record of, so it provides nothing to anything. A module that wants a database wants a
// provider in the graph, and the catalogue below is the first thing to want one.
+5 -5
View File
@@ -12,7 +12,7 @@
* MESH_LAB_INCUS='sudo -n incus'
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap
* MESH_LAB_BUNDLE=.../mesh-host/examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
* MESH_LAB_CATALOG=.../mesh-catalog/modules
* MESH_LAB_KEEP=1 to leave it standing afterwards
*/
@@ -23,7 +23,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy } from "../../src/lifecycle/operate.ts";
import { bootstrapBinaryPath, hostBinaryPath } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
import { genesis, type GenesisResult } from "./genesis.ts";
const SCENARIO = "genesis-single";
@@ -49,7 +49,7 @@ const skip =
"bootstrap IMAGE=mesh-builder:development`)" :
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" :
!sourceRef ? "MESH_LAB_SOURCE_REF is not set to the commit to build" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a substrate template" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation template" :
!catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
false;
@@ -75,10 +75,10 @@ before(async () => {
installer: installer as string,
catalogDir,
// The template, not a bundle. The store and broker become the references mesh-catalog pins,
// and the machine pulls them over its uplink like any first node. mesh-control is left
// and the machine pulls them over its uplink like any first node. mesh-controller is left
// naming a registry that does not exist — the installer overwrites it, and leaving it proves
// that it does.
bundleTemplate: substrateBundle(bundle, []),
bundleTemplate: foundationBundle(bundle, []),
source,
sourceRef,
// Phase two builds from the same forge; the repositories sit beside the control plane's.
+20 -20
View File
@@ -37,7 +37,7 @@ export interface GenesisOptions {
/** A checkout of mesh-catalog's `modules/` on this workstation. */
catalogDir: string;
/**
* The substrate TEMPLATE's content — not a bundle. The installer produces the bundle from it,
* The foundation TEMPLATE's content — not a bundle. The installer produces the bundle from it,
* replacing the control plane's image with the id of the image it carries.
*/
bundleTemplate: string;
@@ -91,7 +91,7 @@ export function stepIn(said: string): string {
export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
const node = o.node;
const registry = o.registry ?? "127.0.0.1:5000";
const modules = o.catalogueModules ?? ["distribution", "mesh-control", "builder"];
const modules = o.catalogueModules ?? ["distribution", "mesh-controller", "builder"];
const catalogueOnMachine = o.catalogueOnMachine ?? "/opt/mesh-catalog";
const log = o.log ?? (() => {});
@@ -147,9 +147,9 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
}
report.push(` catalogue full checkout at ${catalogueOnMachine} (${modules.join(", ")} + phase two)`);
const local = join(tmpdir(), `mesh-lab-substrate-${process.pid}-${node}.lock`);
const local = join(tmpdir(), `mesh-lab-foundation-${process.pid}-${node}.lock`);
writeFileSync(local, o.bundleTemplate);
await push(o.instanceId, node, local, "/tmp/substrate-template.lock");
await push(o.instanceId, node, local, "/tmp/foundation-template.lock");
// Supervise the host as a service (the real install path) when asked — the only way it survives a
// reboot. Installs the shipped packaging in the machine, then lets --host-service start+enable it.
@@ -165,7 +165,7 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
BOOTSTRAP_PATH,
`--source ${o.source}`,
`--source-ref ${o.sourceRef}`,
`--bundle /tmp/substrate-template.lock`,
`--bundle /tmp/foundation-template.lock`,
`--catalog ${catalogueOnMachine}`,
`--node ${node}`,
`--registry ${registry}`,
@@ -212,9 +212,9 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
// ------------------------------------------------------------------------------------------
// 1. The control plane answers, asked of the PERMANENT container by name.
const answered = await on(`docker exec mesh-control /mesh-control status`, 60_000);
const answered = await on(`docker exec mesh-controller /mesh-controller status`, 60_000);
report.push(` control plane ${answered.ok ? answered.out.split("\n")[0] : "NO ANSWER"}`);
if (!answered.ok) return stop("after the last step", `mesh-control does not answer:\n${answered.out}`);
if (!answered.ok) return stop("after the last step", `mesh-controller does not answer:\n${answered.out}`);
// 2. The registry replies on /v2/. A container that is up is not a registry that serves.
const v2 = await on(`curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://${registry}/v2/`);
@@ -224,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
// assigned, not by an image id (ADR 0067 states this check in as many words).
const pinnedTo = (await on(`docker inspect --format '{{.Config.Image}}' mesh-control`)).out.trim();
const pinnedTo = (await on(`docker inspect --format '{{.Config.Image}}' mesh-controller`)).out.trim();
report.push(` pinned to ${pinnedTo || "(nothing)"}`);
if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) {
return stop("after the last step",
`mesh-control is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` +
`mesh-controller is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` +
`own configuration, which no registry ever served. The pivot did not happen, so this mesh ` +
`cannot upgrade itself (novox/hq ADR 0067, "the pivot completed").`);
}
if (!new RegExp(`^${registry.replaceAll(".", "\\.")}/mesh-control@sha256:[0-9a-f]{64}$`).test(pinnedTo)) {
if (!new RegExp(`^${registry.replaceAll(".", "\\.")}/mesh-controller@sha256:[0-9a-f]{64}$`).test(pinnedTo)) {
return stop("after the last step",
`mesh-control is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` +
`mesh-controller is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` +
`digest assigned by ${registry}.`);
}
@@ -249,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
// nobody asked for.
const wanted = o.sourceRef.slice(0, 8);
if (!new RegExp(`built mesh-control from ${wanted}`).test(said)) {
if (!new RegExp(`built mesh-controller from ${wanted}`).test(said)) {
return stop("after the last step",
`the installer never said it built mesh-control from ${wanted}. What runs may have been ` +
`the installer never said it built mesh-controller from ${wanted}. What runs may have been ` +
`carried rather than made here, which is a mesh that cannot rebuild its own control plane. ` +
`The installer said:\n${said.split("\n").filter((l) => /built|build/.test(l)).join("\n") || "(nothing about building)"}`);
}
report.push(` built here mesh-control from ${wanted}, by the carried builder`);
report.push(` built here mesh-controller from ${wanted}, by the carried builder`);
// 3b. And the registry really serves it. A reference is a claim; a tag list is the registry agreeing.
const tags = await on(`curl -s --max-time 10 http://${registry}/v2/mesh-control/tags/list`);
const tags = await on(`curl -s --max-time 10 http://${registry}/v2/mesh-controller/tags/list`);
report.push(` registry holds ${tags.out.trim() || "nothing"}`);
if (!tags.out.includes("genesis")) {
return stop("after the last step",
`${registry} does not serve mesh-control, so the digest the container is pinned to names an ` +
`${registry} does not serve mesh-controller, so the digest the container is pinned to names an ` +
`image nothing can pull: ${tags.out.trim()}`);
}
// 4. The temporary control plane is GONE. The name is the audit.
const temp = await on(`docker inspect --format '{{.State.Status}}' temp-mesh-control`);
report.push(` temp-mesh-control ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`);
const temp = await on(`docker inspect --format '{{.State.Status}}' temp-mesh-controller`);
report.push(` temp-mesh-controller ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`);
if (temp.ok) {
return stop("after the last step",
`temp-mesh-control is still ${temp.out.trim()}. Two control planes are consuming this mesh's ` +
`temp-mesh-controller is still ${temp.out.trim()}. Two control planes are consuming this mesh's ` +
`broker queues; neither is wrong and the pivot is not finished.`);
}
// 5. And the mesh has heard from its one node.
const nodes = await on(`docker exec mesh-control /mesh-control node list`);
const nodes = await on(`docker exec mesh-controller /mesh-controller node list`);
report.push(` node list ${nodes.out.trim().split("\n").join(" | ")}`);
const line = nodes.out.split("\n").map((l) => l.trim()).find((l) => l.startsWith(`${node} `));
if (!line || !/^\S+\s+here\b/.test(line)) {
+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 { mustBeHandedOver, pinnedInto, referenceFor, repositoryOf, type HeldImage } from "../../src/pinning.ts";
// --- the substrate bundle, and what its three images are on a real machine ---------------------
// --- the foundation bundle, and what its three images are on a real machine ---------------------
/**
* The example bundle in mesh-host names a registry that no longer exists.
*
* `examples/substrate-first-node.lock` was written **for a target**, and the target was the lab: it
* `examples/foundation-first-node.lock` was written **for a target**, and the target was the lab: it
* pins `192.0.2.250:5000/…` because that is where the registry the lab used to raise served from.
* That registry is gone, so those three references name nothing.
*
* Two of them are ordinary third-party images and belong to the internet. Rather than invent
* digests here, they are the ones the mesh's own modules already pin — mesh-catalog's `postgres`
* and `lavinmq` — so the substrate's store and broker are literally the images the mesh runs. The
* third, mesh-control, exists in no registry at all and becomes the ID the machine holds it under.
* and `lavinmq` — so the foundation's store and broker are literally the images the mesh runs. The
* third, mesh-controller, exists in no registry at all and becomes the ID the machine holds it under.
*
* **The bundle itself should be fixed in mesh-host**, and this substitution deleted with it. It is
* here because the file lives in another repository and because a fixture that lies about where an
@@ -41,13 +41,13 @@ const UPSTREAM_BROKER =
"cloudamqp/lavinmq@sha256:3eb54c12916d700a978c2ea86e6362cd4974b0e3189508718006d4e6d341246b";
/**
* The substrate bundle as a machine should receive it.
* The foundation bundle as a machine should receive it.
*
* Third-party references become upstream ones, which the machine pulls over its uplink; ours
* become the ID the machine was handed. Nothing points inside the scenario any more, which is the
* whole of this change: what the bed proves about a bootstrap is now what would happen anywhere.
*/
export function substrateBundle(path: string, held: HeldImage[]): string {
export function foundationBundle(path: string, held: HeldImage[]): string {
let text = readFileSync(path, "utf8");
text = text.replaceAll(/[A-Za-z0-9_.:-]+\/postgres@sha256:[0-9a-f]{64}/g, UPSTREAM_STORE);
text = text.replaceAll(
+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.
*
* lavinmq is the mesh's control-plane broker AND a user-facing capability: a consumer that requires
* `amqp` is given a scoped vhost + user on a lavinmq PROVIDER, not an account on the control broker
* (novox/hq ADR 0048). That makes it the two-node case, the twin of two-node-db: the substrate's
* (novox/hq ADR 0048). That makes it the two-node case, the twin of two-node-db: the foundation's
* broker publishes 5672 on anchor, and a lavinmq provider must publish 5672 for its consumers to
* reach it — so the two cannot share a machine. `anchor` runs the substrate and nothing else; `laptop`
* reach it — so the two cannot share a machine. `anchor` runs the foundation and nothing else; `laptop`
* runs the provider AND the amqp-ping consumer, co-located, and owns laptop's 5672 uncontested.
*
* The proof is layered:
@@ -14,7 +14,7 @@
* config BEFORE the broker started (ADR 0052) — proven because the broker came up at all and is
* gone from `docker ps -a` (a step, not a service);
* - the lavinmq service and BOTH runtimes (bootstrap done, provisioner running) are up and stable;
* - each module got its own scoped broker account on the substrate broker (anchor), named for the
* - each module got its own scoped broker account on the foundation broker (anchor), named for the
* node that runs it;
* - the provisioner (in mesh-lavinmq on laptop) created the consumer's vhost AND user, both named
* for the login the mesh derived — checked with `lavinmqctl` inside the broker;
@@ -25,12 +25,12 @@
* its vhost — the provider named the vhost after the login — and nothing is hardcoded; the provider's
* `serves` carries the port so the consumer references `${bound:amqp:port}`.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
*
* HELPER — stock the two runtimes into the local daemon before the run (some may already be there):
* scripts/build-module-runtime.sh lavinmq /tmp/lavinmq.tar
* scripts/build-module-runtime.sh amqp-ping /tmp/amqp-ping.tar
* cloudamqp/lavinmq:latest must be in the local daemon too (it is the substrate's own broker image);
* cloudamqp/lavinmq:latest must be in the local daemon too (it is the foundation's own broker image);
* scenarios/lavinmq-bed.yml stocks all of them, and each node pulls what it runs by digest.
*/
@@ -41,7 +41,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -53,11 +53,11 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "lavinmq-bed";
/** The node that carries the provider and its consumer. anchor carries only the substrate. */
/** The node that carries the provider and its consumer. anchor carries only the foundation. */
const NODE = "laptop";
/** The login the mesh derives for the consumer: mesh_<node>_<slug> (slug "ping"; ADR 0049). */
const CONSUMER_LOGIN = "mesh_laptop_ping";
@@ -86,7 +86,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
/** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<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. */
@@ -95,9 +95,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -118,7 +118,7 @@ async function settled(node: string, withinMs = 1_200_000): Promise<void> {
} | undefined;
let said = "";
try {
const asked = await on("anchor", `docker exec mesh-control /mesh-control status --json`);
const asked = await on("anchor", `docker exec mesh-controller /mesh-controller status --json`);
said = asked.out;
if (asked.ok) state = JSON.parse(said);
} catch (err) {
@@ -157,11 +157,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) {
@@ -178,7 +178,7 @@ after(async () => {
await destroyAll(`${SCENARIO}-`);
}, { timeout: 600_000 });
test("the lavinmq provider and its consumer ride laptop while the substrate broker owns 5672 on anchor", {
test("the lavinmq provider and its consumer ride laptop while the foundation broker owns 5672 on anchor", {
skip, timeout: 1_500_000,
}, async () => {
// ================================================================================================
@@ -287,7 +287,7 @@ test("the lavinmq provider and its consumer ride laptop while the substrate brok
// --- add, issue a scoped broker account, assign to laptop --------------------------------------
async function addIssueAssign(name: string, manifest: string): Promise<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`);
const issued = await mesh(`module issue ${name} --node ${NODE}`);
assert.match(issued, /broker account/, issued);
@@ -311,14 +311,14 @@ test("the lavinmq provider and its consumer ride laptop while the substrate brok
await settled(NODE);
// ================================================================================================
// THE two-node split — the substrate broker owns 5672 on anchor, the provider owns it on laptop.
// THE two-node split — the foundation broker owns 5672 on anchor, the provider owns it on laptop.
// ================================================================================================
const onAnchor = await must("anchor", `docker ps --format '{{.Names}}'`);
const onLaptop = await must(NODE, `docker ps --format '{{.Names}}'`);
assert.match(onAnchor, /(^|\n)mesh-broker(\n|$)/, "the substrate broker is not on the first node");
assert.match(onAnchor, /(^|\n)mesh-broker(\n|$)/, "the foundation broker is not on the first node");
assert.doesNotMatch(onAnchor, /(^|\n)lavinmq(\n|$)/,
"the lavinmq provider landed on the substrate node — the 5672 collision this bed exists to avoid");
assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the substrate store leaked onto the second node");
"the lavinmq provider landed on the foundation node — the 5672 collision this bed exists to avoid");
assert.doesNotMatch(onLaptop, /(^|\n)mesh-store(\n|$)/, "the foundation store leaked onto the second node");
assert.match(onLaptop, /(^|\n)lavinmq(\n|$)/, "the lavinmq provider is not on the second node");
// ================================================================================================
@@ -359,7 +359,7 @@ test("the lavinmq provider and its consumer ride laptop while the substrate brok
}
// ================================================================================================
// Each module got its own scoped broker account on the substrate broker (anchor), named for the
// Each module got its own scoped broker account on the foundation broker (anchor), named for the
// node that runs it and the module.
// ================================================================================================
const users = await must("anchor", `docker exec mesh-broker lavinmqctl list_users 2>&1`);
+11 -11
View File
@@ -15,7 +15,7 @@
* asserts the templated URL and that a request to it reaches the running server (ollama answers
* /v1/models even with no model pulled — the wiring is what is proven, not a model's output).
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* No module runtime image is built — both modules are pure declaration.
*/
@@ -26,7 +26,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -38,7 +38,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "local-model-bed";
@@ -67,11 +67,11 @@ async function must(command: string, timeoutMs?: number): Promise<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 }> {
return on(`docker exec mesh-control /mesh-control ${command}`);
return on(`docker exec mesh-controller /mesh-controller ${command}`);
}
/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */
@@ -80,7 +80,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -117,7 +117,7 @@ async function settled(withinMs = 600_000): Promise<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 issue ${name} --node ${MACHINE}`);
await mesh(`assign ${MACHINE} ${name}`);
@@ -132,11 +132,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
+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
* test, is the entire provider/consumer contract working as one thing.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which
* scenarios/redis-node.yml stocks.
*/
@@ -23,7 +23,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -35,7 +35,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "redis-node";
@@ -64,7 +64,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -73,7 +73,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -86,7 +86,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -118,11 +118,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -201,12 +201,12 @@ test("the mesh grants a consumer redis's cache, and the credential it delivers a
resources: [{ id: "state", type: "directory", path: "/var/lib/cacheuser", mode: "0700" }],
});
await must(`printf %s ${quote(redisManifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`);
await must(`printf %s ${quote(redisManifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`);
await mesh("module add /redis.json");
await mesh(`module issue redis --node ${MACHINE}`);
await mesh(`assign ${MACHINE} redis`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/cacheuser.json && docker cp /tmp/cacheuser.json mesh-control:/cacheuser.json`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/cacheuser.json && docker cp /tmp/cacheuser.json mesh-controller:/cacheuser.json`);
await mesh("module add /cacheuser.json");
await mesh(`assign ${MACHINE} cacheuser`);
+47 -47
View File
@@ -6,10 +6,10 @@
* project keeps saying cannot be checked any other way (novox/hq ADR 0001: every fault of
* 2026-08-22 was found in production because nothing could be stood up locally).
*
* It needs a host binary and the substrate bundle:
* It needs a host binary and the foundation bundle:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
*
* The bundle's image references are rewritten to the ones this scenario's own registry serves.
* A digest belongs to whatever registry serves it, so a committed bundle names a registry that is
@@ -24,7 +24,7 @@ import { raise } from "../../src/lifecycle/raise.ts";
import { pinnedInto, stillUnpinned, type HeldImage } from "../../src/pinning.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
import { ready, returnTo, keep, rememberStock, warmStock } from "../../src/warm.ts";
@@ -36,7 +36,7 @@ const capability = await labIsUsable();
const binary = hostBinaryPath();
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
const builder = process.env["MESH_LAB_BUILDER"] ?? "";
/** mesh-control's `examples/modules`, so the manifests proven here are the ones that ship. */
/** mesh-controller's `examples/modules`, so the manifests proven here are the ones that ship. */
const moduleExamples = process.env["MESH_LAB_MODULES"] ?? "";
const skip = !capability.usable
@@ -44,7 +44,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "two-nodes";
@@ -105,7 +105,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
/** The control plane, which runs in a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<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 = "";
try {
const asked = await on("anchor",
`docker exec mesh-control /mesh-control status --json`);
`docker exec mesh-controller /mesh-controller status --json`);
said = asked.out;
// Parsed inside the try on purpose: a truncated answer from a struggling machine is the
// same fact as no answer, and the likeliest moment for one is exactly the machine this
@@ -186,11 +186,11 @@ async function settled(node: string, withinMs = 480_000): Promise<void> {
* The bundle, as a machine should receive it.
*
* The committed example was written for a target that had a registry the lab raised. Its two
* third-party images become upstream references the machine pulls itself; mesh-control, which
* third-party images become upstream references the machine pulls itself; mesh-controller, which
* exists in no registry, becomes the ID this machine was handed.
*/
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
/** Take a token out of what `token issue` printed. It is the one base64url blob on its own line. */
@@ -253,8 +253,8 @@ before(async () => {
// because the control plane's image is named by the ID this machine holds it under, which is not
// knowable until it has been handed over.
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`);
await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`);
// A build machine, so anything here can ask the mesh to build something. Placed rather than
// assumed: nothing else in this scenario would start one.
@@ -289,7 +289,7 @@ after(async () => {
test("a bare machine becomes a mesh", { skip, timeout: 600_000 }, async () => {
const running = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const container of ["mesh-store", "mesh-broker", "mesh-control"]) {
for (const container of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(running, new RegExp(container), `${container} is not running`);
}
// Answering, not merely up. A container that is running is not a control plane that replies —
@@ -334,8 +334,8 @@ test("a credential reaches both ends and the mesh holds neither", { skip, timeou
`"content":"PGHOST=$\{bound:postgres-database:at\}\\nPGPORT=$\{bound:postgres-database:port\}\\n` +
`PGUSER=$\{bound:postgres-database:as\}\\nPGPASSWORD=$\{secret:postgres-database\}\\n"}]}' ` +
`> /tmp/app.json`);
await must("anchor", `docker cp /tmp/pg.json mesh-control:/pg.json`);
await must("anchor", `docker cp /tmp/app.json mesh-control:/app.json`);
await must("anchor", `docker cp /tmp/pg.json mesh-controller:/pg.json`);
await must("anchor", `docker cp /tmp/app.json mesh-controller:/app.json`);
await mesh("module add /pg.json");
await mesh("module add /app.json");
@@ -423,7 +423,7 @@ test("when a machine cannot do what it was told, the mesh says which and why", {
// is the situation `status` exists to distinguish from a machine that refused everything.
await must("anchor", `printf %s '{"module":"impossible","version":"1","resources":[` +
`{"id":"nothing","type":"package","package":"a-package-that-does-not-exist"}]}' > /tmp/imp.json`);
await must("anchor", `docker cp /tmp/imp.json mesh-control:/imp.json`);
await must("anchor", `docker cp /tmp/imp.json mesh-controller:/imp.json`);
await mesh("module add /imp.json");
await mesh("assign laptop impossible");
await mesh("push laptop");
@@ -471,7 +471,7 @@ test("a declaration waits for a machine that is switched off", { skip, timeout:
await must("anchor", `printf %s '{"module":"while-away","version":"1","resources":[` +
`{"id":"note","type":"file","path":"/etc/mesh-while-away","content":"waited"}]}' > /tmp/away.json`);
await must("anchor", `docker cp /tmp/away.json mesh-control:/away.json`);
await must("anchor", `docker cp /tmp/away.json mesh-controller:/away.json`);
await mesh("module add /away.json");
await mesh("assign laptop while-away");
await mesh("push laptop");
@@ -512,13 +512,13 @@ test("a declaration waits for a machine that is switched off", { skip, timeout:
test("unassigning takes away exactly what it should", { skip, timeout: 900_000 }, async () => {
// Removal is the half nobody tests. The mesh takes away what IT declared and no longer declares,
// and never what the machine raised for itself from its bundle — which is the fault that
// destroyed a substrate once (novox/hq 04-ISSUES/010).
// destroyed a foundation once (novox/hq 04-ISSUES/010).
//
// Two modules, so the test can tell "removed the right one" from "removed everything".
for (const [name, path] of [["kept", "/etc/mesh-kept"], ["going", "/etc/mesh-going"]] as const) {
await must("anchor", `printf %s '{"module":"${name}","version":"1","resources":[` +
`{"id":"note","type":"file","path":"${path}","content":"${name}"}]}' > /tmp/${name}.json`);
await must("anchor", `docker cp /tmp/${name}.json mesh-control:/${name}.json`);
await must("anchor", `docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
await mesh(`assign anchor ${name}`);
}
@@ -536,11 +536,11 @@ test("unassigning takes away exactly what it should", { skip, timeout: 900_000 }
assert.ok((await on("anchor", `test -f /etc/mesh-kept`)).ok,
"unassigning one module took another one's file with it");
// And the substrate this machine raised from its own bundle is untouched. It was not declared by
// And the foundation this machine raised from its own bundle is untouched. It was not declared by
// the mesh, so the mesh must never remove it — the machine would take its own control plane
// away, which is exactly what happened before origins existed.
const running = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const container of ["mesh-store", "mesh-broker", "mesh-control"]) {
for (const container of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(running, new RegExp(container),
`${container} was removed by a declaration that never declared it`);
}
@@ -565,7 +565,7 @@ test("a machine that fell behind catches up without being named", { skip, timeou
await must("anchor", `printf %s '{"module":"fixable","version":"1","resources":[` +
`{"id":"pkg","type":"package","package":"a-package-that-does-not-exist-yet"},` +
`{"id":"note","type":"file","path":"/etc/mesh-fixable","content":"here"}]}' > /tmp/fix.json`);
await must("anchor", `docker cp /tmp/fix.json mesh-control:/fix.json`);
await must("anchor", `docker cp /tmp/fix.json mesh-controller:/fix.json`);
await mesh("module add /fix.json");
await mesh("assign laptop fixable");
await mesh("push laptop");
@@ -585,7 +585,7 @@ test("a machine that fell behind catches up without being named", { skip, timeou
// Fix the cause, the way somebody would: the module stops asking for the impossible thing.
await must("anchor", `printf %s '{"module":"fixable","version":"1","resources":[` +
`{"id":"note","type":"file","path":"/etc/mesh-fixable","content":"here"}]}' > /tmp/fix.json`);
await must("anchor", `docker cp /tmp/fix.json mesh-control:/fix.json`);
await must("anchor", `docker cp /tmp/fix.json mesh-controller:/fix.json`);
await mesh("module add /fix.json");
// And nobody names the machine.
@@ -626,7 +626,7 @@ test("the mesh runs its own artifact store", { skip, timeout: 900_000 }, async (
// and a builder will not start without an artifact store to publish to, so a mesh that has just
// bootstrapped cannot build the module that gives it one. Adding the manifest directly is the
// path a real first mesh has to take, so it is the path this walks.
await must("anchor", `docker cp /root/registry/module.json mesh-control:/registry.json`);
await must("anchor", `docker cp /root/registry/module.json mesh-controller:/registry.json`);
await mesh("module add /registry.json");
await mesh("assign anchor registry");
await mesh("push anchor");
@@ -658,7 +658,7 @@ test("a machine serves its internal name with a certificate the mesh issued", {
`"certificate":{"into":"/etc/mesh/serving.crt","authority":"/etc/mesh/authority.crt"},` +
`"resources":[{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"}]}' ` +
`> /tmp/served.json`);
await must("anchor", `docker cp /tmp/served.json mesh-control:/served.json`);
await must("anchor", `docker cp /tmp/served.json mesh-controller:/served.json`);
await mesh("module add /served.json");
await mesh("assign anchor served");
await mesh("push anchor");
@@ -768,7 +768,7 @@ test("a machine filters exactly what its modules declared, and nothing else", {
`{"id":"filter","type":"service","unit":"mesh-filter.service","state":"running",` +
`"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`);
for (const f of ["talker", "firewall"]) {
await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`);
await must("anchor", `docker cp /tmp/${f}.json mesh-controller:/${f}.json`);
await mesh(`module add /${f}.json`);
}
await mesh("assign laptop talker");
@@ -972,7 +972,7 @@ test("rotating a credential moves both ends, and the old one stops working", {
`"resources":[{"id":"dir","type":"directory","path":"/etc/realapp","mode":"0755"}]}' ` +
`> /tmp/realapp.json`);
for (const f of ["realstore", "realapp"]) {
await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`);
await must("anchor", `docker cp /tmp/${f}.json mesh-controller:/${f}.json`);
await mesh(`module add /${f}.json`);
}
await mesh("assign anchor realstore");
@@ -1081,7 +1081,7 @@ test("a route is a grant: a workload is reached by the name it asked for", {
`{"id":"app","type":"container","name":"storefront",` +
`"image":"${ARTIFACT_STORE}","ports":["8088:5000"]}]}' > /tmp/storefront.json`);
for (const f of ["frontdoor", "storefront"]) {
await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`);
await must("anchor", `docker cp /tmp/${f}.json mesh-controller:/${f}.json`);
await mesh(`module add /${f}.json`);
}
await mesh("assign anchor frontdoor");
@@ -1135,7 +1135,7 @@ test("a route is a grant: a workload is reached by the name it asked for", {
assert.ok(withdrawn,
`the route outlived the module that asked for it:\n${after}\n\n` +
`the machine did apply — this is what the mesh would send now:\n` +
`${(await on("anchor", `docker exec mesh-control /mesh-control plan anchor --files`)).out}`);
`${(await on("anchor", `docker exec mesh-controller /mesh-controller plan anchor --files`)).out}`);
let gone = false;
for (let i = 0; i < 15 && !gone; i++) {
@@ -1159,7 +1159,7 @@ test("model access is answered by a record, and the key the mesh took is one it
`"secrets":{"model-access":"/etc/assistant/key"},` +
`"resources":[{"id":"dir","type":"directory","path":"/etc/assistant","mode":"0755"}]}' ` +
`> /tmp/assistant.json`);
await must("anchor", `docker cp /tmp/assistant.json mesh-control:/assistant.json`);
await must("anchor", `docker cp /tmp/assistant.json mesh-controller:/assistant.json`);
await mesh("module add /assistant.json");
// The licences first. With none recorded at all the honest answer is that nothing provides
@@ -1171,7 +1171,7 @@ test("model access is answered by a record, and the key the mesh took is one it
// then says the machine's set cannot be applied. The refusal names both candidates and the
// command. ADR 0024 warns this will be felt — which is correct, and correct is not the same as
// usable.
const refused = await on("anchor", `docker exec mesh-control /mesh-control assign laptop assistant`);
const refused = await on("anchor", `docker exec mesh-controller /mesh-controller assign laptop assistant`);
assert.ok(!refused.ok,
`a consumer was given model access without anybody saying which:\n${refused.out}`);
for (const want of ["personal", "the-organisation", "licence use"]) {
@@ -1182,7 +1182,7 @@ test("model access is answered by a record, and the key the mesh took is one it
await mesh("licence use personal laptop assistant");
// Chosen, and still no key: the mesh has one thing to deliver and has not been given it.
const noKey = await on("anchor", `docker exec mesh-control /mesh-control plan laptop`);
const noKey = await on("anchor", `docker exec mesh-controller /mesh-controller plan laptop`);
assert.ok(!noKey.ok, `a module was planned with a licence that has no key:\n${noKey.out}`);
assert.match(noKey.out, /licence key personal/, noKey.out);
@@ -1190,7 +1190,7 @@ test("model access is answered by a record, and the key the mesh took is one it
// command line is a key in shell history and in every process listing taken while it ran.
const secret = "sk-test-" + "0123456789abcdef".repeat(2);
const accepted = await must("anchor",
`printf %s ${quote(secret)} | docker exec -i mesh-control /mesh-control licence key personal`);
`printf %s ${quote(secret)} | docker exec -i mesh-controller /mesh-controller licence key personal`);
assert.match(accepted, /sealed to 1 holder/, accepted);
assert.doesNotMatch(accepted, new RegExp(secret),
"the key was echoed back, so the one copy that matters is on a terminal");
@@ -1298,11 +1298,11 @@ test("the board names the machine that is not doing what it was told", {
await must("anchor", `printf %s '{"module":"board","version":"1",` +
`"listens":[{"port":8090,"from":"mesh","why":"the board"}],` +
`"resources":[]}' > /tmp/board.json`);
await must("anchor", `docker cp /tmp/board.json mesh-control:/board.json`);
await must("anchor", `docker cp /tmp/board.json mesh-controller:/board.json`);
await mesh("module add /board.json");
// Served from the control plane's own container, reading the mesh on every request.
await must("anchor", `docker exec -d mesh-control /mesh-control board --listen 0.0.0.0:8090`);
await must("anchor", `docker exec -d mesh-controller /mesh-controller board --listen 0.0.0.0:8090`);
await new Promise((r) => setTimeout(r, 3000));
const read = async (path: string) =>
@@ -1325,7 +1325,7 @@ test("the board names the machine that is not doing what it was told", {
await must("anchor", `printf %s '{"module":"impossible","version":"1",` +
`"resources":[{"id":"nowhere","type":"service","unit":"nothing-like-this.service",` +
`"state":"running"}]}' > /tmp/impossible.json`);
await must("anchor", `docker cp /tmp/impossible.json mesh-control:/impossible.json`);
await must("anchor", `docker cp /tmp/impossible.json mesh-controller:/impossible.json`);
await mesh("module add /impossible.json");
await mesh("assign laptop impossible");
await mesh("push laptop");
@@ -1389,7 +1389,7 @@ test("the hub can be filtered without severing the mesh", {
`ExecStart=/usr/bin/nft -f ${rules}\\n[Install]\\nWantedBy=multi-user.target\\n"},` +
`{"id":"filter","type":"service","unit":"hub-filter.service","state":"running",` +
`"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/hubfilter.json`);
await must("anchor", `docker cp /tmp/hubfilter.json mesh-control:/hubfilter.json`);
await must("anchor", `docker cp /tmp/hubfilter.json mesh-controller:/hubfilter.json`);
await mesh("module add /hubfilter.json");
await mesh("assign anchor hubfilter");
await mesh("push anchor");
@@ -1414,7 +1414,7 @@ test("the hub can be filtered without severing the mesh", {
await must("anchor", `printf %s '{"module":"stillworks","version":"1",` +
`"resources":[{"id":"marker","type":"file","path":"/etc/mesh-still-works",` +
`"content":"yes","mode":"0644"}]}' > /tmp/stillworks.json`);
await must("anchor", `docker cp /tmp/stillworks.json mesh-control:/stillworks.json`);
await must("anchor", `docker cp /tmp/stillworks.json mesh-controller:/stillworks.json`);
await mesh("module add /stillworks.json");
await mesh("assign laptop stillworks");
await mesh("push laptop");
@@ -1450,7 +1450,7 @@ test("a container reaches another machine by the name the mesh gave it", {
`"capabilities":["container-runtime"],` +
`"resources":[{"id":"idle","type":"container","name":"resolves",` +
`"image":"${ARTIFACT_STORE}"}]}' > /tmp/resolves.json`);
await must("anchor", `docker cp /tmp/resolves.json mesh-control:/resolves.json`);
await must("anchor", `docker cp /tmp/resolves.json mesh-controller:/resolves.json`);
await mesh("module add /resolves.json");
await mesh("assign laptop resolves");
await mesh("push laptop");
@@ -1539,7 +1539,7 @@ test("every name under a machine resolves to that machine", {
});
test("a service is reached by a name under the machine it runs on", {
skip: skip || (!moduleExamples ? "set MESH_LAB_MODULES to mesh-control's examples/modules" : false),
skip: skip || (!moduleExamples ? "set MESH_LAB_MODULES to mesh-controller's examples/modules" : false),
timeout: 900_000,
}, async () => {
// postgres.novox.internal, plex.ace.internal — the first label is the service and the rest is
@@ -1555,7 +1555,7 @@ test("a service is reached by a name under the machine it runs on", {
for (const name of ["dnsmasq", "resolved-split-dns"]) {
const manifest = readFileSync(`${moduleExamples}/${name}.json`, "utf8");
await must("anchor", `cat > /tmp/${name}.json <<'MANIFEST'\n${manifest}\nMANIFEST`);
await must("anchor", `docker cp /tmp/${name}.json mesh-control:/${name}.json`);
await must("anchor", `docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
}
@@ -1700,18 +1700,18 @@ test("a third-party workload is adopted, with the credential it already had", {
},
],
}))} > /umami.json`);
await must("anchor", `docker cp /umami.json mesh-control:/umami.json`);
await must("anchor", `docker cp /umami.json mesh-controller:/umami.json`);
await mesh("module add /umami.json");
// **Accepted, not generated.** The value is what the database already answers to; the mesh
// seals it and cannot read it again. Given whole, as the environment lines the containers read.
await must("anchor",
`printf %s ${quote(`POSTGRES_PASSWORD=${password}`)} | ` +
`docker exec -i mesh-control /mesh-control secret accept anchor umami database --from -`);
`docker exec -i mesh-controller /mesh-controller secret accept anchor umami database --from -`);
await must("anchor",
`printf %s ${quote(
`DATABASE_URL=postgresql://umami:${password}@umami-db:5432/umami`)} | ` +
`docker exec -i mesh-control /mesh-control secret accept anchor umami app --from -`);
`docker exec -i mesh-controller /mesh-controller secret accept anchor umami app --from -`);
await mesh("assign anchor umami");
await mesh("push anchor", 300_000);
@@ -1815,7 +1815,7 @@ test("the real modules resolve together, and compose a declaration a host accept
}
planned.push(name);
await must("anchor", `printf %s ${quote(pinned)} > /${name}.json`);
await must("anchor", `docker cp /${name}.json mesh-control:/${name}.json`);
await must("anchor", `docker cp /${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
}
@@ -1932,7 +1932,7 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000
assert.deepEqual(stillUnpinned(pinned), [],
`${name} still names an image nothing serves, so it could not start`);
await must("anchor", `printf %s ${quote(pinned)} > /run-${name}.json`);
await must("anchor", `docker cp /run-${name}.json mesh-control:/run-${name}.json`);
await must("anchor", `docker cp /run-${name}.json mesh-controller:/run-${name}.json`);
await mesh(`module add /run-${name}.json`);
await mesh(`assign anchor ${name}`);
}
@@ -2018,7 +2018,7 @@ test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600
assert.deepEqual(stillUnpinned(pinned), [],
"redis still names an image nothing serves, so it could not start");
await must("anchor", `printf %s ${quote(pinned)} > /run-redis.json`);
await must("anchor", `docker cp /run-redis.json mesh-control:/run-redis.json`);
await must("anchor", `docker cp /run-redis.json mesh-controller:/run-redis.json`);
await mesh("module add /run-redis.json");
// A consumer with no container: what is under test is the credential's reach, and files on the
@@ -2030,7 +2030,7 @@ test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600
`"secrets":{"redis-cache":"/var/lib/cachetest/cache.secret"},` +
`"resources":[{"id":"state","type":"directory","path":"/var/lib/cachetest","mode":"0700"}]}' ` +
`> /cachetest.json`);
await must("anchor", `docker cp /cachetest.json mesh-control:/cachetest.json`);
await must("anchor", `docker cp /cachetest.json mesh-controller:/cachetest.json`);
await mesh("module add /cachetest.json");
await mesh("assign anchor redis");
+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
* placed by the test.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh minio builds mesh-runtime-minio:development (with mc), which
* scenarios/minio-node.yml stocks. minio/minio:latest must be in the local daemon.
*/
@@ -21,7 +21,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "minio-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -74,7 +74,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -93,7 +93,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -125,11 +125,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -203,12 +203,12 @@ test("the mesh grants a consumer an S3 bucket, and the credential it delivers re
resources: [{ id: "state", type: "directory", path: "/var/lib/bucketuser", mode: "0700" }],
});
await must(`printf %s ${quote(minioManifest)} > /tmp/minio.json && docker cp /tmp/minio.json mesh-control:/minio.json`);
await must(`printf %s ${quote(minioManifest)} > /tmp/minio.json && docker cp /tmp/minio.json mesh-controller:/minio.json`);
await mesh("module add /minio.json");
await mesh(`module issue minio --node ${MACHINE}`);
await mesh(`assign ${MACHINE} minio`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/bucketuser.json && docker cp /tmp/bucketuser.json mesh-control:/bucketuser.json`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/bucketuser.json && docker cp /tmp/bucketuser.json mesh-controller:/bucketuser.json`);
await mesh("module add /bucketuser.json");
await mesh(`assign ${MACHINE} bucketuser`);
+1 -1
View File
@@ -33,7 +33,7 @@ const skip = !capability.usable
? `lab not usable: ${capability.why}`
: !provisioner
? "set MESH_LAB_OBJECTSTORE_PROVISIONER to a built provisioner " +
"(mesh-control: go build ./examples/objectstore-provisioner)"
"(mesh-controller: go build ./examples/objectstore-provisioner)"
: false;
const SCENARIO = "an-object-store";
+25 -25
View File
@@ -31,9 +31,9 @@
* MESH_LAB_INCUS='sudo -n incus'
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap
* MESH_LAB_BUNDLE=.../mesh-host/examples/substrate-first-node.lock
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
* MESH_LAB_CATALOG=.../mesh-catalog/modules
* MESH_LAB_SOURCE=<forge>/mesh-control.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_SOURCE=<forge>/mesh-controller.git MESH_LAB_SOURCE_REF=<commit>
* MESH_LAB_KEEP=1 to leave it standing afterwards
*/
import { test, before, after } from "node:test";
@@ -45,7 +45,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec, push } from "../../src/lifecycle/operate.ts";
import { bootstrapBinaryPath, hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
import { genesis, type GenesisResult } from "./genesis.ts";
import { incus } from "../../src/incus/client.ts";
import { instanceNameOf } from "../../src/lifecycle/operate.ts";
@@ -69,7 +69,7 @@ const REGISTRY = `${ANCHOR}:5000`;
*/
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
/**
* What `amqp-ping` requires, and what the substrate does not supply.
* What `amqp-ping` requires, and what the foundation does not supply.
*
* The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a
* record of, so it provides nothing to anything. A module asking for `amqp` is asking for a
@@ -98,7 +98,7 @@ const FILTER_MODULE = "nftables";
const STORE = { module: "postgres", repo: "mesh-catalog", path: "modules/postgres", container: "mesh-postgres" };
const CATALOGUE = { module: "mesh-catalog", repo: "mesh-catalog", path: "modules/mesh-catalog", container: "mesh-catalog" };
/** The control plane, rebuilt from its own repository — the step that ends the installer's tenure. */
const CONTROL_PLANE = { module: "mesh-control", repo: "mesh-control", path: "", container: "mesh-control" };
const CONTROL_PLANE = { module: "mesh-controller", repo: "mesh-controller", path: "", container: "mesh-controller" };
/**
* What this mesh must hold when it is finished, and what must be RUNNING on the machine.
@@ -108,14 +108,14 @@ const CONTROL_PLANE = { module: "mesh-control", repo: "mesh-control", path: "",
* the registry and the builder — are here too: they are carried in, and a mesh missing any of them
* is not one.
*/
const MUST_HOLD = ["mesh-control", "distribution", "builder", "mesh-tools", "postgres",
const MUST_HOLD = ["mesh-controller", "distribution", "builder", "mesh-tools", "postgres",
"mesh-catalog", "lavinmq", "amqp-ping"];
const MUST_RUN = ["mesh-control", "mesh-registry", "mesh-broker", "mesh-store",
const MUST_RUN = ["mesh-controller", "mesh-registry", "mesh-broker", "mesh-store",
"mesh-postgres", "mesh-catalog", "mesh-lavinmq", "amqp-ping"];
/** Named once, because the step title is also how later steps say what they waited on. */
const NEEDS = "the mesh runs a broker for that module to talk to";
const GENESIS = "a bare machine becomes a mesh of one, raised by the installer";
const SUBSTRATE = "the substrate is up — a store and a broker of the mesh's own";
const FOUNDATION = "the foundation is up — a store and a broker of the mesh's own";
const BUILT_CP = "the control plane is one this mesh built, not one it was handed";
const PIVOTED = "the pivot finished — what raised the mesh is gone";
const HAS_REGISTRY = "the registry serves this mesh its own images";
@@ -191,7 +191,7 @@ const skip =
!installer ? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap" :
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" :
!sourceRef ? "MESH_LAB_SOURCE_REF is not set to the commit to build" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a substrate template" :
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation template" :
!catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
false;
@@ -220,19 +220,19 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
// The control plane is a live, mesh-MANAGED container: the mesh recreates it whenever its
// declaration changes — most visibly when the machine first gets its `.internal` name on the
// private network, which becomes the container's `--add-host` (containers are immutable, so a new
// spec is a new container). A `docker exec mesh-control` that lands in that brief recreate window
// spec is a new container). A `docker exec mesh-controller` that lands in that brief recreate window
// fails with "container ... is not running". That is not the mesh being wrong — it is a command
// racing a legitimate restart — so it is retried until the control plane answers again. A real
// command failure (anything else) still throws at once.
const deadline = Date.now() + (timeoutMs ?? 120_000);
for (;;) {
const { out, ok } = await on(CONTROL, `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
const { out, ok } = await on(CONTROL, `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs);
if (ok) return out;
if (/is not running|No such container/i.test(out) && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 2_000));
continue;
}
throw new Error(`${CONTROL}: docker exec mesh-control /mesh-control ${command}\n${out}`);
throw new Error(`${CONTROL}: docker exec mesh-controller /mesh-controller ${command}\n${out}`);
}
}
@@ -304,7 +304,7 @@ async function waitForContainer(node: string, container: string, seconds = 200):
*
* **The control plane runs in a container, so a file on the machine is not a file it can open.**
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
* inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is
* inside mesh-controller, which is correct and was briefly mistaken for a missing manifest. It is
* copied the last step of the way with `docker cp`.
*
* **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp`
@@ -315,7 +315,7 @@ async function registerModule(module: string, manifest: string): Promise<string>
const onMachine = `/tmp/${module}.json`;
const inContainer = `/${module}.json`;
await push(instanceId, CONTROL, manifest, onMachine);
await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`);
await must(CONTROL, `docker cp ${onMachine} mesh-controller:${inContainer}`);
return mesh(`module add ${inContainer}`);
}
function tokenFrom(said: string): string {
@@ -393,7 +393,7 @@ function report(name: string): string {
*/
const PLAN: { code: string; title: string }[] = [
{ code: "R1", title: GENESIS },
{ code: "R2", title: SUBSTRATE },
{ code: "R2", title: FOUNDATION },
{ code: "R3", title: BUILT_CP },
{ code: "R4", title: PIVOTED },
{ code: "R5", title: HAS_REGISTRY },
@@ -479,7 +479,7 @@ before(async () => {
// whose anchor is somewhere else every node, including this one, would enrol against an
// address nothing answers on. The installer refuses to guess it and says so, which is
// right: it does not know what this machine is called from outside.
bundleTemplate: substrateBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
bundleTemplate: foundationBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
registry: REGISTRY,
source,
sourceRef,
@@ -514,7 +514,7 @@ before(async () => {
// saying it published an image and the registry serving one are different facts, and it is the
// second that matters.
await step("R2", SUBSTRATE, GENESIS, async () => {
await step("R2", FOUNDATION, GENESIS, async () => {
await waitForContainer(CONTROL, "mesh-store", 120);
await waitForContainer(CONTROL, "mesh-broker", 120);
return (await on(CONTROL, `docker ps --format '{{.Names}}\t{{.Status}}'`)).out;
@@ -525,7 +525,7 @@ before(async () => {
// the image has to be one this mesh's own registry serves.
await step("R3", BUILT_CP, GENESIS, async () => {
const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-control 2>&1`)).out.trim();
`docker inspect -f '{{.Config.Image}}' mesh-controller 2>&1`)).out.trim();
assert.match(image, /@sha256:[0-9a-f]{64}/,
`the control plane names its image by tag, not by digest: ${image}`);
assert.ok(image.includes(":5000/"),
@@ -535,17 +535,17 @@ before(async () => {
await step("R4", PIVOTED, BUILT_CP, async () => {
const ps = (await on(CONTROL, `docker ps -a --format '{{.Names}}'`)).out;
assert.doesNotMatch(ps, /^temp-mesh-control$/m,
assert.doesNotMatch(ps, /^temp-mesh-controller$/m,
`the temporary control plane is still here, so the pivot did not finish:\n${ps}`);
return ps;
});
await step("R5", HAS_REGISTRY, SUBSTRATE, async () => {
await step("R5", HAS_REGISTRY, FOUNDATION, async () => {
await waitForContainer(CONTROL, "mesh-registry", 120);
const held = (await on(CONTROL,
`curl -sS --max-time 15 http://127.0.0.1:5000/v2/_catalog`)).out;
assert.match(held, /mesh-control/,
`the registry serves no mesh-control, so nothing was published into it:\n${held}`);
assert.match(held, /mesh-controller/,
`the registry serves no mesh-controller, so nothing was published into it:\n${held}`);
return held.trim();
});
@@ -597,7 +597,7 @@ before(async () => {
return built;
});
// A store of its own. **Not the substrate's.** The installer raises a store for the control
// A store of its own. **Not the foundation's.** The installer raises a store for the control
// plane to keep its own records in, the way it raises a broker — plumbing, not a module the mesh
// has any record of, so it provides nothing to anything. A module that wants a database wants a
// provider in the graph, and the catalogue below is the first thing to want one.
@@ -790,7 +790,7 @@ before(async () => {
// broker: a module's account is scoped to what it declares it emits and consumes, and calling
// a tool needs a temporary reply queue that scope does not cover. So a module can SERVE tools
// and cannot CALL them, and nothing issues an account to anyone who wants to ask (novox/hq
// issue 049). Until that is decided the caller is the substrate's bootstrap admin over the
// issue 049). Until that is decided the caller is the foundation's bootstrap admin over the
// broker's loopback, reached by joining its network namespace.
const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-catalog`)).out.trim();
@@ -1061,7 +1061,7 @@ after(async () => {
for (const name of [
GENESIS,
SUBSTRATE,
FOUNDATION,
BUILT_CP,
PIVOTED,
HAS_REGISTRY,
+14 -14
View File
@@ -12,7 +12,7 @@
* ordinary sealed-delivery path — with NO manager, NO refresh, NO access/refresh split, NO usage. The
* whole of OpenAI's integration in the control plane is one registry line (vendor -> static-key).
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* Build the consumer runtime image into the local daemon first:
* scripts/build-module-runtime.sh openai-consumer /tmp/openai-consumer.tar
*/
@@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "openai-bed";
@@ -69,11 +69,11 @@ async function must(command: string, timeoutMs?: number): Promise<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 }> {
return on(`docker exec mesh-control /mesh-control ${command}`);
return on(`docker exec mesh-controller /mesh-controller ${command}`);
}
/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */
@@ -82,7 +82,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -135,11 +135,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -159,7 +159,7 @@ test("a static-key model-access licence delivers the operator's API key to the c
const consumerImage = pinned("mesh-runtime-openai-consumer");
// --- the licence, a record with vendor openai (static-key) -------------------------------------
// No manager: a static-key licence has none (mesh-control refuses `licence manager` on it). The
// No manager: a static-key licence has none (mesh-controller refuses `licence manager` on it). The
// consumer is put on the licence BEFORE the key is set — the static-key adapter's Accept seals to the
// CURRENT holders, so a holder must exist first, or the key would seal to nobody.
await mesh(`licence add openai personal --serves '{"model":"gpt-model"}'`);
@@ -167,8 +167,8 @@ test("a static-key model-access licence delivers the operator's API key to the c
// The operator sets the static key. `licence key` reads it from a file (never a CLI arg) and seals it
// to the holder; the plaintext is discarded by the control plane. Staged 0644 so distroless
// mesh-control can read the file (docker cp preserves the mode).
await must(`printf %s ${quote(API_KEY)} > /tmp/openai-key && chmod 0644 /tmp/openai-key && docker cp /tmp/openai-key mesh-control:/openai-key`);
// mesh-controller can read the file (docker cp preserves the mode).
await must(`printf %s ${quote(API_KEY)} > /tmp/openai-key && chmod 0644 /tmp/openai-key && docker cp /tmp/openai-key mesh-controller:/openai-key`);
await mesh(`licence key personal --file /openai-key`);
// --- deploy the consumer -----------------------------------------------------------------------
@@ -199,7 +199,7 @@ test("a static-key model-access licence delivers the operator's API key to the c
},
],
});
await must(`printf %s ${quote(consumerManifest)} > /tmp/openai-consumer.json && docker cp /tmp/openai-consumer.json mesh-control:/openai-consumer.json`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/openai-consumer.json && docker cp /tmp/openai-consumer.json mesh-controller:/openai-consumer.json`);
await mesh(`module add /openai-consumer.json`);
await mesh(`module issue openai-consumer --node ${MACHINE}`);
await mesh(`assign ${MACHINE} openai-consumer`);
@@ -9,7 +9,7 @@
* the mesh minted. The proof is the consumer connecting to its database with the credential the mesh
* delivered it. Nothing is placed by the test.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh postgres builds mesh-runtime-postgres:development (with psql),
* which scenarios/postgres-node.yml stocks.
*/
@@ -21,7 +21,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -33,7 +33,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "postgres-node";
@@ -62,7 +62,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -71,7 +71,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -84,7 +84,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -116,11 +116,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -157,7 +157,7 @@ test("the mesh grants a consumer a postgres database, and the credential it deli
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
{ id: "net", type: "network", name: "postgres" },
{
// No published port here: the substrate's own store already holds host :5432 on this
// No published port here: the foundation's own store already holds host :5432 on this
// single-node bed, and the consumer reaches postgres over the private network by name. The
// committed manifest publishes it for cross-node consumers, which is a different node.
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
@@ -194,12 +194,12 @@ test("the mesh grants a consumer a postgres database, and the credential it deli
resources: [{ id: "state", type: "directory", path: "/var/lib/dbuser", mode: "0700" }],
});
await must(`printf %s ${quote(postgresManifest)} > /tmp/postgres.json && docker cp /tmp/postgres.json mesh-control:/postgres.json`);
await must(`printf %s ${quote(postgresManifest)} > /tmp/postgres.json && docker cp /tmp/postgres.json mesh-controller:/postgres.json`);
await mesh("module add /postgres.json");
await mesh(`module issue postgres --node ${MACHINE}`);
await mesh(`assign ${MACHINE} postgres`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/dbuser.json && docker cp /tmp/dbuser.json mesh-control:/dbuser.json`);
await must(`printf %s ${quote(consumerManifest)} > /tmp/dbuser.json && docker cp /tmp/dbuser.json mesh-controller:/dbuser.json`);
await mesh("module add /dbuser.json");
await mesh(`assign ${MACHINE} dbuser`);
@@ -20,7 +20,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -32,7 +32,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "redis-node";
@@ -61,7 +61,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -70,7 +70,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -83,7 +83,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -115,11 +115,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -184,7 +184,7 @@ test("redis's runtime, on the backend's private network, binds the broker and pr
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`);
await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`);
await mesh("module add /redis.json");
await mesh(`module issue redis --node ${MACHINE}`);
await mesh(`assign ${MACHINE} redis`);
@@ -9,10 +9,10 @@
* password gets PONG — where a provisioner that invented its own password would answer WRONGPASS.
*
* A hand-written contributions file and secret stand in for the control plane here (a full grant
* from a second module is a heavier bed); their SHAPE is exactly what mesh-control writes — a
* from a second module is a heavier bed); their SHAPE is exactly what mesh-controller writes — a
* `receives` doc with `as`/`secret`, and the secret file the host leaves after unsealing.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which
* scenarios/redis-node.yml stocks.
*/
@@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "redis-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -74,7 +74,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -87,7 +87,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -119,11 +119,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -184,7 +184,7 @@ test("redis creates a consumer's login with the password the mesh minted, sealin
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-control:/redis.json`);
await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`);
await mesh("module add /redis.json");
await mesh(`module issue redis --node ${MACHINE}`);
await mesh(`assign ${MACHINE} redis`);
+1 -1
View File
@@ -25,7 +25,7 @@ const provisioner = process.env["MESH_LAB_PROVISIONER"] ?? "";
const skip = !capability.usable
? `lab not usable: ${capability.why}`
: !provisioner
? "set MESH_LAB_PROVISIONER to a built provisioner (mesh-control: go build ./examples/postgres-provisioner)"
? "set MESH_LAB_PROVISIONER to a built provisioner (mesh-controller: go build ./examples/postgres-provisioner)"
: false;
const SCENARIO = "a-provider";
+14 -14
View File
@@ -25,7 +25,7 @@
* publicly-trusted certificate and answering an HTTP-01 challenge at the name — is proven separately
* by certificates.test.ts, which drives the same proxy binary against a real ACME server (Pebble).
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-route-proxy-image.sh builds mesh-route-proxy:development into the local daemon;
* scenarios/route-forwarding.yml stocks it and alpine:latest, and serves both by digest.
*/
@@ -37,7 +37,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -49,7 +49,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "route-forwarding";
@@ -81,7 +81,7 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
/** The control plane, a container on the node. */
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. */
@@ -90,9 +90,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -105,7 +105,7 @@ async function settled(withinMs = 600_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -143,12 +143,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
// Raise the foundation — store, broker, control — from the bundle.
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// The node joins its own mesh, so it is a node the mesh can assign to, and the host runs so it
@@ -216,13 +216,13 @@ test("the mesh routes a public name through the proxy to the consumer, and withd
],
});
await must(`printf %s ${quote(proxyManifest)} > /tmp/route-proxy.json && docker cp /tmp/route-proxy.json mesh-control:/route-proxy.json`);
await must(`printf %s ${quote(proxyManifest)} > /tmp/route-proxy.json && docker cp /tmp/route-proxy.json mesh-controller:/route-proxy.json`);
await mesh("module add /route-proxy.json");
// No `module issue`: route-proxy has no broker account and no own-secret to mint. `assign` resolves
// its plan and the provider is matchable by a consumer's route from that alone.
await mesh(`assign ${MACHINE} route-proxy`);
await must(`printf %s ${quote(webManifest)} > /tmp/hello-web.json && docker cp /tmp/hello-web.json mesh-control:/hello-web.json`);
await must(`printf %s ${quote(webManifest)} > /tmp/hello-web.json && docker cp /tmp/hello-web.json mesh-controller:/hello-web.json`);
await mesh("module add /hello-web.json");
await mesh(`assign ${MACHINE} hello-web`);
@@ -12,7 +12,7 @@
* the container was replaced (a new container id) and the config on disk carries the new value.
* It builds the host from source (no --no-build), because the behaviour under test is the host's.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh grafana builds mesh-runtime-grafana:development, which
* scenarios/grafana-node.yml stocks.
*/
@@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -36,7 +36,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "grafana-node";
@@ -65,7 +65,7 @@ async function must(command: string, timeoutMs?: number): Promise<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. */
@@ -74,7 +74,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -87,7 +87,7 @@ async function settled(withinMs = 480_000): Promise<void> {
const until = Date.now() + withinMs;
let last = "";
while (Date.now() < until) {
const asked = await on(`docker exec mesh-control /mesh-control status --json`);
const asked = await on(`docker exec mesh-controller /mesh-controller status --json`);
if (asked.ok) {
try {
const state = JSON.parse(asked.out) as {
@@ -112,7 +112,7 @@ async function settled(withinMs = 480_000): Promise<void> {
async function setToken(token: string): Promise<void> {
const settings = JSON.stringify({ url: "http://127.0.0.1:3000", token });
await must(`printf %s ${quote(settings)} > /tmp/s.json && docker cp /tmp/s.json mesh-control:/s.json`);
await must(`printf %s ${quote(settings)} > /tmp/s.json && docker cp /tmp/s.json mesh-controller:/s.json`);
await mesh(`settings set grafana /s.json --node ${MACHINE}`);
}
@@ -129,11 +129,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000);
const up = await must(`docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
await mesh(`node add ${MACHINE}`);
@@ -170,7 +170,7 @@ test("a running runtime is recreated when its settings change, and reads the new
},
],
});
await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-control:/grafana.json`);
await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-controller:/grafana.json`);
await mesh("module add /grafana.json");
await setToken("token-alpha");
+13 -13
View File
@@ -1,9 +1,9 @@
/**
* The whole `ace` server's converted service set, installed together on ONE node behind the
* substrate — the media / home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of
* foundation — the media / home-automation half of the whole-mesh rehearsal (novox/hq). Sibling of
* whole-mesh-novox.test.ts; same harness and topology, a larger, media-heavy module set.
*
* Substrate (store, broker, control) rides `anchor` and nothing else; ALL of ace's services ride the
* Foundation (store, broker, control) rides `anchor` and nothing else; ALL of ace's services ride the
* `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres/redis
* providers co-located with them. The media stack shares the operator-owned library directories
* under /services/media (ADR 0051 `accesses`): the mesh writes an `access` resource that CONFIRMS
@@ -20,7 +20,7 @@
* references of OURS are rewritten to the IDs the machine holds, and the co-located
* host-port collisions are remapped at load time (see REMAP).
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
*/
import { test, before, after } from "node:test";
@@ -31,7 +31,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -44,7 +44,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "whole-mesh-ace";
@@ -171,7 +171,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
}
async function mesh(command: string, timeoutMs?: number): Promise<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. */
@@ -180,7 +180,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function loadManifest(name: string): { manifest: string; broker: boolean } {
@@ -214,7 +214,7 @@ interface NodeState {
}
async function nodeState(node: string): Promise<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 };
let state: {
wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[];
@@ -248,11 +248,11 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000);
await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 900_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
for (const machine of ["anchor", NODE]) {
@@ -294,7 +294,7 @@ test("the whole ace service set resolves, installs and converges on one node in
if (DROPPED.some((d) => d.name === name)) continue;
try {
const { manifest, broker } = loadManifest(name);
await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`);
await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
if (broker) {
await mesh(`module issue ${name} --node ${NODE}`);
+55 -55
View File
@@ -5,12 +5,12 @@
*
* hosting (public) home (private, behind a NAT access point)
* novox 192.0.2.20 — the ANCHOR: ace 10.99.1.10 the home server, media/IoT set
* substrate (store/broker/ shanks 10.99.1.20 workstation (light: portainer only)
* foundation (store/broker/ shanks 10.99.1.20 workstation (light: portainer only)
* control) + the whole novox g14 10.99.1.30 workstation (light: portainer only)
* set + overlay hub + ingress
*
* There is NO separate anchor: novox IS the anchor. The substrate runs on novox, and novox also
* enrols as a node and receives its own service set — the substrate host and a service node at once.
* There is NO separate anchor: novox IS the anchor. The foundation runs on novox, and novox also
* enrols as a node and receives its own service set — the foundation host and a service node at once.
*
* TWO ACTS, AND THE BED NOW DISTINGUISHES THEM (novox/hq ADR 0067).
*
@@ -22,9 +22,9 @@
* is a WORKING MESH OF ONE, and this bed asserts exactly that before going any further.
*
* JOINING — ace, shanks and g14 then join a mesh that already exists: host binary, token,
* `enrol`, run the agent. No bootstrap, no substrate, no registry. novox is NOT enrolled again.
* `enrol`, run the agent. No bootstrap, no foundation, no registry. novox is NOT enrolled again.
*
* The bed used to do neither. It applied the substrate bundle itself and looped enrolment over all
* The bed used to do neither. It applied the foundation bundle itself and looped enrolment over all
* four machines as one continuous operation — which got the order right by accident and modelled
* the wrong shape, and is why ADR 0067's own acceptance check ("the bed bootstraps through the
* installer rather than around it") went unmet. Genesis GATES joining: if it stops, the bed says
@@ -39,10 +39,10 @@
* BEFORE any heavy module lands, so the cross-segment-overlay verdict survives whatever the module
* convergence then does. Phase B converges the full node sets and reports per node.
*
* SUBSTRATE-ON-NOVOX PORT COLLISIONS (a real consequence of collapsing the anchor onto novox that the
* separate-anchor beds never hit): the substrate store binds 127.0.0.1:5432 and novox's postgres
* provider publishes 5432; the substrate broker binds 5671 + 127.0.0.1:5672 and novox's lavinmq
* provider publishes 5672. The two provider host publishes are REMAPPED off the substrate's ports
* FOUNDATION-ON-NOVOX PORT COLLISIONS (a real consequence of collapsing the anchor onto novox that the
* separate-anchor beds never hit): the foundation store binds 127.0.0.1:5432 and novox's postgres
* provider publishes 5432; the foundation broker binds 5671 + 127.0.0.1:5672 and novox's lavinmq
* provider publishes 5672. The two provider host publishes are REMAPPED off the foundation's ports
* (REMAP below); consumers reach the providers over the mesh network on the container port, so the
* host side is free to move. Reported as a topology finding.
*
@@ -50,7 +50,7 @@
* (whole-mesh-full-live) and NOT torn down — it is left standing and browsable. Without it the bed
* behaves like every other: raise in before(), destroy in after().
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap MESH_LAB_CATALOG=.../mesh-catalog/modules
*/
@@ -65,7 +65,7 @@ import { destroy, exec, instanceNameOf, push } from "../../src/lifecycle/operate
import {
bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH,
} from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import { referenceFor, type HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -84,7 +84,7 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: !installer || !existsSync(installer)
? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap (mesh-host `make " +
"bootstrap IMAGE=mesh-builder:development`). The anchor is raised BY the installer now, " +
@@ -96,7 +96,7 @@ const skip = !capability.usable
: false;
const SCENARIO = "whole-mesh-full";
/** novox hosts the substrate and the control plane; it is where `mesh` commands run. */
/** novox hosts the foundation and the control plane; it is where `mesh` commands run. */
const CONTROL = "novox";
/** Every node in the mesh. novox is on hosting; the rest are behind the home gateway. */
const NODES = ["novox", "ace", "shanks", "g14"];
@@ -118,7 +118,7 @@ const CATALOGUE_ON_MACHINE = "/opt/mesh-catalog";
* `internal/bootstrap` RegistryModule and ControlPlaneModule. If it ever opens a third, this list
* is where the bed finds out, by the installer saying which manifest it could not read.
*/
const CATALOGUE_MODULES = ["registry", "mesh-control", "builder"];
const CATALOGUE_MODULES = ["registry", "mesh-controller", "builder"];
/**
* Where this mesh keeps its own images, as the anchor reaches it.
@@ -221,7 +221,7 @@ const CORE_NOVOX = new Set([
const GAPS_NOVOX = new Set([
"umami", "mailu", "firewall", "fail2ban", "only-office", "de-spiegel", "amqp-email-forwarder",
// step-ca is reported, not gated: the internal-CA ISSUANCE path is still being fixed in
// mesh-control, and this bed is not the place to discover that a fix has not landed yet. What is
// mesh-controller, and this bed is not the place to discover that a fix has not landed yet. What is
// gated is the half that is decided and cheap — see the ADR 0066 section at the end.
"step-ca",
]);
@@ -274,13 +274,13 @@ const PLAN: { node: string; mods: Mod[]; core: Set<string>; gaps: Set<string> }[
/**
* Host-port remaps (per module; host ports are per-VM so novox's and ace's never clash across nodes).
* The two SUBSTRATE collisions are the new ones: postgres 5432 and lavinmq 5672 are moved off the
* substrate store/broker's host ports, which only exist on novox because that is where the substrate
* The two FOUNDATION collisions are the new ones: postgres 5432 and lavinmq 5672 are moved off the
* foundation store/broker's host ports, which only exist on novox because that is where the foundation
* runs. The rest break the novox web/app host-port collisions (route-proxy fronts 80/443).
*/
const REMAP: Record<string, Record<string, string>> = {
// 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
// it. letta on ace died of exactly this — "is the server running on that host and accepting
// TCP/IP connections?" — while postgres sat healthy beside it.
@@ -344,7 +344,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
/** The control plane, a container on novox (the anchor). */
async function mesh(command: string, timeoutMs?: number): Promise<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. */
@@ -353,7 +353,7 @@ function pinned(reference: string): string {
}
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
function loadManifest(name: string): { manifest: string; broker: boolean } {
@@ -405,7 +405,7 @@ interface NodeState {
}
async function nodeState(node: string): Promise<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 };
let state: {
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
// being sealed to the machine, on a lab node, for a CA thrown away with the scenario.
"chmod 0644 /tmp/ca/root.crt /tmp/ca/root.key /tmp/ca/key-password",
"docker cp /tmp/ca/root.crt mesh-control:/ca-root-cert",
"docker cp /tmp/ca/root.key mesh-control:/ca-root-key",
"docker cp /tmp/ca/key-password mesh-control:/ca-root-key-password",
"docker cp /tmp/ca/root.crt mesh-controller:/ca-root-cert",
"docker cp /tmp/ca/root.key mesh-controller:/ca-root-key",
"docker cp /tmp/ca/key-password mesh-controller:/ca-root-key-password",
].join("\n"), 180_000);
if (!made.ok) {
console.log(`CA ROOT NOT MADE on ${CONTROL}:\n${made.out.split("\n").slice(-8).join("\n")}`);
@@ -527,7 +527,7 @@ function stepIn(said: string): string {
* Put on the anchor what the installer needs to read, and run it.
*
* **This is the whole of what changed, and it is not a refactor.** The bed used to apply the
* substrate bundle itself, by hand, and then enrol four machines in one loop. It got the order
* foundation bundle itself, by hand, and then enrol four machines in one loop. It got the order
* right by accident and it modelled the wrong shape: an install procedure that exists only as a
* test fixture is exercised by whoever writes tests and never by whoever installs, which is why
* every bootstrap fault this year was found late (novox/hq ADR 0067). The anchor is now raised by
@@ -566,31 +566,31 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
}
report.push(` catalogue ${CATALOGUE_MODULES.join(", ")} at ${CATALOGUE_ON_MACHINE}`);
// The substrate TEMPLATE — not the bundle. The installer produces the bundle from it: it replaces
// The foundation TEMPLATE — not the bundle. The installer produces the bundle from it: it replaces
// the control plane's image with the id of the image it carries, renames that container
// `temp-mesh-control`, and writes the result where a person can read it.
// `temp-mesh-controller`, and writes the result where a person can read it.
//
// Two substitutions still happen here, and both belong to the bed rather than to the installer.
// The example names three images at a registry the lab no longer raises: the store and the broker
// become the upstream references mesh-catalog pins (harness), and the machine pulls them over its
// uplink like any first node. The third, mesh-control, is deliberately LEFT naming that dead
// uplink like any first node. The third, mesh-controller, is deliberately LEFT naming that dead
// registry — the installer overwrites it, and leaving it proves that it does.
//
// And the broker's advertised address. The template hardcodes 192.0.2.10:5671, the old
// separate-anchor address; a token carries MESH_BROKER_ADDRESS verbatim as the endpoint an
// enrolling node dials, so with the substrate on novox it must be novox's own public address or
// enrolling node dials, so with the foundation on novox it must be novox's own public address or
// every node would enrol against a dead one. The installer refuses to guess this and says so
// loudly, which is right — it does not know what this machine is called from outside.
const template = bundleFor(images).replaceAll("192.0.2.10:5671", "192.0.2.20:5671");
const local = join(tmpdir(), `mesh-lab-substrate-${process.pid}.lock`);
const local = join(tmpdir(), `mesh-lab-foundation-${process.pid}.lock`);
writeFileSync(local, template);
await push(instanceId, CONTROL, local, "/tmp/substrate-template.lock");
await push(instanceId, CONTROL, local, "/tmp/foundation-template.lock");
const command = [
BOOTSTRAP_PATH,
`--source ${source}`,
`--source-ref ${sourceRef}`,
`--bundle /tmp/substrate-template.lock`,
`--bundle /tmp/foundation-template.lock`,
`--catalog ${CATALOGUE_ON_MACHINE}`,
`--node ${CONTROL}`,
`--registry ${MESH_REGISTRY}`,
@@ -628,10 +628,10 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// ------------------------------------------------------------------------------------------
// 1. The control plane answers. Asked of the PERMANENT container by name — `status` opens all
// three stores, so a reply proves the connections it was given are the substrate's own.
const answered = await on(CONTROL, `docker exec mesh-control /mesh-control status`, 60_000);
// three stores, so a reply proves the connections it was given are the foundation's own.
const answered = await on(CONTROL, `docker exec mesh-controller /mesh-controller status`, 60_000);
report.push(` control plane ${answered.ok ? answered.out.split("\n")[0] : "NO ANSWER"}`);
if (!answered.ok) return stop("after step 10", `mesh-control does not answer:\n${answered.out}`);
if (!answered.ok) return stop("after step 10", `mesh-controller does not answer:\n${answered.out}`);
// 2. The registry replies on /v2/ — the registry API's own "yes, I am one and I am ready". A
// container that is up is not a registry that serves.
@@ -643,22 +643,22 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// 3. THE PIVOT COMPLETED. ADR 0067 states this check in as many words: after installing, the
// running control plane's image is pinned by a digest THE MESH'S OWN REGISTRY ASSIGNED — not
// by an image id. If it is still an image id, the substrate's container is what is running,
// by an image id. If it is still an image id, the foundation's container is what is running,
// nothing was published, and this mesh can never roll out its own upgrades.
const pinnedTo = (await on(CONTROL, `docker inspect --format '{{.Config.Image}}' mesh-control`))
const pinnedTo = (await on(CONTROL, `docker inspect --format '{{.Config.Image}}' mesh-controller`))
.out.trim();
report.push(` pinned to ${pinnedTo || "(nothing)"}`);
if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) {
return stop("after step 10",
`mesh-control is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` +
`mesh-controller is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` +
`own configuration, which no registry ever served. The pivot did not happen: what is ` +
`running is the image the installer carried, not one this mesh published, so this mesh ` +
`cannot upgrade itself (novox/hq ADR 0067, "the pivot completed").`);
}
if (!new RegExp(`^${MESH_REGISTRY.replaceAll(".", "\\.")}/mesh-control@sha256:[0-9a-f]{64}$`)
if (!new RegExp(`^${MESH_REGISTRY.replaceAll(".", "\\.")}/mesh-controller@sha256:[0-9a-f]{64}$`)
.test(pinnedTo)) {
return stop("after step 10",
`mesh-control is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` +
`mesh-controller is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` +
`digest assigned by ${MESH_REGISTRY}.`);
}
@@ -669,36 +669,36 @@ async function genesis(images: HeldImage[]): Promise<GenesisResult> {
// outside to one that can — same container, same digest, same registry. The difference is
// whether a build happened, and the only place that is visible is the installer saying so.
const wanted = sourceRef.slice(0, 8);
if (!new RegExp(`built mesh-control from ${wanted}`).test(said)) {
if (!new RegExp(`built mesh-controller from ${wanted}`).test(said)) {
return stop("after the last step",
`the installer never said it built mesh-control from ${wanted}. What runs may have been ` +
`the installer never said it built mesh-controller from ${wanted}. What runs may have been ` +
`carried rather than made here, which is a mesh that cannot rebuild its own control plane.`);
}
report.push(` built here mesh-control from ${wanted}, by the carried builder`);
report.push(` built here mesh-controller from ${wanted}, by the carried builder`);
// 3b. And the registry really serves it, asked of the registry rather than of the container. A
// reference is a claim; a tag list is the registry agreeing.
const tags = await on(CONTROL,
`curl -s --max-time 10 http://${MESH_REGISTRY}/v2/mesh-control/tags/list`);
`curl -s --max-time 10 http://${MESH_REGISTRY}/v2/mesh-controller/tags/list`);
report.push(` registry holds ${tags.out.trim() || "nothing"}`);
if (!tags.out.includes("genesis")) {
return stop("after step 10",
`${MESH_REGISTRY} does not serve mesh-control, so the digest the container is pinned to ` +
`${MESH_REGISTRY} does not serve mesh-controller, so the digest the container is pinned to ` +
`names an image nothing can pull: ${tags.out.trim()}`);
}
// 4. The temporary control plane is GONE. Two control planes is the half-finished state, and the
// name is the audit: a machine running mesh-control and not temp-mesh-control has pivoted.
const temp = await on(CONTROL, `docker inspect --format '{{.State.Status}}' temp-mesh-control`);
report.push(` temp-mesh-control ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`);
// name is the audit: a machine running mesh-controller and not temp-mesh-controller has pivoted.
const temp = await on(CONTROL, `docker inspect --format '{{.State.Status}}' temp-mesh-controller`);
report.push(` temp-mesh-controller ${temp.ok ? `STILL HERE (${temp.out.trim()})` : "gone"}`);
if (temp.ok) {
return stop("after step 10",
`temp-mesh-control is still ${temp.out.trim()}. Two control planes are consuming this ` +
`temp-mesh-controller is still ${temp.out.trim()}. Two control planes are consuming this ` +
`mesh's broker queues; neither is wrong and the pivot is not finished.`);
}
// 5. And the mesh has heard from its one node. Everything the join phase does next depends on it.
const nodes = await on(CONTROL, `docker exec mesh-control /mesh-control node list`);
const nodes = await on(CONTROL, `docker exec mesh-controller /mesh-controller node list`);
report.push(` node list ${nodes.out.trim().split("\n").join(" | ")}`);
const line = nodes.out.split("\n").map((l) => l.trim()).find((l) => l.startsWith(`${CONTROL} `));
if (!line || !/^\S+\s+here\b/.test(line)) {
@@ -715,7 +715,7 @@ async function genesis(images: HeldImage[]): Promise<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
* enrolling it again would present the mesh with a second identity for a node it already knows —
@@ -866,7 +866,7 @@ test("the full mesh forms across the access point and both server sets converge"
const known = added.get(name);
if (known !== undefined) return known;
const { manifest, broker } = loadManifest(name);
await must(CONTROL, `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`);
await must(CONTROL, `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
added.set(name, broker);
return broker;
@@ -892,7 +892,7 @@ test("the full mesh forms across the access point and both server sets converge"
// Operator-provided app credentials (own-secrets), delivered as fake values through `secret accept`.
const credentialDelivered = new Map<string, boolean>();
for (const name of new Set(CREDENTIALS.map((c) => c.name))) {
await must(CONTROL, `printf %s ${quote(`fake-${name}-value`)} > /tmp/fake-${name} && docker cp /tmp/fake-${name} mesh-control:/fake-${name}`);
await must(CONTROL, `printf %s ${quote(`fake-${name}-value`)} > /tmp/fake-${name} && docker cp /tmp/fake-${name} mesh-controller:/fake-${name}`);
}
for (const c of CREDENTIALS) {
if (!assigned[c.node]!.has(c.module)) {
@@ -912,7 +912,7 @@ test("the full mesh forms across the access point and both server sets converge"
if (!assigned[s.node]!.has(s.module)) continue;
try {
const inControl = `/secret-${s.module}-${s.name}`;
await must(CONTROL, `printf %s ${quote(s.value)} > /tmp${inControl} && docker cp /tmp${inControl} mesh-control:${inControl}`);
await must(CONTROL, `printf %s ${quote(s.value)} > /tmp${inControl} && docker cp /tmp${inControl} mesh-controller:${inControl}`);
await mesh(`secret accept ${s.node} ${s.module} ${s.name} --from ${inControl}`);
} catch (err) {
console.log(`OPERATOR SECRET FAILED ${s.node}/${s.module}/${s.name}: ${(err as Error).message.split("\n").slice(0, 2).join(" | ")}`);
@@ -1021,7 +1021,7 @@ test("the full mesh forms across the access point and both server sets converge"
// and before this bed set a public domain it composed to nothing on every node, silently.
//
// What is NOT checked here is issuance: whether route-proxy actually obtains a certificate from
// step-ca over ACME. That path is being fixed in mesh-control as this is written, and a bed that
// step-ca over ACME. That path is being fixed in mesh-controller as this is written, and a bed that
// gated on it would be reporting somebody else's in-flight work as this bed's failure.
// ================================================================================================
const adr: string[] = ["================ ADR 0066: LABELLED ROUTES ================"];
+21 -21
View File
@@ -1,11 +1,11 @@
/**
* The whole `novox` server's converted service set, installed together on ONE node behind the
* substrate — the whole-catalogue install this rebuild has never actually run. First stage of a
* foundation — the whole-catalogue install this rebuild has never actually run. First stage of a
* whole-mesh rehearsal (novox/hq).
*
* Topology (proven by assigned-two-node-db.test.ts): the substrate (store, broker, control) rides
* Topology (proven by assigned-two-node-db.test.ts): the foundation (store, broker, control) rides
* `anchor` and nothing else; ALL of novox's services ride the `novox` node. novox's own postgres
* provider owns 5432 there, so it cannot co-locate with the substrate store. An overlay is placed so
* provider owns 5432 there, so it cannot co-locate with the foundation store. An overlay is placed so
* each consumer's binding `at` resolves to novox's private address and reaches the providers
* co-located with it.
*
@@ -28,7 +28,7 @@
* host ports here (container ports unchanged); the provider ports the consumers actually connect to
* (postgres 5432, minio 9000, mongodb 27017, mssql 1433) are left as-is. See REMAP below.
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
* scripts/build-module-runtime.sh builds one runtime per module that has code; the route-proxy image
* is built by scripts/build-route-proxy-image.sh; scenarios/whole-mesh-novox.yml stocks them all
* alongside every server image.
@@ -42,7 +42,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
const capability = await labIsUsable();
@@ -55,13 +55,13 @@ const skip = !capability.usable
: !binary || !existsSync(binary)
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
: !bundle || !existsSync(bundle)
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
: false;
const SCENARIO = "whole-mesh-novox";
const NODE = "novox";
/** Where the committed module.json files live: the mesh-catalog beside mesh-control. */
/** Where the committed module.json files live: the mesh-catalog beside mesh-controller. */
const catalogDir = process.env["MESH_LAB_CATALOG"]
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
@@ -103,7 +103,7 @@ const MODULES: { name: string; containers: string[]; node?: boolean }[] = [
* fail2ban declares `capabilities: ["intrusion-prevention"]`, but mesh-host advertises no such
* capability: profile/detectors.go defines container-runtime, package-manager, service-manager,
* firewall, overlay, seat, privileged and graphical-session — nothing for intrusion-prevention. So
* NO node can ever host fail2ban. Worse, `mesh-control assign` records the assignment even while
* NO node can ever host fail2ban. Worse, `mesh-controller assign` records the assignment even while
* reporting it "cannot be applied", and the whole-node `push` then refuses to resolve the ENTIRE node
* ("nothing was sent") over that one un-hostable assignment — one bad module blocks every other. It
* is therefore left unassigned here so the rest of the set can be proven. (novox/hq — escalated.)
@@ -118,8 +118,8 @@ const DROPPED: { name: string; why: string }[] = [
/**
* The provable CORE: modules that converge WHOLE on this node (every container up and stable) once
* the substrate resolves and applies the set. This bed gates green on the CORE — a regression in any
* of these turns it red. It is the substrate + all five providers + the four consumers that reach
* the foundation resolves and applies the set. This bed gates green on the CORE — a regression in any
* of these turns it red. It is the foundation + all five providers + the four consumers that reach
* their providers and stay up + the four standalone apps.
*/
const CORE = new Set([
@@ -132,7 +132,7 @@ const CORE = new Set([
* KNOWN GAPS: modules that RESOLVE and are placed, but whose containers do not stay up because the
* committed catalog manifest is incomplete (or, for firewall, a node-service expectation is unmet).
* They are reported every run with the exact failure, and escalated (novox/hq) — but they do not gate
* green, because the gap is in the catalog/host, not in this bed or the mesh substrate.
* green, because the gap is in the catalog/host, not in this bed or the mesh foundation.
*
* umami — the mesh-umami provisioner needs the umami server URL and admin password in its
* provisioner.env; the manifest wires neither, so it dies "UMAMI url or admin password
@@ -185,7 +185,7 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
/** The control plane, a container on the first node. */
async function mesh(command: string, timeoutMs?: number): Promise<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. */
@@ -194,9 +194,9 @@ function pinned(reference: string): string {
return onTheMachine(reference, held);
}
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
return foundationBundle(bundle, images);
}
/**
@@ -236,7 +236,7 @@ interface NodeState {
/** Ask the mesh, in its own terms, what a node has done with what it was sent. Never throws. */
async function nodeState(node: string): Promise<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 };
let state: {
wrong: { node: string; outcome: string; refused?: string; failed?: { id: string; error: string }[] }[];
@@ -270,12 +270,12 @@ before(async () => {
instanceId = raised.instanceId;
held = raised.images;
// anchor raises the substrate from its bundle, digests rewritten to the scenario registry's.
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000);
// anchor raises the foundation from its bundle, digests rewritten to the scenario registry's.
await must("anchor", `cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/foundation.lock`, 900_000);
const up = await must("anchor", `docker ps --format '{{.Names}}'`);
for (const c of ["mesh-store", "mesh-broker", "mesh-control"]) {
assert.match(up, new RegExp(c), `the substrate did not raise ${c}:\n${up}`);
for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) {
assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`);
}
// Both machines join the one mesh and run a host so they apply what they are pushed.
@@ -316,7 +316,7 @@ test("the whole novox service set resolves, installs and converges on one node i
for (const { name } of MODULES) {
try {
const { manifest, broker } = loadManifest(name);
await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-control:/${name}.json`);
await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
await mesh(`module add /${name}.json`);
if (broker) {
await mesh(`module issue ${name} --node ${NODE}`);
+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.
test("a run against code that has since changed is not current", () => {
const said = judge(
passing("2026-08-31T11:00:00Z", { "mesh-lab": "aaa", "mesh-control": "bbb" }),
passing("2026-08-31T11:00:00Z", { "mesh-lab": "aaa", "mesh-controller": "bbb" }),
now,
{ "mesh-lab": "aaa", "mesh-control": "ccc" },
{ "mesh-lab": "aaa", "mesh-controller": "ccc" },
);
assert.equal(said.current, false, "a run against changed code was reported as current");
const text = said.lines.join("\n");
assert.match(text, /mesh-control\s+at bbb, now at ccc/, text);
assert.match(text, /mesh-controller\s+at bbb, now at ccc/, text);
assert.match(text, /code that has since changed/, text);
});
+10 -10
View File
@@ -16,8 +16,8 @@ import {
*/
const HELD: HeldImage[] = [
{
requested: "mesh-control:development",
repository: "mesh-control",
requested: "mesh-controller:development",
repository: "mesh-controller",
reference: "sha256:" + "a".repeat(64),
},
{
@@ -51,7 +51,7 @@ test("the repository is the reference without its tag", () => {
*/
test("only what is built here and published nowhere counts as ours", () => {
for (const ours of [
"mesh-control:development", "mesh-runtime-plex:development", "mesh-route-proxy:development",
"mesh-controller:development", "mesh-runtime-plex:development", "mesh-route-proxy:development",
"mesh-provision-postgres@sha256:" + "0".repeat(64),
]) {
assert.ok(isMeshBuilt(ours), `${ours} is one of ours and was not recognised`);
@@ -61,7 +61,7 @@ test("only what is built here and published nowhere counts as ours", () => {
"registry.example:5000/novox/www:latest",
// A registry host in front of one of our names does NOT make it ours: it says somebody
// published it, so the machine can fetch it from there like anything else.
"registry.example:5000/mesh-control:development",
"registry.example:5000/mesh-controller:development",
]) {
assert.ok(!isMeshBuilt(theirs), `${theirs} is not ours and was claimed`);
}
@@ -96,16 +96,16 @@ test("a third-party image is left exactly as the manifest wrote it", () => {
});
test("a reference of ours that already carries a registry is still redirected", () => {
// What the committed substrate bundle looks like: written for a target that had a registry.
const before = `"image": "192.0.2.250:5000/mesh-control@sha256:${"e".repeat(64)}"`;
// What the committed foundation bundle looks like: written for a target that had a registry.
const before = `"image": "192.0.2.250:5000/mesh-controller@sha256:${"e".repeat(64)}"`;
assert.equal(pinnedInto(before, HELD), `"image": "sha256:${"a".repeat(64)}"`);
});
// A longer repository ending in a shorter one must not be half-replaced.
test("a repository that ends in another one is not partly rewritten", () => {
const before = `"image": "our-mesh-control@sha256:${"7".repeat(64)}"`;
const before = `"image": "our-mesh-controller@sha256:${"7".repeat(64)}"`;
assert.equal(pinnedInto(before, HELD), before,
"'our-mesh-control' was rewritten because it ends in 'mesh-control'");
"'our-mesh-controller' was rewritten because it ends in 'mesh-controller'");
});
test("every image in a whole manifest is settled at once", () => {
@@ -127,7 +127,7 @@ test("every image in a whole manifest is settled at once", () => {
});
test("what a repository is held under can be asked for, and absence is not an empty string", () => {
assert.equal(referenceFor(HELD, "mesh-control"), `sha256:${"a".repeat(64)}`);
assert.equal(referenceFor(HELD, "mesh-controller"), `sha256:${"a".repeat(64)}`);
assert.equal(referenceFor(HELD, "mesh-runtime-plex"), undefined);
});
@@ -144,7 +144,7 @@ test("what is still a placeholder can be named", () => {
*/
test("an image the machine cannot fetch by itself is handed over", () => {
for (const handed of [
"mesh-control:development",
"mesh-controller:development",
"mesh-runtime-plex:development",
`registry.example/novox/www@sha256:${"a".repeat(64)}`,
"registry.example:5000/novox/photos-server:latest",
+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
// artifact, not that artifact plus everything in `all:`. The opposite reading would place
// things nobody asked for, which is the shape of fault this lab exists to catch.
const placements = planPlacements(scenario("place:\n all: [host]\n anchor: [substrate]"));
const placements = planPlacements(scenario("place:\n all: [host]\n anchor: [foundation]"));
const byMachine = new Map(placements.map((p) => [p.machine, p.artifacts]));
assert.deepEqual(byMachine.get("anchor"), ["substrate"], "anchor should have ONLY substrate");
assert.deepEqual(byMachine.get("anchor"), ["foundation"], "anchor should have ONLY foundation");
assert.deepEqual(byMachine.get("peer"), ["host"]);
});
@@ -74,11 +74,11 @@ scenario: s
segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true }
images: [mesh-control:development, mesh-runtime-redis:development]
images: [mesh-controller:development, mesh-runtime-redis:development]
place: { all: [host, runtime] }
`);
assert.deepEqual(planHeldImages(s), [
{ machine: "anchor", images: ["mesh-control:development", "mesh-runtime-redis:development"] },
{ machine: "anchor", images: ["mesh-controller:development", "mesh-runtime-redis:development"] },
]);
});
@@ -90,16 +90,16 @@ machines:
anchor:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
images: [mesh-control:development]
images: [mesh-controller:development]
laptop:
at: { segment: hosting, address: [192.0.2.20] }
egress: true
images: [mesh-runtime-redis:development]
images: [mesh-control:development, mesh-runtime-redis:development]
images: [mesh-controller:development, mesh-runtime-redis:development]
place: { all: [host, runtime] }
`);
assert.deepEqual(planHeldImages(s), [
{ machine: "anchor", images: ["mesh-control:development"] },
{ machine: "anchor", images: ["mesh-controller:development"] },
{ machine: "laptop", images: ["mesh-runtime-redis:development"] },
]);
});
@@ -113,7 +113,7 @@ segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true }
bare: { at: { segment: hosting, address: [192.0.2.20] }, egress: true, images: [] }
images: [mesh-control:development]
images: [mesh-controller:development]
place: { all: [host, runtime] }
`);
assert.deepEqual(planHeldImages(s).map((p) => p.machine), ["anchor"]);
@@ -132,15 +132,15 @@ place: { all: [host] }
test("a tier that does not exist is refused BY NAME", () => {
// Named individually rather than refused as a whole, so a scenario placing a host and a
// substrate is told exactly which half the lab cannot do — rather than being told `place:`
// foundation is told exactly which half the lab cannot do — rather than being told `place:`
// is unsupported when half of it now works.
try {
assertSupported(scenario("place:\n all: [host, substrate]\n peer: [control]"));
assertSupported(scenario("place:\n all: [host, foundation]\n peer: [control]"));
assert.fail("expected a refusal");
} catch (err) {
assert.ok(err instanceof UnsupportedError);
const missing = err.missing.join("\n");
assert.match(missing, /substrate/, "the substrate was not named");
assert.match(missing, /foundation/, "the foundation was not named");
assert.match(missing, /control/, "the control plane was not named");
assert.doesNotMatch(missing, /place: host/, "the host is placeable and was refused anyway");
}
@@ -172,18 +172,18 @@ test("an image reference is placeable, and a bare 'image:' is not", () => {
test("the placeables are host, runtime and an image", () => {
assert.ok(isPlaceable("host"));
assert.ok(isPlaceable("runtime"));
assert.ok(!isPlaceable("substrate"), "the tiers above tier 0 do not exist yet");
assert.ok(!isPlaceable("foundation"), "the tiers above tier 0 do not exist yet");
assert.ok(!isPlaceable("control-plane"));
});
test("an unplaceable artifact is named, not refused as a whole", () => {
// A scenario placing a host and a substrate is told which half the lab cannot do — refusing
// A scenario placing a host and a foundation is told which half the lab cannot do — refusing
// wholesale would send somebody looking for the wrong problem.
const scenario = {
name: "s",
segments: {},
machines: { a: {} as never },
place: { a: ["host", "runtime", "image:alpine@sha256:x", "substrate"] },
place: { a: ["host", "runtime", "image:alpine@sha256:x", "foundation"] },
} as unknown as Parameters<typeof assertSupported>[0];
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
// unplaceable. Searching for the bare word matched the message's own list of what CAN
// be placed, which mentions runtime — so the test failed on a correct message.
assert.ok(err.message.includes("place: substrate —"), "the unplaceable one is named");
assert.ok(err.message.includes("place: foundation —"), "the unplaceable one is named");
assert.ok(!err.message.includes("place: image:alpine"), "a placeable one is not");
assert.ok(!err.message.includes("place: runtime —"), "nor is runtime");
assert.ok(!err.message.includes("place: host —"), "nor is host");
+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 ?? []),
];
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",
);
});
+4 -4
View File
@@ -60,20 +60,20 @@ place: { all: [host] }`);
});
test("a tier above 0 is still refused, and named", () => {
// The refusal narrowed rather than disappearing. A scenario placing a host AND a substrate
// The refusal narrowed rather than disappearing. A scenario placing a host AND a foundation
// must be told which half is missing — not that `place:` is unsupported, when half of it
// now works.
const scenario = parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
place: { all: [host, substrate] }`);
place: { all: [host, foundation] }`);
try {
assertSupported(scenario);
assert.fail("should have refused");
} catch (err) {
assert.ok(err instanceof UnsupportedError);
assert.equal(err.missing.length, 1, `expected only the substrate: ${err.missing.join(", ")}`);
assert.match(err.missing[0] ?? "", /substrate/);
assert.equal(err.missing.length, 1, `expected only the foundation: ${err.missing.join(", ")}`);
assert.match(err.missing[0] ?? "", /foundation/);
assert.match(err instanceof Error ? err.message : "", /silently lacks them/);
}
});
+3 -3
View File
@@ -280,7 +280,7 @@ test("one of ours in images: is accepted", () => {
assert.doesNotThrow(() => parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: [mesh-control:development, mesh-route-proxy:development]
images: [mesh-controller:development, mesh-route-proxy:development]
place: { all: [runtime] }`));
});
@@ -288,7 +288,7 @@ test("images: is named by tag — an image ID is not knowable until the image is
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: ["mesh-control@sha256:${"0".repeat(64)}"]
images: ["mesh-controller@sha256:${"0".repeat(64)}"]
place: { all: [runtime] }`,
/is pinned by digest/);
});
@@ -303,7 +303,7 @@ machines:
at: { segment: net, address: [192.0.2.1] }
egress: true
images: [mesh-runtime-redis:development]
images: [mesh-control:development]
images: [mesh-controller:development]
place: { all: [runtime] }`,
/is not in this scenario's images/);
});
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import { judge, type Warm } from "../src/warm.ts";
const at = "2026-08-31T20:00:00Z";
const built = { "mesh-lab": "aaa", "mesh-host": "bbb", "mesh-control": "ccc" };
const built = { "mesh-lab": "aaa", "mesh-host": "bbb", "mesh-controller": "ccc" };
const warm = (over: Partial<Warm> = {}): Warm =>
({ scenario: "two-nodes", instanceId: "mlab-two-nodes-1", images: [], against: built, at, ...over });