Merge pull request 'One-node bed: SDK-by-version, rename, and the store/broker upgrade proofs' (#27) from feat/a-bed-that-hands-over-nothing into main
This commit was merged in pull request #27.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"scenario": "one-node-mesh",
|
||||
"established": 22,
|
||||
"of": 22,
|
||||
"steps": [
|
||||
{
|
||||
"code": "R1",
|
||||
"title": "a bare machine becomes a mesh of one, raised by the installer",
|
||||
"status": "pass",
|
||||
"seconds": 316,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "R2",
|
||||
"title": "the foundation is up — a store and a broker of the mesh's own",
|
||||
"status": "pass",
|
||||
"seconds": 1,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "R3",
|
||||
"title": "the control plane is one this mesh built, not one it was handed",
|
||||
"status": "pass",
|
||||
"seconds": 0,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "R4",
|
||||
"title": "the pivot finished — what raised the mesh is gone",
|
||||
"status": "pass",
|
||||
"seconds": 0,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "R5",
|
||||
"title": "the registry serves this mesh its own images",
|
||||
"status": "pass",
|
||||
"seconds": 0,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "R6",
|
||||
"title": "the machine is enrolled, and an agent is running on it",
|
||||
"status": "pass",
|
||||
"seconds": 0,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "R7",
|
||||
"title": "the builder is installed as a module, with an account",
|
||||
"status": "pass",
|
||||
"seconds": 0,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P1",
|
||||
"title": "the mesh builds the shared base from source",
|
||||
"status": "pass",
|
||||
"seconds": 3,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P2",
|
||||
"title": "the mesh builds and runs a store of its own",
|
||||
"status": "pass",
|
||||
"seconds": 7,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P3",
|
||||
"title": "the mesh builds and runs its own catalogue",
|
||||
"status": "pass",
|
||||
"seconds": 4,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P4",
|
||||
"title": "the mesh rebuilds its own control plane from source",
|
||||
"status": "pass",
|
||||
"seconds": 30,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "N1",
|
||||
"title": "the mesh puts itself on a private network, and its machine has a name",
|
||||
"status": "pass",
|
||||
"seconds": 4,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "N2",
|
||||
"title": "the machine has a packet filter, loaded from what modules declared",
|
||||
"status": "pass",
|
||||
"seconds": 2,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "U1",
|
||||
"title": "the mesh builds a module standing on that base",
|
||||
"status": "pass",
|
||||
"seconds": 14,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "U2",
|
||||
"title": "the mesh runs a broker for that module to talk to",
|
||||
"status": "pass",
|
||||
"seconds": 26,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "U3",
|
||||
"title": "the anchor runs the module the mesh built",
|
||||
"status": "pass",
|
||||
"seconds": 7,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "V1",
|
||||
"title": "the control plane can describe the mesh, and what it says is true",
|
||||
"status": "pass",
|
||||
"seconds": 1,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "V2",
|
||||
"title": "the catalogue holds every module this mesh built",
|
||||
"status": "pass",
|
||||
"seconds": 3,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "V3",
|
||||
"title": "the machine's networking is what the modules asked for",
|
||||
"status": "pass",
|
||||
"seconds": 1,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "V4",
|
||||
"title": "every resource the mesh declared is true on the machine",
|
||||
"status": "pass",
|
||||
"seconds": 13,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "E1",
|
||||
"title": "a change to a module's source reaches the machine on its own",
|
||||
"status": "pass",
|
||||
"seconds": 12,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "E2",
|
||||
"title": "the mesh comes back after the machine reboots",
|
||||
"status": "pass",
|
||||
"seconds": 33,
|
||||
"why": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# FOUR FRESH MACHINES AND THE INSTALLER. Nothing is handed to them.
|
||||
#
|
||||
# This is `whole-mesh-full`'s topology with `genesis-single`'s honesty. The four-machine bed loads
|
||||
# thirty-four of the mesh's own images onto its machines from the workstation, because it does not
|
||||
# build them — they are produced by hand beside the bed and copied in. That is a shape no real
|
||||
# installation has, and it is the same class of fiction the lab already removed once: it used to
|
||||
# raise a registry inside the scenario, and a bootstrap that only worked against it went green here
|
||||
# and would have failed on any real machine.
|
||||
#
|
||||
# So this bed hands over NOTHING. There is no `images:` list. Every machine gets a container
|
||||
# runtime and the host binary, which are prerequisites of the machine rather than parts of the
|
||||
# mesh, and after that the mesh is on its own:
|
||||
#
|
||||
# - the foundation, the registry and the builder's own dependencies are PULLED from the internet,
|
||||
# which is where a bare machine gets them;
|
||||
# - the control plane is BUILT, by the builder the installer carries, from a repository and a
|
||||
# commit it is told to use;
|
||||
# - every module after that is BUILT by the mesh's own builder and published into the mesh's own
|
||||
# registry, and a machine that runs one PULLS it from there.
|
||||
#
|
||||
# That last clause is the thing no bed has ever checked, and it is why this one has four machines
|
||||
# rather than one. Genesis puts the builder, the registry and everything they make on ONE machine.
|
||||
# A second machine running a mesh-built module has to fetch it from a registry that asks who it is,
|
||||
# and nothing yet gives a joined node an account for it. The bed asks anyway, in its own test, so
|
||||
# the gap is a named failure rather than an absence.
|
||||
#
|
||||
# hosting (public, routable) home (private, behind the access point)
|
||||
# anchor 192.0.2.20 ── anchor home-server 10.99.1.10 home server
|
||||
# foundation, registry, workstation 10.99.1.20 workstation
|
||||
# builder, control plane laptop 10.99.1.30 workstation
|
||||
#
|
||||
# EGRESS IS NOT OPTIONAL HERE. With nothing loaded, a sealed machine stops at the installer's first
|
||||
# pull. Every machine has a way out, and it is a SECOND path: each still reaches the rest of the
|
||||
# scenario through its declared gateway, and the uplink carries only what leaves the scenario —
|
||||
# the public images, and the forge the control plane is cloned from.
|
||||
#
|
||||
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap
|
||||
# MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
|
||||
# MESH_LAB_CATALOG=.../mesh-catalog/modules
|
||||
# MESH_LAB_SOURCE=<forge url> MESH_LAB_SOURCE_REF=<commit>
|
||||
scenario: fresh-mesh
|
||||
|
||||
segments:
|
||||
# The routable segment. anchor lives here; its public address is the broker endpoint every token
|
||||
# carries and the overlay hub the home nodes dial.
|
||||
hosting:
|
||||
kind: public
|
||||
cidr: [192.0.2.0/24]
|
||||
|
||||
# The household segment behind an ordinary home router: masquerades v4 outbound, forwards
|
||||
# inbound, expires idle mappings after two minutes.
|
||||
home:
|
||||
kind: private
|
||||
cidr: [10.99.1.0/24]
|
||||
gateway:
|
||||
to: hosting
|
||||
address: [192.0.2.50]
|
||||
nat: [v4]
|
||||
forwardable: true
|
||||
mapping_ttl: 120s
|
||||
|
||||
machines:
|
||||
# The anchor. Raised by the installer into a mesh of one, and then asked to build.
|
||||
#
|
||||
# Sized for what it actually does here: the store, the broker, the registry, TWO control planes
|
||||
# during the pivot, the builder, and a build worksphome-server holding a Node toolchain image and an npm
|
||||
# cache. It is NOT sized for the whole anchor service set, because this bed does not run one — it
|
||||
# proves the machinery that would produce it.
|
||||
anchor:
|
||||
at: { segment: hosting, address: [192.0.2.20] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 12GiB
|
||||
cpus: 6
|
||||
disk: 60GiB
|
||||
|
||||
# Three machines that JOIN. Host binary and a token, nothing else — no bootstrap, no foundation,
|
||||
# no registry. They are deliberately small: what they are here to prove is that a joined machine
|
||||
# can be given a module the mesh built, which is a question about credentials and not about load.
|
||||
home-server:
|
||||
at: { segment: home, address: [10.99.1.10] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 4GiB
|
||||
cpus: 2
|
||||
disk: 25GiB
|
||||
workstation:
|
||||
at: { segment: home, address: [10.99.1.20] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 3GiB
|
||||
cpus: 2
|
||||
disk: 20GiB
|
||||
laptop:
|
||||
at: { segment: home, address: [10.99.1.30] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 3GiB
|
||||
cpus: 2
|
||||
disk: 20GiB
|
||||
|
||||
# **No `images:` key, and that is the whole point of this file.** Anything a machine holds here, it
|
||||
# pulled or the mesh built. See the header.
|
||||
|
||||
plhome-server:
|
||||
all: [host, runtime]
|
||||
@@ -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]
|
||||
|
||||
@@ -20,7 +20,7 @@ machines:
|
||||
cpus: 2
|
||||
|
||||
images:
|
||||
- mesh-control:development
|
||||
- mesh-controller:development
|
||||
- mesh-runtime-grafana:development
|
||||
|
||||
place:
|
||||
|
||||
@@ -28,7 +28,7 @@ machines:
|
||||
inbound: allow
|
||||
|
||||
images:
|
||||
- mesh-control:development
|
||||
- mesh-controller:development
|
||||
|
||||
place:
|
||||
all: [host, runtime]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# ONE MACHINE, AND EVERYTHING A MESH HAS TO BE. Nothing is handed to it.
|
||||
#
|
||||
# The common case, and the one worth getting right first: a person with a single machine runs the
|
||||
# installer and ends up with a mesh that works. Not a mesh that *runs* — `17-raising-a-mesh` is
|
||||
# careful about that difference, and so is this scenario. Genesis ends with a foundation, a registry,
|
||||
# a built control plane and a builder, and a mesh in that state cannot produce anything and holds no
|
||||
# record of what it has. Calling that "up" is how the catalogue came to be missing from a test for
|
||||
# weeks without anything complaining.
|
||||
#
|
||||
# So the test driving this asks the harder question: can this machine, given nothing but a container
|
||||
# runtime and the host binary, end up holding
|
||||
#
|
||||
# - a foundation and a registry it pulled from the internet,
|
||||
# - a control plane it BUILT, and then rebuilt from its own repository through the module path,
|
||||
# - a builder that takes work over the broker,
|
||||
# - the shared base every module with code of its own stands on,
|
||||
# - a store of its own — the foundation's is the control plane's own plumbing, not a provider,
|
||||
# - a catalogue, so it can say what it has and what a change reaches,
|
||||
# - and a module of its own, built, provisioned and running.
|
||||
#
|
||||
# **There is no `images:` key, and that is the whole point of this file.** The four-machine scenario
|
||||
# next door loads thirty-four of the mesh's own images from the workstation because it does not
|
||||
# build them — a shape no real installation has. Here nothing is loaded. What the machine holds it
|
||||
# either pulled from the internet or made.
|
||||
#
|
||||
# EGRESS IS NOT OPTIONAL. With nothing loaded, a sealed machine stops at the installer's first pull.
|
||||
#
|
||||
# MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BOOTSTRAP_BINARY=.../mesh-bootstrap
|
||||
# MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock
|
||||
# MESH_LAB_CATALOG=.../mesh-catalog/modules
|
||||
# MESH_LAB_SOURCE=<forge url> MESH_LAB_SOURCE_REF=<commit>
|
||||
scenario: one-node-mesh
|
||||
|
||||
segments:
|
||||
hosting:
|
||||
kind: public
|
||||
cidr: [192.0.2.0/24]
|
||||
|
||||
machines:
|
||||
# The address matters: the foundation template names the broker at a fixed address, and a token
|
||||
# carries that verbatim as the endpoint an enrolling node dials. With one machine, that machine
|
||||
# must BE it, or the mesh hands out an endpoint nothing answers on.
|
||||
#
|
||||
# Sized for what it actually does: the store, the broker, the registry, two control planes during
|
||||
# the pivot, the builder, a Node toolchain and an npm cache in the build workspace, and then every
|
||||
# core module it builds and runs on top of that.
|
||||
anchor:
|
||||
at: { segment: hosting, address: [192.0.2.10] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 12GiB
|
||||
cpus: 6
|
||||
disk: 60GiB
|
||||
|
||||
place:
|
||||
all: [host, runtime]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,7 +20,7 @@ machines:
|
||||
cpus: 2
|
||||
|
||||
images:
|
||||
- mesh-control:development
|
||||
- mesh-controller:development
|
||||
- mesh-runtime-sonarr:development
|
||||
|
||||
place:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
+49
-6
@@ -7,7 +7,7 @@
|
||||
* from the internet, and that is now the thing worth proving before a raise says it is finished.
|
||||
*
|
||||
* The failure it exists to stop is the same one, in the same shape: `raise` returns, the caller
|
||||
* applies a substrate, the first pull fails, no node enrols, and the instance is left a bare
|
||||
* applies a foundation, the first pull fails, no node enrols, and the instance is left a bare
|
||||
* shell — with the cause several steps back and looking like a mesh fault rather than a lab one.
|
||||
*
|
||||
* Two things are checked, in this order, because they fail differently and the difference is the
|
||||
@@ -89,7 +89,38 @@ export async function confirmEgress(
|
||||
// the retry is tightened so a silent server costs seconds rather than the step. A broken uplink
|
||||
// still fails the check above, before any of this.
|
||||
await resilientResolver(name);
|
||||
|
||||
// **And then prove it is STEADY, because one success proved nothing.**
|
||||
//
|
||||
// The check above is satisfied by a single answer, and that is how a machine resolving one
|
||||
// query in three passed it and then killed two installs twenty minutes later. What a run needs
|
||||
// is not "a name resolved once" but "names resolve reliably", and those differ by exactly the
|
||||
// failure that has cost the most time here. Consecutive, because alternating success and
|
||||
// timeout is the observed shape — a total count would pass on the same machine.
|
||||
const steady = await steadilyResolves(name, 5);
|
||||
if (steady < 5) {
|
||||
throw new EgressError(
|
||||
`${machine} resolves ${UPSTREAM} only ${steady} time(s) in five consecutive tries.\n` +
|
||||
` It has egress and an unreliable resolver, which does not fail here — it fails later, ` +
|
||||
`inside a pull or a clone, as "could not resolve host" with the cause long out of view.\n` +
|
||||
` The uplink's own resolver is the usual culprit and is deliberately last in the list; ` +
|
||||
`a machine still failing this has something wrong upstream of the lab.`,
|
||||
);
|
||||
}
|
||||
log(` ${machine} resolves steadily (5/5)`);
|
||||
}
|
||||
}
|
||||
|
||||
/** How many of `tries` consecutive lookups answered. Stops at the first failure. */
|
||||
async function steadilyResolves(name: string, tries: number): Promise<number> {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
const said = await incusOk(
|
||||
["exec", name, "--", "sh", "-c",
|
||||
`timeout 8 getent hosts ${UPSTREAM} >/dev/null && echo yes`], 20_000,
|
||||
);
|
||||
if (said?.trim() !== "yes") return i;
|
||||
}
|
||||
return tries;
|
||||
}
|
||||
|
||||
async function resolves(name: string, waitSeconds: number): Promise<boolean> {
|
||||
@@ -140,11 +171,22 @@ async function reaches(name: string, waitSeconds: number): Promise<string | null
|
||||
* The shape of the problem is visible in `resolvectl status`: the machine has sensible global
|
||||
* fallbacks, and the *link* carrying the default route has exactly one server — the uplink gateway.
|
||||
* resolved will not reach for a global fallback while the link it is using has a server of its own,
|
||||
* so one unanswered packet is one failed lookup. Under four machines pulling images at once that
|
||||
* happens, and it has ended three runs long after the egress check passed.
|
||||
* so one unanswered packet is one failed lookup.
|
||||
*
|
||||
* The uplink stays first, so the modelled path is still the one used and still proven by the check
|
||||
* above. The others answer only when it does not.
|
||||
* **The uplink goes last, and this is a preference, not a fix.** The uplink's resolver is a single
|
||||
* dnsmasq with no unique knowledge any scenario machine needs — machines here address each other by
|
||||
* address, and the mesh writes its own names into /etc/hosts — so there is no reason for anything
|
||||
* to wait on it. It is kept, last, so DHCP-supplied names still answer.
|
||||
*
|
||||
* **It is written down as a preference because it was once mistaken for the cure.** Lookups were
|
||||
* timing out two times in three; the uplink was first in the list; reordering it made five in five;
|
||||
* the conclusion drew itself and was wrong. The machines were sharing ONE DHCP lease (see
|
||||
* `distinguishMachines` in raise.ts), so which machine could resolve anything depended on which had
|
||||
* last won an ARP race — and reordering resolvers on a machine that has just won looks exactly like
|
||||
* a fix. Nothing below this line will save a bed whose machines share an address.
|
||||
*
|
||||
* The caches are flushed afterwards, because resolved remembers the failures it collected while
|
||||
* the bad server was in front.
|
||||
*/
|
||||
async function resilientResolver(name: string): Promise<void> {
|
||||
await incus([
|
||||
@@ -152,7 +194,8 @@ async function resilientResolver(name: string): Promise<void> {
|
||||
`link=$(ip -4 route show default | awk '{print $5}' | head -n1); ` +
|
||||
`via=$(ip -4 route show default | awk '{print $3}' | head -n1); ` +
|
||||
`if [ -n "$link" ] && command -v resolvectl >/dev/null 2>&1; then ` +
|
||||
`resolvectl dns "$link" $via 1.1.1.1 8.8.8.8 >/dev/null 2>&1 || true; fi; true`,
|
||||
`resolvectl dns "$link" 1.1.1.1 8.8.8.8 9.9.9.9 $via >/dev/null 2>&1 || true; ` +
|
||||
`resolvectl flush-caches >/dev/null 2>&1 || true; fi; true`,
|
||||
], 30_000);
|
||||
}
|
||||
|
||||
|
||||
+25
-2
@@ -5,7 +5,7 @@
|
||||
* the thing it exists to test did not exist (novox/hq 03-DESIGN/00-as-is/11-the-lab.md). Tier
|
||||
* 0 now does, so this is the seam where the lab acquires a consumer.
|
||||
*
|
||||
* Only `host` is placeable. Everything else in the placement vocabulary — the substrate, a
|
||||
* Only `host` is placeable. Everything else in the placement vocabulary — the foundation, a
|
||||
* control plane, a forge — is still refused by name rather than ignored, because a scenario
|
||||
* that declares something and raises without it is the fault this lab was built to catch
|
||||
* (novox/hq 04-ISSUES/003).
|
||||
@@ -515,7 +515,7 @@ export async function loadHeldImages(
|
||||
throw new Error(
|
||||
`${requested} is not on this workstation, so there is nothing to hand the machines.\n` +
|
||||
` It is one of the mesh's own images and exists in no registry — nothing can pull it.\n` +
|
||||
` Build it first (mesh-control's \`make image …\`, or scripts/build-module-runtime.sh).`,
|
||||
` Build it first (mesh-controller's \`make image …\`, or scripts/build-module-runtime.sh).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -626,3 +626,26 @@ export async function loadHeldImages(
|
||||
|
||||
return held;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the host's systemd packaging, so the installer supervises it as a service rather than a
|
||||
* background process — the path a real machine takes, and the only one that survives a reboot. The
|
||||
* lab places the binary at HOST_PATH; the shipped launcher runs $MESH_HOST_BIN (default
|
||||
* /usr/bin/nox-mesh-host), so a symlink points that at the binary rather than editing the packaged
|
||||
* unit. The installer's --host-service then starts AND enables it (novox/hq ADR 0005).
|
||||
*/
|
||||
export async function installHostService(
|
||||
instanceName: string,
|
||||
machine: string,
|
||||
packagingDir: string,
|
||||
logMessage: (message: string) => void = () => {},
|
||||
): Promise<void> {
|
||||
const launcher = join(packagingDir, "nox-mesh-host-launch");
|
||||
const unit = join(packagingDir, "nox-mesh-host.service");
|
||||
await incus(["exec", instanceName, "--", "mkdir", "-p", "/usr/lib/nox-mesh-host"], 60_000);
|
||||
await incus(["file", "push", launcher, `${instanceName}/usr/lib/nox-mesh-host/launch`, "--mode", "0755"], 120_000);
|
||||
await incus(["file", "push", unit, `${instanceName}/etc/systemd/system/nox-mesh-host.service`, "--mode", "0644"], 120_000);
|
||||
await incus(["exec", instanceName, "--", "ln", "-sf", HOST_PATH, "/usr/bin/nox-mesh-host"], 60_000);
|
||||
await incus(["exec", instanceName, "--", "systemctl", "daemon-reload"], 60_000);
|
||||
logMessage(`installed nox-mesh-host.service on ${machine}`);
|
||||
}
|
||||
|
||||
+92
-1
@@ -307,9 +307,18 @@ export async function raise(
|
||||
enter("waiting for machines to become usable");
|
||||
await waitUntilAllUsable(created, readyTimeout, log);
|
||||
|
||||
// **Before the uplink lease is asked for, make sure each machine asks as itself.**
|
||||
enter("giving each machine its own identity");
|
||||
await distinguishMachines(created, log);
|
||||
|
||||
enter("applying declared addresses");
|
||||
await applyAddresses(scenario, instanceId, byMachine, log);
|
||||
|
||||
// The positive control for the step above: if two machines share an uplink address, say so
|
||||
// HERE, where it is one obvious sentence, rather than letting it surface an hour later as
|
||||
// intermittent name resolution on some machines and not others.
|
||||
await noTwoMachinesShareAnAddress(scenario, byMachine, log);
|
||||
|
||||
// Transit first: a gateway's default route points at it, so it has to exist.
|
||||
enter("wiring the public networks together");
|
||||
const transit = await raiseTransit(scenario, instanceId, log);
|
||||
@@ -329,7 +338,7 @@ export async function raise(
|
||||
// **Only now is "this machine can reach the outside" a true statement.** The route, the
|
||||
// gateway and the machine's own filtering are all in place, so this is the path a pull takes.
|
||||
// A raise that returned without checking would hand the next step a fact it depends on and
|
||||
// has no way to test — which is how a substrate apply used to die on its first pull.
|
||||
// has no way to test — which is how a foundation apply used to die on its first pull.
|
||||
enter("confirming egress reaches the internet");
|
||||
await confirmEgress(scenario, byMachine, log);
|
||||
|
||||
@@ -356,3 +365,85 @@ export async function raise(
|
||||
throw new RaiseError(instanceId, step, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Give every machine a machine-id of its own, before any of them asks for an uplink lease.
|
||||
*
|
||||
* **Every machine in a bed is a clone of one base image, so they all boot with the SAME
|
||||
* `/etc/machine-id`.** systemd's DHCP client derives its client identifier from that file, and the
|
||||
* uplink's dnsmasq keys leases on the client identifier rather than on the MAC — so four machines
|
||||
* with four distinct MACs were all handed *the same address*, and the host kept one ARP entry for
|
||||
* it. Whichever machine last answered an ARP request got everybody's replies.
|
||||
*
|
||||
* This is the fault behind every "the lab's DNS is flaky" run. It does not present as an address
|
||||
* conflict; it presents as name resolution that works two times in three, as pulls that succeed on
|
||||
* a retry, and as one machine out of four being fine — because that machine happened to be holding
|
||||
* the address. It survived an earlier diagnosis that blamed resolver ordering, because reordering
|
||||
* resolvers on a machine that has just won the ARP race does appear to fix it.
|
||||
*
|
||||
* **Ordering is the whole of it, and the first version got it wrong in the other direction.** That
|
||||
* version also restarted networkd and waited for a new lease, which is what you would do to repair
|
||||
* a machine already holding a shared address. Here there is nothing to repair yet: the uplink is
|
||||
* still down at this point and `applyAddresses` is what asks for the lease. So this writes the
|
||||
* identity and stops, and the request that follows is made as somebody.
|
||||
*
|
||||
* Machines without `systemd-machine-id-setup` are left alone; the step is advisory.
|
||||
*/
|
||||
async function distinguishMachines(names: string[], log: (m: string) => void): Promise<void> {
|
||||
for (const name of names) {
|
||||
const said = await incusOk([
|
||||
"exec", name, "--", "sh", "-c",
|
||||
// Regenerated rather than written: `systemd-machine-id-setup` owns the format, and a
|
||||
// hand-made value that is not 32 hex characters is rejected by systemd at next boot.
|
||||
`command -v systemd-machine-id-setup >/dev/null 2>&1 || { echo unchanged; exit 0; }; ` +
|
||||
`rm -f /etc/machine-id && systemd-machine-id-setup >/dev/null 2>&1; ` +
|
||||
`cat /etc/machine-id`,
|
||||
], 60_000);
|
||||
log(` ${name} is ${said?.trim().slice(0, 12) || "unchanged"}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse to go on if two machines took the same uplink address.
|
||||
*
|
||||
* A check rather than a comment, because the failure it guards is invisible where it happens and
|
||||
* unrecognisable where it surfaces. Every machine that declares egress is asked what address it
|
||||
* holds; two the same is a stop, named as what it is.
|
||||
*/
|
||||
async function noTwoMachinesShareAnAddress(
|
||||
scenario: Scenario,
|
||||
byMachine: Map<string, string>,
|
||||
log: (m: string) => void,
|
||||
): Promise<void> {
|
||||
const held = new Map<string, string[]>();
|
||||
for (const [machine, spec] of Object.entries(scenario.machines)) {
|
||||
if (!spec.egress || spec.at === "detached") continue;
|
||||
const name = byMachine.get(machine);
|
||||
if (!name) continue;
|
||||
const said = (await incusOk([
|
||||
"exec", name, "--", "sh", "-c",
|
||||
// The `src` of the default route, NOT its last field — the first version of this took
|
||||
// `$NF` and compared four machines' route METRIC, which is identical by construction and
|
||||
// made the guard fire on every bed. A check that cannot be wrong is worth less than one
|
||||
// that is read carefully once.
|
||||
`ip -4 -o route show default 2>/dev/null | ` +
|
||||
`awk '{for (i = 1; i <= NF; i++) if ($i == "src") { print $(i + 1); exit }}' | head -n1`,
|
||||
], 30_000))?.trim();
|
||||
if (!said) continue;
|
||||
held.set(said, [...(held.get(said) ?? []), machine]);
|
||||
}
|
||||
const shared = [...held.entries()].filter(([, who]) => who.length > 1);
|
||||
if (shared.length === 0) {
|
||||
if (held.size > 0) log(` every machine with egress took an address of its own`);
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`two machines took the SAME uplink address: ` +
|
||||
shared.map(([a, who]) => `${a} held by ${who.join(" and ")}`).join("; ") + `.\n` +
|
||||
` They are clones of one image, so they present one DHCP client identity unless each is ` +
|
||||
`given its own machine-id before the lease is asked for.\n` +
|
||||
` This does not fail as an address conflict. It fails later, as name resolution that works ` +
|
||||
`about two times in three and as pulls that succeed on a retry, because the host holds one ` +
|
||||
`ARP entry and whichever machine answered last receives the replies.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* FOUR FRESH MACHINES, AND NOTHING HANDED TO THEM.
|
||||
*
|
||||
* The four-machine bed (`whole-mesh-full`) proves the mesh converges. It does so by loading
|
||||
* thirty-four of the mesh's own images onto its machines from the workstation, because it does not
|
||||
* build them — something beside the bed built them and copied them in. No real installation looks
|
||||
* like that, and the lab has been burned by exactly this shape before: it used to raise a registry
|
||||
* inside the scenario, and a bootstrap that only worked against that registry went green here and
|
||||
* would have failed on any bare machine.
|
||||
*
|
||||
* This bed hands over nothing. The scenario has no `images:` list at all. What the machines get is
|
||||
* a container runtime and the host binary — prerequisites of a machine, not parts of a mesh — and
|
||||
* from there:
|
||||
*
|
||||
* 1. novox is raised into a mesh of one by the installer, which BUILDS the control plane.
|
||||
* 2. ace, shanks and g14 JOIN it, across a household NAT, with a token and nothing else.
|
||||
* 3. The mesh builds the shared base from source, with its own builder.
|
||||
* 4. The mesh builds a real module standing on that base.
|
||||
* 5. The anchor runs it, pinned to a digest the mesh's own registry assigned.
|
||||
* 6. A JOINED machine runs it — which means pulling from a registry that asks who it is.
|
||||
*
|
||||
* Steps 1 and 2 are proven elsewhere and are here because the later ones need them. **Steps 3
|
||||
* through 6 are what this bed exists for**, and 6 is the one nothing has ever checked: genesis
|
||||
* puts the builder, the registry and everything they produce on ONE machine, so every earlier
|
||||
* proof of a mesh-built module running is a proof about the machine that built it. A second
|
||||
* machine has to fetch, and fetching needs an account nothing yet grants (novox/hq issue 042).
|
||||
*
|
||||
* Each step is recorded separately rather than allowed to throw, so a gap at 6 reports as a gap at
|
||||
* 6 instead of erasing the evidence for 3, 4 and 5.
|
||||
*
|
||||
* MESH_LAB_INCUS='sudo -n incus'
|
||||
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
|
||||
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap
|
||||
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
|
||||
* MESH_LAB_CATALOG=.../mesh-catalog/modules
|
||||
* MESH_LAB_SOURCE=<forge>/mesh-controller.git MESH_LAB_SOURCE_REF=<commit>
|
||||
* MESH_LAB_KEEP=1 to leave it standing afterwards
|
||||
*/
|
||||
import { test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec, push } from "../../src/lifecycle/operate.ts";
|
||||
import { bootstrapBinaryPath, hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
|
||||
import { genesis, type GenesisResult } from "./genesis.ts";
|
||||
|
||||
const SCENARIO = "fresh-mesh";
|
||||
const CONTROL = "anchor";
|
||||
const HOME_NODES = ["home-server", "workstation", "laptop"];
|
||||
/** The joined machine asked to run a mesh-built module. Any of the three would do. */
|
||||
const SECOND = "home-server";
|
||||
|
||||
/** The anchor's public address — what every other machine dials, and what its own token must name. */
|
||||
const ANCHOR = "192.0.2.20";
|
||||
/** Where this mesh's registry answers, on the anchor's public address so a joined node can reach it. */
|
||||
const REGISTRY = `${ANCHOR}:5000`;
|
||||
|
||||
/**
|
||||
* The module built on top of the base, and the base it stands on.
|
||||
*
|
||||
* `amqp-ping` is deliberately small and deliberately REAL: its own TypeScript, compiled by the
|
||||
* shared toolchain, running on the shared runtime, talking to the broker. A module whose artifact
|
||||
* is a mirrored public image would pass every assertion below while skipping the whole of what is
|
||||
* under test (novox/hq SELF-UPGRADE-PLAN, rule 1).
|
||||
*/
|
||||
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
|
||||
/**
|
||||
* What `amqp-ping` requires, and what the foundation does not supply.
|
||||
*
|
||||
* The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a
|
||||
* record of, so it provides nothing to anything. A module asking for `amqp` is asking for a
|
||||
* provider in the graph, and this is it. No build: its image is upstream.
|
||||
*/
|
||||
const PROVIDER = { module: "lavinmq", repo: "mesh-catalog", path: "modules/lavinmq", container: "mesh-lavinmq" };
|
||||
/**
|
||||
* The store, and the catalogue that cannot exist without it.
|
||||
*
|
||||
* Both were missing from this test entirely, and nothing complained, because nothing asked. A mesh
|
||||
* with no catalogue holds no module graph — it cannot say what it has, what a module is made of,
|
||||
* what a change reaches, or what must be rebuilt. It ran anyway, which is the point: "the mesh is
|
||||
* up" was being read off genesis finishing.
|
||||
*/
|
||||
const STORE = { module: "postgres", repo: "mesh-catalog", path: "modules/postgres", container: "mesh-postgres" };
|
||||
const CATALOGUE = { module: "mesh-catalog", repo: "mesh-catalog", path: "modules/mesh-catalog", container: "mesh-catalog" };
|
||||
/** The control plane, rebuilt from its own repository — the step that ends the installer's tenure. */
|
||||
const CONTROL_PLANE = { module: "mesh-controller", repo: "mesh-controller", path: "", container: "mesh-controller" };
|
||||
/** Named once, because the step title is also how later steps say what they waited on. */
|
||||
const NEEDS = "the mesh runs a broker for that module to talk to";
|
||||
const GENESIS = "a bare machine becomes a mesh of one, raised by the installer";
|
||||
const BASE_BUILT = "the mesh builds the shared base from source";
|
||||
const STORE_RUNS = "the mesh builds and runs a store of its own";
|
||||
const CATALOGUE_RUNS = "the mesh builds and runs its own catalogue";
|
||||
const CONTROL_REBUILT = "the mesh rebuilds its own control plane from source";
|
||||
const MODULE_BUILT = "the mesh builds a module standing on that base";
|
||||
const ANCHOR_RUNS = "the anchor runs the module the mesh built";
|
||||
const JOINED = "three machines join the mesh, and reach it over its private network";
|
||||
const SECOND_RUNS = "a joined machine runs the module the mesh built";
|
||||
const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" };
|
||||
|
||||
const capability = await labIsUsable();
|
||||
const binary = hostBinaryPath();
|
||||
const installer = bootstrapBinaryPath();
|
||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
||||
const catalogDir = process.env["MESH_LAB_CATALOG"] ?? "";
|
||||
const source = process.env["MESH_LAB_SOURCE"] ?? "";
|
||||
const sourceRef = process.env["MESH_LAB_SOURCE_REF"] ?? "";
|
||||
const KEEP = !!process.env["MESH_LAB_KEEP"];
|
||||
const FIXED_ID = process.env["MESH_LAB_INSTANCE_ID"] ?? (KEEP ? "fresh-mesh-live" : undefined);
|
||||
|
||||
/**
|
||||
* Where a repository other than the control plane's lives.
|
||||
*
|
||||
* Derived from `MESH_LAB_SOURCE` by swapping the last path segment, because every one of these
|
||||
* repositories sits beside the others under the same owner on the same forge. Overridable, so a
|
||||
* forge that is arranged differently does not need this bed edited.
|
||||
*/
|
||||
function forgeUrl(repo: string): string {
|
||||
const override = process.env[`MESH_LAB_SOURCE_${repo.toUpperCase().replaceAll("-", "_")}`];
|
||||
if (override) return override;
|
||||
return source.replace(/[^/]+\.git$/, `${repo}.git`);
|
||||
}
|
||||
/**
|
||||
* What to build, per repository.
|
||||
*
|
||||
* A branch is acceptable for an ordinary build; only genesis insists on a commit (ADR 0071). Per
|
||||
* repository rather than one value for all of them, because a change under test usually lives in
|
||||
* one repository and the rest should be built from what everyone else has — building them all from
|
||||
* a feature branch would prove that branch against itself.
|
||||
*
|
||||
* MESH_LAB_BUILD_REF the default for every repository
|
||||
* MESH_LAB_BUILD_REF_MESH_CATALOG ...overridden for one
|
||||
*/
|
||||
function refFor(repo: string): string {
|
||||
const override = process.env[`MESH_LAB_BUILD_REF_${repo.toUpperCase().replaceAll("-", "_")}`];
|
||||
return override ?? process.env["MESH_LAB_BUILD_REF"] ?? "main";
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared base's manifest, on this workstation.
|
||||
*
|
||||
* The base is a repository with a manifest at its root (novox/hq ADR 0069), so unlike the
|
||||
* catalogue's modules it is not under `MESH_LAB_CATALOG`. Derived from that path on the convention
|
||||
* that the checkouts sit beside each other, and overridable for a layout where they do not.
|
||||
*/
|
||||
const baseManifest = process.env["MESH_LAB_BASE_MANIFEST"] ??
|
||||
resolve(catalogDir, "..", "..", BASE.repo, "module.json");
|
||||
|
||||
const skip =
|
||||
!capability.usable ? capability.why :
|
||||
!binary ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" :
|
||||
!installer ? "MESH_LAB_BOOTSTRAP_BINARY is not set to a built mesh-bootstrap" :
|
||||
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" :
|
||||
!sourceRef ? "MESH_LAB_SOURCE_REF is not set to the commit to build" :
|
||||
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation template" :
|
||||
!catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
|
||||
false;
|
||||
|
||||
let instanceId = "";
|
||||
let raised: GenesisResult;
|
||||
|
||||
// ---- talking to the machines ------------------------------------------------------------------
|
||||
|
||||
function quote(s: string): string {
|
||||
return `'${s.replaceAll("'", `'\\''`)}'`;
|
||||
}
|
||||
async function on(machine: string, command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> {
|
||||
const { stdout } = await exec(instanceId, machine, [
|
||||
"sh", "-c", `exec 2>&1\n${command}\necho "__exit=$?"`,
|
||||
], timeoutMs);
|
||||
const marker = stdout.lastIndexOf("__exit=");
|
||||
if (marker < 0) return { out: stdout, ok: false };
|
||||
return { out: stdout.slice(0, marker), ok: stdout.slice(marker + 7).trim() === "0" };
|
||||
}
|
||||
async function must(machine: string, command: string, timeoutMs?: number): Promise<string> {
|
||||
const { out, ok } = await on(machine, command, timeoutMs);
|
||||
if (!ok) throw new Error(`${machine}: ${command}\n${out}`);
|
||||
return out;
|
||||
}
|
||||
async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
||||
return must(CONTROL, `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs);
|
||||
}
|
||||
|
||||
/** A module the mesh has to make for itself: where its source is, and what it runs when it works. */
|
||||
interface CoreModule { module: string; repo: string; path: string; container: string }
|
||||
|
||||
/**
|
||||
* Build a module from source, install it, and wait for it to actually run.
|
||||
*
|
||||
* The whole sequence a module goes through, in one place because the mesh has to do it for several
|
||||
* before it is a mesh at all: register the manifest, build it from its repository and path, issue
|
||||
* the broker account its runtime needs, assign it, send it, and then ask the machine whether the
|
||||
* container is up.
|
||||
*
|
||||
* **The account is not optional and is not swallowed.** A module's runtime is a tool host: it
|
||||
* connects to the mesh's broker before doing anything. Without an account the mesh still fills the
|
||||
* secret the module declares it owns — with a generated value — so the container starts, fails to
|
||||
* parse a password as a credential document, and loops on a JSON syntax error mentioning no
|
||||
* missing account. That cost a step that reported PASS.
|
||||
*/
|
||||
async function bringUp(m: CoreModule): Promise<string> {
|
||||
await registerModule(m.module, resolve(catalogDir, m.module, "module.json"));
|
||||
const built = await mesh(
|
||||
`build ${forgeUrl(m.repo)} --path ${m.path} --ref ${refFor(m.repo)} --wait 1200s`, 1_500_000);
|
||||
assert.doesNotMatch(built, /failed/i, built);
|
||||
await mesh(`module issue ${m.module} --node ${CONTROL}`);
|
||||
await mesh(`assign ${CONTROL} ${m.module}`);
|
||||
await mesh(`push ${CONTROL}`, 600_000);
|
||||
return `${built}\n${await waitForContainer(CONTROL, m.container)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for one container, BY NAME, to be running.
|
||||
*
|
||||
* **Named exactly, because substring matching passed a step that had failed.** Waiting for "a
|
||||
* container whose name contains lavinmq" was satisfied by the broker — `lavinmq`, up and healthy —
|
||||
* while the thing actually under test, the module's own runtime `mesh-lavinmq`, was crash-looping
|
||||
* beside it. The step went green and the fault was found by reading `docker ps` by hand.
|
||||
*/
|
||||
async function waitForContainer(node: string, container: string, seconds = 200): Promise<string> {
|
||||
const deadline = Date.now() + seconds * 1_000;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
const ps = (await on(node, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out;
|
||||
last = ps;
|
||||
const line = ps.split("\n").find((l) => l.split("\t")[0]?.trim() === container);
|
||||
if (line && /^Up /.test(line.split("\t")[1]?.trim() ?? "")) return ps;
|
||||
await new Promise((r) => setTimeout(r, 5_000));
|
||||
}
|
||||
const logs = (await on(node, `docker logs --tail 15 ${container} 2>&1`)).out;
|
||||
throw new Error(
|
||||
`${container} is not running on ${node}.\n\ncontainers:\n${last}\n\nwhat it said:\n${logs}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module from a manifest on this workstation.
|
||||
*
|
||||
* **The control plane runs in a container, so a file on the machine is not a file it can open.**
|
||||
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
|
||||
* inside mesh-controller, which is correct and was briefly mistaken for a missing manifest. It is
|
||||
* copied the last step of the way with `docker cp`.
|
||||
*
|
||||
* **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp`
|
||||
* to copy into — `docker cp` says so in those words. `/` is the one directory every image has.
|
||||
*/
|
||||
async function registerModule(module: string, manifest: string): Promise<string> {
|
||||
assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`);
|
||||
const onMachine = `/tmp/${module}.json`;
|
||||
const inContainer = `/${module}.json`;
|
||||
await push(instanceId, CONTROL, manifest, onMachine);
|
||||
await must(CONTROL, `docker cp ${onMachine} mesh-controller:${inContainer}`);
|
||||
return mesh(`module add ${inContainer}`);
|
||||
}
|
||||
function tokenFrom(said: string): string {
|
||||
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" "));
|
||||
assert.ok(found, `no token in:\n${said}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
// ---- steps, recorded rather than thrown --------------------------------------------------------
|
||||
|
||||
interface Step { ok: boolean; why: string; said: string }
|
||||
const steps = new Map<string, Step>();
|
||||
const order: string[] = [];
|
||||
|
||||
/** Run a step, remember what it said, and never throw. A step whose predecessor failed is skipped. */
|
||||
async function step(name: string, after_: string | null, fn: () => Promise<string>): Promise<void> {
|
||||
order.push(name);
|
||||
if (after_ && !steps.get(after_)?.ok) {
|
||||
steps.set(name, { ok: false, why: `not attempted — "${after_}" did not succeed`, said: "" });
|
||||
console.log(`SKIPPED ${name}`);
|
||||
return;
|
||||
}
|
||||
console.log(`\n======== ${name} ========`);
|
||||
try {
|
||||
const said = await fn();
|
||||
steps.set(name, { ok: true, why: "", said });
|
||||
console.log(`OK ${name}`);
|
||||
} catch (err) {
|
||||
const why = (err as Error).message;
|
||||
steps.set(name, { ok: false, why, said: "" });
|
||||
console.log(`FAILED ${name}\n${why.split("\n").slice(0, 25).join("\n")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function report(name: string): string {
|
||||
const s = steps.get(name);
|
||||
if (!s) return `${name}: never ran`;
|
||||
const lines = order.map((n) => {
|
||||
const it = steps.get(n);
|
||||
return ` ${it?.ok ? "PASS" : "FAIL"} ${n}`;
|
||||
});
|
||||
return `${s.why}\n\nWhere this bed got to:\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
if (skip) return;
|
||||
|
||||
const bed = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
||||
onProgress: (m) => console.log(`raise: ${m}`),
|
||||
...(FIXED_ID ? { instanceId: FIXED_ID } : {}),
|
||||
});
|
||||
instanceId = bed.instanceId;
|
||||
console.log(`INSTANCE ${instanceId}${KEEP ? " (KEEP — will be left standing)" : ""}`);
|
||||
console.log(`NOTHING WAS LOADED: this scenario names no images. Every image on every machine ` +
|
||||
`below was pulled from the internet or built by the mesh.`);
|
||||
|
||||
// ---- 1. GENESIS -----------------------------------------------------------------------------
|
||||
//
|
||||
// The shared description, the same one `genesis-single` calls. The bundle is the TEMPLATE with
|
||||
// nothing held: no image is pre-resolved, because none is here to resolve to.
|
||||
await step("a bare machine becomes a mesh of one, raised by the installer", null, async () => {
|
||||
try {
|
||||
raised = await genesis({
|
||||
instanceId,
|
||||
node: CONTROL,
|
||||
installer: installer as string,
|
||||
catalogDir,
|
||||
// The broker's advertised address, corrected.
|
||||
//
|
||||
// The template hardcodes 192.0.2.10:5671 — the address of the anchor in the single-machine
|
||||
// bed. A token carries this verbatim as the endpoint an enrolling node dials, so on a mesh
|
||||
// whose anchor is somewhere else every node, including this one, would enrol against an
|
||||
// address nothing answers on. The installer refuses to guess it and says so, which is
|
||||
// right: it does not know what this machine is called from outside.
|
||||
bundleTemplate: foundationBundle(bundle, []).replaceAll("192.0.2.10:5671", `${ANCHOR}:5671`),
|
||||
registry: REGISTRY,
|
||||
source,
|
||||
sourceRef,
|
||||
toolsSource: forgeUrl(BASE.repo),
|
||||
catalogSource: forgeUrl(MODULE.repo),
|
||||
catalogRef: refFor(MODULE.repo),
|
||||
log: (m) => console.log(m),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`the installer never ran: ${(err as Error).message}`);
|
||||
}
|
||||
if (!raised.ok) throw new Error(`${raised.step || "no step named"}: ${raised.why}\n\n${raised.report.join("\n")}`);
|
||||
return raised.report.join("\n");
|
||||
});
|
||||
|
||||
// ---- 2..6. THE MESH BECOMES ONE --------------------------------------------------------------
|
||||
//
|
||||
// **Joining used to be here, second, and that was the wrong order.** Three machines were enrolled
|
||||
// into a mesh that could not yet produce a single module, and the step was reported as though
|
||||
// something had been shown. They do join — reliably — but joining a mesh that can build nothing
|
||||
// proves only that enrolment works, which was never the doubtful part.
|
||||
//
|
||||
// `17-raising-a-mesh` says it plainly: genesis ends with a mesh of one that RUNS, and that is not
|
||||
// the same as a mesh that WORKS; what remains after the core modules are built is "adding
|
||||
// machines, and deciding what they run". So everything a mesh needs to be a mesh happens first,
|
||||
// and machines arrive at the end.
|
||||
|
||||
// The toolchain and runtime every module with code of its own stands on. It is a module, and it
|
||||
// is built like one — cloned from the forge by the builder installing put here, compiled on the
|
||||
// machine, published into the mesh's own registry.
|
||||
await step(BASE_BUILT, GENESIS, async () => {
|
||||
// Registered from the manifest the builder will also read, so what the mesh holds and what it
|
||||
// builds are the same description of the same module.
|
||||
//
|
||||
// **Not swallowed.** This call used to end in `.catch(() => {})`, on the reasoning that the
|
||||
// base might already be known. It hid a real failure — the manifest was being named at a path
|
||||
// inside a container that had never seen it — and the step passed anyway, because a base with
|
||||
// nothing to stand on builds whether or not the mesh has a record of it.
|
||||
await registerModule(BASE.module, baseManifest);
|
||||
const built = await mesh(
|
||||
`build ${forgeUrl(BASE.repo)} --ref ${refFor(BASE.repo)} --wait 1200s`, 1_500_000);
|
||||
assert.doesNotMatch(built, /failed/i, built);
|
||||
return built;
|
||||
});
|
||||
|
||||
// A store of its own. **Not the foundation's.** The installer raises a store for the control
|
||||
// plane to keep its own records in, the way it raises a broker — plumbing, not a module the mesh
|
||||
// has any record of, so it provides nothing to anything. A module that wants a database wants a
|
||||
// provider in the graph, and the catalogue below is the first thing to want one.
|
||||
await step(STORE_RUNS, BASE_BUILT, () => bringUp(STORE));
|
||||
|
||||
// And the catalogue, which was missing from this test altogether.
|
||||
//
|
||||
// Without it the mesh holds no module graph: it cannot say what it has, what a module is made
|
||||
// of, what a change to one reaches, or what must be rebuilt. A mesh in that state still runs,
|
||||
// which is exactly how its absence went unnoticed — "the mesh is up" was being read off the
|
||||
// installer finishing rather than off the mesh being able to answer anything.
|
||||
await step(CATALOGUE_RUNS, STORE_RUNS, () => bringUp(CATALOGUE));
|
||||
|
||||
// The control plane, rebuilt from its own repository and rolled out.
|
||||
//
|
||||
// The installer built it once, which is what got the mesh running. Building it again THROUGH THE
|
||||
// MODULE PATH — build, notice the version moved, roll it out — is a different claim: it is the
|
||||
// moment the mesh stops depending on the installer for anything, and the first time the thing
|
||||
// that performs an upgrade performs one on itself.
|
||||
await step(CONTROL_REBUILT, CATALOGUE_RUNS, async () => {
|
||||
const built = await mesh(
|
||||
`build ${forgeUrl(CONTROL_PLANE.repo)} --ref ${sourceRef} --wait 1200s`, 1_500_000);
|
||||
assert.doesNotMatch(built, /failed/i, built);
|
||||
const rolled = await mesh(`upgrade ${CONTROL_PLANE.module} roll-out`, 900_000);
|
||||
// Asked of the machine rather than believed from the command: the control plane that answers
|
||||
// afterwards is the one that has to be running for anything below this line to mean anything.
|
||||
await waitForContainer(CONTROL, CONTROL_PLANE.container);
|
||||
return `${built}\n${rolled}`;
|
||||
});
|
||||
|
||||
// ---- 7..8. SOMETHING TO RUN ---------------------------------------------------------------
|
||||
await step(MODULE_BUILT, CONTROL_REBUILT, async () => {
|
||||
await registerModule(MODULE.module, resolve(catalogDir, MODULE.module, "module.json"));
|
||||
const built = await mesh(
|
||||
`build ${forgeUrl(MODULE.repo)} --path ${MODULE.path} --ref ${refFor(MODULE.repo)} --wait 1200s`,
|
||||
1_500_000);
|
||||
assert.doesNotMatch(built, /failed/i, built);
|
||||
// The point of the whole step: what came out is named by a digest this mesh's registry
|
||||
// assigned, not by a placeholder and not by a tag.
|
||||
const builds = await mesh(`builds ${MODULE.module}`);
|
||||
assert.match(builds, /sha256:[0-9a-f]{12}/,
|
||||
`the build recorded no digest — the module is not pinned to anything this registry serves:\n${builds}`);
|
||||
return `${built}\n${builds}`;
|
||||
});
|
||||
|
||||
// `amqp-ping` requires the `amqp` provision, and the mesh refused to place it: "nothing provides
|
||||
// amqp, wanted by amqp-ping". That refusal is correct and is the reason this step exists rather
|
||||
// than the reason to pick an easier module.
|
||||
//
|
||||
// `lavinmq` is that module. It was first added here on the belief that it needed no building —
|
||||
// its broker is an upstream image — and the mesh refused it again: two of its three containers
|
||||
// named a placeholder digest, "which is never a real image". Also right. The broker is upstream,
|
||||
// but the module is not only the broker: it carries a run-once bootstrap that writes the broker's
|
||||
// configuration, a provisioner that grants each consumer its own vhost and user, tools and an
|
||||
// event consumer, all of it its own code.
|
||||
await step(NEEDS, MODULE_BUILT, () => bringUp(PROVIDER));
|
||||
|
||||
// The machine that built it. This is the case every earlier proof covered, and it is here as the
|
||||
// control for the last step: if this fails, that one's failure says nothing about fetching.
|
||||
await step(ANCHOR_RUNS, NEEDS, async () => {
|
||||
await mesh(`module issue ${MODULE.module} --node ${CONTROL}`);
|
||||
await mesh(`assign ${CONTROL} ${MODULE.module}`);
|
||||
await mesh(`push ${CONTROL}`, 600_000);
|
||||
return waitForContainer(CONTROL, MODULE.module);
|
||||
});
|
||||
|
||||
// ---- 9. NOW MACHINES MAY ARRIVE ---------------------------------------------------------------
|
||||
//
|
||||
// Host binary and a token, and nothing else. The anchor is NOT in this loop — the installer
|
||||
// enrolled it, and enrolling it again would offer the mesh a second identity for a node it
|
||||
// already knows.
|
||||
//
|
||||
// And they are placed on the mesh's private network, which the earlier ordering never did. The
|
||||
// mesh refuses to bind a consumer to a provider on another machine when either is missing from
|
||||
// it — "they are not both on the private network" — so without this the last step fails for a
|
||||
// reason that has nothing to do with what it is asking.
|
||||
await step(JOINED, ANCHOR_RUNS, async () => {
|
||||
const said: string[] = [];
|
||||
await mesh(`overlay place ${CONTROL} --hub --endpoint ${ANCHOR}:51820 --site hosting`);
|
||||
for (const machine of HOME_NODES) {
|
||||
await mesh(`node add ${machine}`);
|
||||
const token = tokenFrom(await mesh(`token issue --node ${machine}`));
|
||||
const out = await must(machine, `${HOST_PATH} enrol --token ${quote(token)}`, 180_000);
|
||||
assert.match(out, new RegExp(`enrolled as ${machine}`), out);
|
||||
await must(machine, `nohup ${HOST_PATH} run > /var/log/mesh-host.log 2>&1 & sleep 3`);
|
||||
await mesh(`overlay place ${machine} --site home`);
|
||||
said.push(` ${machine} enrolled, running, and on the private network`);
|
||||
}
|
||||
said.push((await mesh("node list")).trim());
|
||||
said.push((await mesh("overlay show")).trim());
|
||||
return said.join("\n");
|
||||
});
|
||||
|
||||
// ---- 10. AND A MACHINE THAT DID NOT BUILD IT --------------------------------------------------
|
||||
//
|
||||
// **The thing nothing has ever checked.** This machine did not build the image and has never seen
|
||||
// it. To run it, it must fetch it from the mesh's registry — and a joined node has no account
|
||||
// there (novox/hq issue 042), nor any reason to trust a registry serving plain HTTP over the
|
||||
// network (issue 048). Both are open, and both are invisible on a mesh of one.
|
||||
//
|
||||
// Asked anyway, and asked LAST, so that when it fails everything above still stands as evidence
|
||||
// of what does work.
|
||||
await step(SECOND_RUNS, JOINED, async () => {
|
||||
await mesh(`module issue ${MODULE.module} --node ${SECOND}`);
|
||||
await mesh(`assign ${SECOND} ${MODULE.module}`);
|
||||
await mesh(`push ${SECOND}`, 600_000);
|
||||
try {
|
||||
return await waitForContainer(SECOND, MODULE.module);
|
||||
} catch (err) {
|
||||
const log = (await on(SECOND, `tail -40 /var/log/mesh-host.log`)).out;
|
||||
throw new Error(`${(err as Error).message}\n\nwhat the host said:\n${log}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n================ WHAT THIS MESH DID FOR ITSELF ================`);
|
||||
for (const n of order) console.log(` ${steps.get(n)?.ok ? "PASS" : "FAIL"} ${n}`);
|
||||
}, { timeout: 7_200_000 });
|
||||
|
||||
after(async () => {
|
||||
if (KEEP) {
|
||||
console.log(`\nLEFT STANDING: ${instanceId} — not destroyed (MESH_LAB_KEEP).`);
|
||||
return;
|
||||
}
|
||||
if (instanceId) await destroy(instanceId);
|
||||
await destroyAll(`${SCENARIO}-`);
|
||||
}, { timeout: 900_000 });
|
||||
|
||||
for (const name of [
|
||||
"a bare machine becomes a mesh of one, raised by the installer",
|
||||
"three machines join it across the household gateway",
|
||||
"the mesh builds the shared base from source",
|
||||
"the mesh builds a module standing on that base",
|
||||
NEEDS,
|
||||
"the anchor runs the module the mesh built",
|
||||
"a joined machine runs the module the mesh built",
|
||||
]) {
|
||||
test(name, { skip, timeout: 60_000 }, () => {
|
||||
assert.ok(steps.get(name)?.ok, report(name));
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
* MESH_LAB_INCUS='sudo -n incus'
|
||||
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
|
||||
* MESH_LAB_BOOTSTRAP_BINARY=.../mesh-host/mesh-bootstrap
|
||||
* MESH_LAB_BUNDLE=.../mesh-host/examples/substrate-first-node.lock
|
||||
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
|
||||
* MESH_LAB_CATALOG=.../mesh-catalog/modules
|
||||
* MESH_LAB_KEEP=1 to leave it standing afterwards
|
||||
*/
|
||||
@@ -23,7 +23,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy } from "../../src/lifecycle/operate.ts";
|
||||
import { bootstrapBinaryPath, hostBinaryPath } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, substrateBundle } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts";
|
||||
import { genesis, type GenesisResult } from "./genesis.ts";
|
||||
|
||||
const SCENARIO = "genesis-single";
|
||||
@@ -49,7 +49,7 @@ const skip =
|
||||
"bootstrap IMAGE=mesh-builder:development`)" :
|
||||
!source ? "MESH_LAB_SOURCE is not set to the repository the control plane is built from" :
|
||||
!sourceRef ? "MESH_LAB_SOURCE_REF is not set to the commit to build" :
|
||||
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a substrate template" :
|
||||
!bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation template" :
|
||||
!catalogDir || !existsSync(catalogDir) ? "MESH_LAB_CATALOG is not set to mesh-catalog/modules" :
|
||||
false;
|
||||
|
||||
@@ -75,12 +75,15 @@ before(async () => {
|
||||
installer: installer as string,
|
||||
catalogDir,
|
||||
// The template, not a bundle. The store and broker become the references mesh-catalog pins,
|
||||
// and the machine pulls them over its uplink like any first node. mesh-control is left
|
||||
// and the machine pulls them over its uplink like any first node. mesh-controller is left
|
||||
// naming a registry that does not exist — the installer overwrites it, and leaving it proves
|
||||
// that it does.
|
||||
bundleTemplate: substrateBundle(bundle, []),
|
||||
bundleTemplate: foundationBundle(bundle, []),
|
||||
source,
|
||||
sourceRef,
|
||||
// Phase two builds from the same forge; the repositories sit beside the control plane's.
|
||||
toolsSource: source.replace(/[^/]+\.git$/, "mesh-tools.git"),
|
||||
catalogSource: source.replace(/[^/]+\.git$/, "mesh-catalog.git"),
|
||||
log: (m) => console.log(m),
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
+92
-29
@@ -12,10 +12,12 @@
|
||||
* description of genesis, and both the single-node bed and the four-node bed call it.
|
||||
*/
|
||||
import { existsSync, writeFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts";
|
||||
import { placeBootstrap, BOOTSTRAP_PATH, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { placeBootstrap, installHostService, BOOTSTRAP_PATH, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
/** What genesis did, or where it stopped. */
|
||||
export interface GenesisResult {
|
||||
@@ -35,7 +37,7 @@ export interface GenesisOptions {
|
||||
/** A checkout of mesh-catalog's `modules/` on this workstation. */
|
||||
catalogDir: string;
|
||||
/**
|
||||
* The substrate TEMPLATE's content — not a bundle. The installer produces the bundle from it,
|
||||
* The foundation TEMPLATE's content — not a bundle. The installer produces the bundle from it,
|
||||
* replacing the control plane's image with the id of the image it carries.
|
||||
*/
|
||||
bundleTemplate: string;
|
||||
@@ -53,6 +55,31 @@ export interface GenesisOptions {
|
||||
*/
|
||||
source: string;
|
||||
sourceRef: string;
|
||||
/**
|
||||
* Phase two: where the shared base and the catalogue's modules are built from.
|
||||
*
|
||||
* The installer no longer stops at a mesh that runs — it builds the base, a store, the
|
||||
* catalogue, chooses the network and the filter, and asks a human where one must choose. This
|
||||
* bed has no human, so every choice arrives as a flag, and a required choice with no flag is
|
||||
* the installer refusing — which is the behaviour, not a lab problem.
|
||||
*/
|
||||
toolsSource: string;
|
||||
catalogSource: string;
|
||||
/** Where the shared library is built from — published before the base resolves it (ADR 0076). */
|
||||
sdkSource: string;
|
||||
sdkRef?: string;
|
||||
/** What of the base repo to build. Defaults to main; a feature branch under test overrides it. */
|
||||
toolsRef?: string;
|
||||
/** The site the anchor is placed at on the private network. Must match how the operator/test
|
||||
* places it afterwards, or the first re-place changes the overlay and recreates the control plane. */
|
||||
site?: string;
|
||||
/** Supervise the host as a systemd service (the real install path) instead of --host-in-background,
|
||||
* so it survives a reboot. Needs hostBinary to locate the packaging. */
|
||||
hostService?: boolean;
|
||||
/** The built mesh-host binary on this workstation; its repo's packaging/ dir supplies the unit. */
|
||||
hostBinary?: string;
|
||||
/** What of the catalogue to build. A branch under test is the usual reason this is not main. */
|
||||
catalogRef?: string;
|
||||
log?: (m: string) => void;
|
||||
}
|
||||
|
||||
@@ -64,7 +91,7 @@ export function stepIn(said: string): string {
|
||||
export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
|
||||
const node = o.node;
|
||||
const registry = o.registry ?? "127.0.0.1:5000";
|
||||
const modules = o.catalogueModules ?? ["registry", "mesh-control", "builder"];
|
||||
const modules = o.catalogueModules ?? ["distribution", "mesh-controller", "builder"];
|
||||
const catalogueOnMachine = o.catalogueOnMachine ?? "/opt/mesh-catalog";
|
||||
const log = o.log ?? (() => {});
|
||||
|
||||
@@ -98,30 +125,66 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
|
||||
// The catalogue. `mesh-bootstrap --catalog` reads manifests from a CHECKOUT on the machine,
|
||||
// because at this moment the mesh has no forge, no build machine and — until the registry step
|
||||
// finishes — no registry. A manifest is a file, and somebody has to have put it there.
|
||||
await must(`mkdir -p ${modules.map((m) => `${catalogueOnMachine}/modules/${m}`).join(" ")}`);
|
||||
//
|
||||
// **The WHOLE checkout, not the three genesis names.** `--catalog` is documented as a checkout
|
||||
// of the catalogue repository, and phase two reads more from it than genesis does — postgres,
|
||||
// mesh-catalog, the packet filter, whatever extras. Stocking only the bootstrap three left
|
||||
// phase two unable to read a manifest that was never put there (novox/hq installer step 14). The
|
||||
// production equivalent is an operator with a full checkout, or the installer cloning the repo;
|
||||
// the lab stands in for that by copying the whole tree once.
|
||||
const bundleTar = join(tmpdir(), `mesh-lab-catalogue-${process.pid}-${node}.tar`);
|
||||
execFileSync("tar", ["-cf", bundleTar, "-C", o.catalogDir, "."]);
|
||||
await must(`mkdir -p ${catalogueOnMachine}/modules`);
|
||||
await push(o.instanceId, node, bundleTar, "/tmp/catalogue.tar");
|
||||
await must(`tar -xf /tmp/catalogue.tar -C ${catalogueOnMachine}/modules`);
|
||||
// The three genesis itself needs must be present, or the pivot cannot even begin — checked here
|
||||
// rather than discovered at step 8, where the message is about a missing file.
|
||||
for (const module of modules) {
|
||||
const from = resolve(o.catalogDir, module, "module.json");
|
||||
if (!existsSync(from)) return stop("preparing the catalogue", `the catalogue has no ${module}/module.json at ${from}`);
|
||||
await push(o.instanceId, node, from, `${catalogueOnMachine}/modules/${module}/module.json`);
|
||||
const at = `${catalogueOnMachine}/modules/${module}/module.json`;
|
||||
if (!(await on(`test -f ${at}`)).ok) {
|
||||
return stop("preparing the catalogue", `the catalogue has no ${module}/module.json`);
|
||||
}
|
||||
report.push(` catalogue ${modules.join(", ")} at ${catalogueOnMachine}`);
|
||||
}
|
||||
report.push(` catalogue full checkout at ${catalogueOnMachine} (${modules.join(", ")} + phase two)`);
|
||||
|
||||
const local = join(tmpdir(), `mesh-lab-substrate-${process.pid}-${node}.lock`);
|
||||
const local = join(tmpdir(), `mesh-lab-foundation-${process.pid}-${node}.lock`);
|
||||
writeFileSync(local, o.bundleTemplate);
|
||||
await push(o.instanceId, node, local, "/tmp/substrate-template.lock");
|
||||
await push(o.instanceId, node, local, "/tmp/foundation-template.lock");
|
||||
|
||||
// Supervise the host as a service (the real install path) when asked — the only way it survives a
|
||||
// reboot. Installs the shipped packaging in the machine, then lets --host-service start+enable it.
|
||||
if (o.hostService) {
|
||||
if (!o.hostBinary) {
|
||||
return stop("preparing the host service", "hostService needs hostBinary to find the packaging");
|
||||
}
|
||||
await installHostService(name, node, join(dirname(o.hostBinary), "packaging"), (m) => log(`genesis:${m}`));
|
||||
report.push(` host supervised by nox-mesh-host.service`);
|
||||
}
|
||||
|
||||
const command = [
|
||||
BOOTSTRAP_PATH,
|
||||
`--source ${o.source}`,
|
||||
`--source-ref ${o.sourceRef}`,
|
||||
`--bundle /tmp/substrate-template.lock`,
|
||||
`--bundle /tmp/foundation-template.lock`,
|
||||
`--catalog ${catalogueOnMachine}`,
|
||||
`--node ${node}`,
|
||||
`--registry ${registry}`,
|
||||
`--tools-source ${o.toolsSource}`,
|
||||
`--tools-ref ${o.toolsRef ?? "main"}`,
|
||||
`--catalog-source ${o.catalogSource}`,
|
||||
`--catalog-ref ${o.catalogRef ?? "main"}`,
|
||||
`--sdk-source ${o.sdkSource}`,
|
||||
`--sdk-ref ${o.sdkRef ?? "main"}`,
|
||||
// The choices, answered the unattended way. Both have one option today, so these are
|
||||
// redundant on purpose: the day a second filter exists, this bed keeps working instead of
|
||||
// refusing, and choosing becomes a thing it visibly does.
|
||||
`--site ${o.site ?? "main"}`,
|
||||
`--private-network wireguard`,
|
||||
`--packet-filter nftables`,
|
||||
`--host ${HOST_PATH}`,
|
||||
// The lab has no unit to supervise the host with, and the installer refuses to invent one — a
|
||||
// unit file is a packaging decision. A host started this way does not survive a reboot.
|
||||
`--host-in-background`,
|
||||
// A service when the packaging was installed above (survives a reboot); otherwise the
|
||||
// background process, which does not — the installer refuses to invent a unit either way.
|
||||
...(o.hostService ? [] as string[] : [`--host-in-background`]),
|
||||
].join(" ");
|
||||
|
||||
// Run up to three times. Not to paper over a failure — every attempt's failing step is printed —
|
||||
@@ -149,9 +212,9 @@ export async function genesis(o: GenesisOptions): Promise<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/`);
|
||||
@@ -161,17 +224,17 @@ export async function genesis(o: GenesisOptions): Promise<GenesisResult> {
|
||||
|
||||
// 3. THE PIVOT COMPLETED — the running control plane is pinned by a digest THIS MESH'S REGISTRY
|
||||
// assigned, not by an image id (ADR 0067 states this check in as many words).
|
||||
const pinnedTo = (await on(`docker inspect --format '{{.Config.Image}}' mesh-control`)).out.trim();
|
||||
const pinnedTo = (await on(`docker inspect --format '{{.Config.Image}}' mesh-controller`)).out.trim();
|
||||
report.push(` pinned to ${pinnedTo || "(nothing)"}`);
|
||||
if (/^sha256:[0-9a-f]{64}$/.test(pinnedTo)) {
|
||||
return stop("after the last step",
|
||||
`mesh-control is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` +
|
||||
`mesh-controller is running from ${pinnedTo}, which is an IMAGE ID — the digest of the image's ` +
|
||||
`own configuration, which no registry ever served. The pivot did not happen, so this mesh ` +
|
||||
`cannot upgrade itself (novox/hq ADR 0067, "the pivot completed").`);
|
||||
}
|
||||
if (!new RegExp(`^${registry.replaceAll(".", "\\.")}/mesh-control@sha256:[0-9a-f]{64}$`).test(pinnedTo)) {
|
||||
if (!new RegExp(`^${registry.replaceAll(".", "\\.")}/mesh-controller@sha256:[0-9a-f]{64}$`).test(pinnedTo)) {
|
||||
return stop("after the last step",
|
||||
`mesh-control is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` +
|
||||
`mesh-controller is running from ${pinnedTo || "nothing this bed could read"}, which is not a ` +
|
||||
`digest assigned by ${registry}.`);
|
||||
}
|
||||
|
||||
@@ -186,34 +249,34 @@ export async function genesis(o: GenesisOptions): Promise<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)) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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`);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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 ================"];
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user