The launcher already ran `host run`, and `run` on a machine with no identity exited with an error. So a freshly installed host, sitting exactly as intended waiting for somebody to bring it a token, would have counted three failed starts and rolled back its own installation. It waits now, and says what it is waiting for. That is the *hosted* state from the lifecycle: the host is running, it has no identity, and there is nobody to link to. Every machine passes through it. An identity that exists and cannot be read is still a fault rather than a wait. Treating that as "not enrolled yet" would leave a node sitting quietly for ever while the mesh believes it is a member. Also: a node now says it is there once a minute. Nothing but its name, because anything more would be a report, and reports are rare where this is constant -- reading one as the other would make a quiet node look like a stale one. Not published mandatory, unlike a report: losing one is nothing, the next is a minute away, and the mesh reads a gap rather than counting arrivals. Verified in the lab: a node was stopped and the mesh said "out of touch 4m", then it was started and the mesh said "here" again, without anything else being touched.
132 lines
6.4 KiB
Plaintext
132 lines
6.4 KiB
Plaintext
// substrate-first-node.lock — what a machine must be before a mesh exists.
|
|
//
|
|
// Steps 0 to 5 of the bootstrap (novox/hq 03-DESIGN/01-to-be/07-the-substrate.md): a container
|
|
// runtime, a store, a database per context, that context's schema, and the broker.
|
|
//
|
|
// It stops before step 6, where the control plane runs.
|
|
//
|
|
// The broker generates its OWN certificate, in its own image, into a volume it then mounts read
|
|
// only. Self-signed, because at this moment there is no mesh to issue one and no public name to
|
|
// obtain one for -- and it does not matter, because what a joining node checks is the fingerprint
|
|
// pinned in its token, not a chain or a name (novox/hq ADR 0004). The subject is decoration.
|
|
//
|
|
// PINNED BY DIGEST, and the digest is not decoration: a tag can be made to point at a different
|
|
// image, and this file is applied on a machine with no mesh to ask about anything. These digests
|
|
// belong to the registry the lab raises, which is what a real node pulls from anyway — what is
|
|
// required is a reference that is exact and cannot move (novox/hq ADR 0006).
|
|
//
|
|
// The store waits up to three minutes rather than one. A machine that has just pulled the
|
|
// image and is running initdb for the first time can take longer than sixty seconds, and it
|
|
// failed that way three times in the lab -- a flaky bootstrap that a second run always fixed,
|
|
// which is the worst kind because it teaches people to run things twice.
|
|
//
|
|
// The store's data is a NAMED VOLUME, not a directory on the machine. A directory the host
|
|
// creates is owned by root, and the database runs as somebody else inside the container — so it
|
|
// could not write, and the container crash-looped. A named volume lets the image set up its own
|
|
// ownership, and outlives the container, which is what you want for the thing holding the mesh's
|
|
// state.
|
|
{
|
|
"declaration": 1,
|
|
"resources": [
|
|
{
|
|
"id": "container-runtime",
|
|
"type": "package",
|
|
"package": "docker"
|
|
},
|
|
{
|
|
"id": "container-runtime-running",
|
|
"type": "service",
|
|
"unit": "docker.service",
|
|
"state": "running",
|
|
"boot": "enabled"
|
|
},
|
|
{
|
|
"id": "store",
|
|
"type": "container",
|
|
"name": "mesh-store",
|
|
"image": "192.0.2.250:5000/postgres@sha256:7abf537131b66ed5af448d90653abf1679b0c7e9a1f07efdd4c3108a401b259a",
|
|
"env": {
|
|
"POSTGRES_PASSWORD": "bootstrap",
|
|
"PGDATA": "/var/lib/postgresql/data/pgdata"
|
|
},
|
|
"ports": ["127.0.0.1:5432:5432"],
|
|
"volumes": ["mesh-store-data:/var/lib/postgresql/data"]
|
|
},
|
|
{
|
|
"id": "store-ready",
|
|
"type": "action",
|
|
"in": "mesh-store",
|
|
"command": ["sh", "-c", "for i in $(seq 1 180); do pg_isready -U postgres >/dev/null 2>&1 && exit 0; sleep 1; done; exit 1"],
|
|
"verify": ["pg_isready", "-U", "postgres"]
|
|
},
|
|
{
|
|
"id": "inventory-database",
|
|
"type": "action",
|
|
"in": "mesh-store",
|
|
"command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE inventory'"],
|
|
"verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw inventory"]
|
|
},
|
|
{
|
|
"id": "identity-database",
|
|
"type": "action",
|
|
"in": "mesh-store",
|
|
"command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE identity'"],
|
|
"verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw identity"]
|
|
},
|
|
{
|
|
"id": "context-schemas",
|
|
"type": "action",
|
|
"command": ["docker", "run", "--rm", "--network", "container:mesh-store",
|
|
"-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable",
|
|
"-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable",
|
|
"192.0.2.250:5000/mesh-control@sha256:5aaaea5f3d0daa7a4cdde4e13dbddc61176d46487e6a89e6d992808e5e771593",
|
|
"migrate"],
|
|
"verify": ["sh", "-c", "docker exec mesh-store psql -U postgres -d inventory -tAc \"select to_regclass('public.node')\" | grep -qx node && docker exec mesh-store psql -U postgres -d identity -tAc \"select to_regclass('public.signing_key')\" | grep -qx signing_key"]
|
|
},
|
|
{
|
|
"id": "broker-certificate",
|
|
"type": "action",
|
|
"command": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls",
|
|
"192.0.2.250:5000/cloudamqp/lavinmq@sha256:b117c254e6e269a29db479e6b410ca4e46e035b4981e49d24b159673ef09d336",
|
|
"-c", "test -f /tls/tls.crt || (openssl req -x509 -newkey rsa:2048 -nodes -keyout /tls/tls.key -out /tls/tls.crt -days 3650 -subj '/CN=mesh-broker' >/dev/null 2>&1 && chmod 644 /tls/tls.crt && chmod 600 /tls/tls.key)"],
|
|
"verify": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls",
|
|
"192.0.2.250:5000/cloudamqp/lavinmq@sha256:b117c254e6e269a29db479e6b410ca4e46e035b4981e49d24b159673ef09d336",
|
|
"-c", "test -s /tls/tls.crt && openssl x509 -in /tls/tls.crt -noout"]
|
|
},
|
|
{
|
|
"id": "broker",
|
|
"type": "container",
|
|
"name": "mesh-broker",
|
|
"image": "192.0.2.250:5000/cloudamqp/lavinmq@sha256:b117c254e6e269a29db479e6b410ca4e46e035b4981e49d24b159673ef09d336",
|
|
"ports": ["5671:5671", "127.0.0.1:5672:5672", "127.0.0.1:15672:15672"],
|
|
"volumes": ["mesh-broker-data:/var/lib/lavinmq", "mesh-broker-tls:/tls:ro"],
|
|
"args": ["--amqps-port=5671", "--cert=/tls/tls.crt", "--key=/tls/tls.key"]
|
|
},
|
|
{
|
|
"id": "broker-ready",
|
|
"type": "action",
|
|
"in": "mesh-broker",
|
|
"command": ["sh", "-c", "for i in $(seq 1 60); do lavinmqctl status >/dev/null 2>&1 && exit 0; sleep 1; done; exit 1"],
|
|
"verify": ["lavinmqctl", "status"]
|
|
},
|
|
{
|
|
"id": "control-plane",
|
|
"type": "container",
|
|
"name": "mesh-control",
|
|
"image": "192.0.2.250:5000/mesh-control@sha256:5aaaea5f3d0daa7a4cdde4e13dbddc61176d46487e6a89e6d992808e5e771593",
|
|
"network": "host",
|
|
"args": ["serve"],
|
|
"volumes": ["mesh-broker-tls:/broker-tls:ro"],
|
|
"env": {
|
|
"MESH_STORE_INVENTORY": "postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable",
|
|
"MESH_STORE_IDENTITY": "postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable",
|
|
"MESH_BROKER_AMQP": "amqp://guest:guest@127.0.0.1:5672/",
|
|
"MESH_BROKER_MANAGEMENT": "http://guest:guest@127.0.0.1:15672",
|
|
"MESH_BROKER_ADDRESS": "192.0.2.10:5671",
|
|
"MESH_BROKER_CERTIFICATE": "/broker-tls/tls.crt"
|
|
}
|
|
}
|
|
|
|
]
|
|
}
|