Rename mesh-control -> mesh-controller, substrate -> foundation

One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-16 18:40:40 +02:00
parent 01c7730fb3
commit 121367319d
48 changed files with 317 additions and 317 deletions
+8 -8
View File
@@ -36,24 +36,24 @@ test:
go test ./... -count=1 go test ./... -count=1
# A default build carries no bundle and refuses to reconcile, which is the honest state for a # A default build carries no bundle and refuses to reconcile, which is the honest state for a
# host nobody has told what a substrate is. # host nobody has told what a foundation is.
build: build:
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o mesh-host ./cmd/mesh-host CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o mesh-host ./cmd/mesh-host
# A host for a real machine, carrying a real bundle: # A host for a real machine, carrying a real bundle:
# make host SYSTEM=arch BUNDLE=path/to/substrate.lock # make host SYSTEM=arch BUNDLE=path/to/foundation.lock
# #
# The bundle replaces the one for SYSTEM, because its contents are per operating system — # The bundle replaces the one for SYSTEM, because its contents are per operating system —
# package names and unit names differ (novox/hq ADR 0005). # package names and unit names differ (novox/hq ADR 0005).
host: host:
@test -n "$(BUNDLE)" || { echo "BUNDLE= is required; a host with no bundle cannot raise a first node"; exit 1; } @test -n "$(BUNDLE)" || { echo "BUNDLE= is required; a host with no bundle cannot raise a first node"; exit 1; }
@test -f "$(BUNDLE)" || { echo "no such bundle: $(BUNDLE)"; exit 1; } @test -f "$(BUNDLE)" || { echo "no such bundle: $(BUNDLE)"; exit 1; }
@test -f internal/bundle/substrate-$(SYSTEM).lock || { echo "no bundle slot for SYSTEM=$(SYSTEM)"; exit 1; } @test -f internal/bundle/foundation-$(SYSTEM).lock || { echo "no bundle slot for SYSTEM=$(SYSTEM)"; exit 1; }
@cp internal/bundle/substrate-$(SYSTEM).lock internal/bundle/substrate-$(SYSTEM).lock.default @cp internal/bundle/foundation-$(SYSTEM).lock internal/bundle/foundation-$(SYSTEM).lock.default
@cp "$(BUNDLE)" internal/bundle/substrate-$(SYSTEM).lock @cp "$(BUNDLE)" internal/bundle/foundation-$(SYSTEM).lock
@CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o mesh-host ./cmd/mesh-host; \ @CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o mesh-host ./cmd/mesh-host; \
status=$$?; \ status=$$?; \
mv internal/bundle/substrate-$(SYSTEM).lock.default internal/bundle/substrate-$(SYSTEM).lock; \ mv internal/bundle/foundation-$(SYSTEM).lock.default internal/bundle/foundation-$(SYSTEM).lock; \
exit $$status exit $$status
@echo "built for $(SYSTEM) carrying $(BUNDLE)" @echo "built for $(SYSTEM) carrying $(BUNDLE)"
@@ -66,7 +66,7 @@ host:
# answered by ADR 0071: the source comes from a mesh that already exists, which is not the one being # answered by ADR 0071: the source comes from a mesh that already exists, which is not the one being
# raised. What cannot be fetched is the thing that does the fetching, and that is what is carried. # raised. What cannot be fetched is the thing that does the fetching, and that is what is carried.
# #
# The image is BUILT ELSEWHERE and handed over — mesh-control's own `make builder-image` — and # The image is BUILT ELSEWHERE and handed over — mesh-controller's own `make builder-image` — and
# embedded here at release time, the same way carrying the bundle breaks the "copy it onto a machine # embedded here at release time, the same way carrying the bundle breaks the "copy it onto a machine
# and run it" cycle (novox/hq ADR 0005). # and run it" cycle (novox/hq ADR 0005).
# #
@@ -90,7 +90,7 @@ BOOTSTRAP_OUT ?= mesh-bootstrap
bootstrap: bootstrap:
@test -n "$(IMAGE)" || { echo "IMAGE= is required; an installer carrying no builder image cannot raise a mesh"; exit 1; } @test -n "$(IMAGE)" || { echo "IMAGE= is required; an installer carrying no builder image cannot raise a mesh"; exit 1; }
@case "$(IMAGE)" in sha256:*) echo "IMAGE=$(IMAGE) is an image id. The installer identifies the carried image by its tag, because an id is the digest of a configuration that a runtime rewrites as it loads. Pass a name:tag"; exit 1;; esac @case "$(IMAGE)" in sha256:*) echo "IMAGE=$(IMAGE) is an image id. The installer identifies the carried image by its tag, because an id is the digest of a configuration that a runtime rewrites as it loads. Pass a name:tag"; exit 1;; esac
@docker image inspect "$(IMAGE)" >/dev/null 2>&1 || { echo "this machine does not hold $(IMAGE) — build it in mesh-control with 'make image'"; exit 1; } @docker image inspect "$(IMAGE)" >/dev/null 2>&1 || { echo "this machine does not hold $(IMAGE) — build it in mesh-controller with 'make image'"; exit 1; }
@test -n "$$(docker image inspect --format '{{len .RepoTags}}' "$(IMAGE)" | grep -v '^0$$')" || { echo "$(IMAGE) has no repository tag, so the saved archive would carry no name the installer can ask a runtime about. Tag it first: docker tag $(IMAGE) mesh-builder:<version>"; exit 1; } @test -n "$$(docker image inspect --format '{{len .RepoTags}}' "$(IMAGE)" | grep -v '^0$$')" || { echo "$(IMAGE) has no repository tag, so the saved archive would carry no name the installer can ask a runtime about. Tag it first: docker tag $(IMAGE) mesh-builder:<version>"; exit 1; }
@cp internal/image/builder.tar internal/image/builder.tar.placeholder @cp internal/image/builder.tar internal/image/builder.tar.placeholder
@docker save --output internal/image/builder.tar "$(IMAGE)" @docker save --output internal/image/builder.tar "$(IMAGE)"
+10 -10
View File
@@ -111,7 +111,7 @@ the fault this exists to prevent.
A host built for a machine carries its declaration **inside the binary**: A host built for a machine carries its declaration **inside the binary**:
``` ```
make host BUNDLE=path/to/substrate.lock make host BUNDLE=path/to/foundation.lock
``` ```
`mesh-host reconcile` then applies it. That is the first node's path — no mesh present, nothing `mesh-host reconcile` then applies it. That is the first node's path — no mesh present, nothing
@@ -126,21 +126,21 @@ Stages 3 and 4 — the link, and enrolment — are designed and not built.
## What stage 2 does not yet prove ## What stage 2 does not yet prove
The design defines stage 2 as *the host applies `substrate.lock` with no mesh present*, and The design defines stage 2 as *the host applies `foundation.lock` with no mesh present*, and
calls out the claim underneath it: **that one host can raise the substrate alone**. calls out the claim underneath it: **that one host can raise the foundation alone**.
The mechanism is proved — a sealed machine, one binary, and it configures itself from what it The mechanism is proved — a sealed machine, one binary, and it configures itself from what it
carries. The claim is not. The substrate is four container services, and: carries. The claim is not. The foundation is four container services, and:
- the vocabulary has no container type, because a container needs an image and where images - the vocabulary has no container type, because a container needs an image and where images
come from is open ([`novox/hq` research 012](https://git.novox.be/novox/hq)); come from is open ([`novox/hq` research 012](https://git.novox.be/novox/hq));
- what belongs in a substrate is not known — the closure for a one-node mesh is what - what belongs in a foundation is not known — the closure for a one-node mesh is what
research 011 and 012 exist to answer; research 011 and 012 exist to answer;
- and the machine used to test this has no container runtime, because a sealed network cannot - and the machine used to test this has no container runtime, because a sealed network cannot
install one. install one.
So `substrate.lock` here is a real bundle with a placeholder's content. Saying that plainly So `foundation.lock` here is a real bundle with a placeholder's content. Saying that plainly
beats shipping a host that claims a substrate it has never raised. beats shipping a host that claims a foundation it has never raised.
## A capability is detected, never assumed ## A capability is detected, never assumed
@@ -198,7 +198,7 @@ repository carries implementation and does not carry decisions.
## Checks that cross into the control plane's repository ## Checks that cross into the control plane's repository
Two things are agreed between this repository and `novox/mesh-control`, and each is a separate Two things are agreed between this repository and `novox/mesh-controller`, and each is a separate
struct on each side. A field renamed on one of them fails **silently** — the crossing succeeds and struct on each side. A field renamed on one of them fails **silently** — the crossing succeeds and
something is simply absent — so both are checked by handing one side's real output to the other's something is simply absent — so both are checked by handing one side's real output to the other's
real parser. Neither runs by default; each skips with a reason, because a repository that fails real parser. Neither runs by default; each skips with a reason, because a repository that fails
@@ -207,7 +207,7 @@ without its neighbour checked out is a repository nobody can build.
**What the mesh sends, read by this host:** **What the mesh sends, read by this host:**
``` ```
mesh-control: ./build/mesh-control plan <node> --json > /tmp/d.json mesh-controller: ./build/mesh-controller plan <node> --json > /tmp/d.json
mesh-host: MESH_EMITTED=/tmp/d.json go test ./internal/declaration/ -v mesh-host: MESH_EMITTED=/tmp/d.json go test ./internal/declaration/ -v
``` ```
@@ -215,7 +215,7 @@ mesh-host: MESH_EMITTED=/tmp/d.json go test ./internal/declaration/ -v
``` ```
mesh-host: MESH_ENROL_OUT=/tmp/enrol.json go test ./internal/link/ mesh-host: MESH_ENROL_OUT=/tmp/enrol.json go test ./internal/link/
mesh-control: MESH_ENROL=/tmp/enrol.json make check mesh-controller: MESH_ENROL=/tmp/enrol.json make check
``` ```
The second writes the private half of the sealing key beside the request, so the mesh's suite can The second writes the private half of the sealing key beside the request, so the mesh's suite can
+12 -12
View File
@@ -8,11 +8,11 @@
// cannot be folded into it without making that sentence false. Same tier, same repository, // cannot be folded into it without making that sentence false. Same tier, same repository,
// different program. // different program.
// //
// Genesis is a pivot (novox/hq ADR 0067). It raises a substrate whose control plane is named by the // Genesis is a pivot (novox/hq ADR 0067). It raises a foundation whose control plane is named by the
// digest of its own configuration — legal exactly where nothing could have served an image — then // digest of its own configuration — legal exactly where nothing could have served an image — then
// enrols this machine, installs the registry module, pushes that image into it to get the manifest // enrols this machine, installs the registry module, pushes that image into it to get the manifest
// digest it has never had, reinstalls the control plane as an ordinary module pinned to it, and // digest it has never had, reinstalls the control plane as an ordinary module pinned to it, and
// drops the temporary one. Without --catalog it stops after the substrate and says why. // drops the temporary one. Without --catalog it stops after the foundation and says why.
package main package main
import ( import (
@@ -40,8 +40,8 @@ import (
var version = "development build" var version = "development build"
const ( const (
defaultTemplate = "substrate.lock" defaultTemplate = "foundation.lock"
defaultOut = "/var/lib/mesh-host/substrate.lock" defaultOut = "/var/lib/mesh-host/foundation.lock"
defaultRegistry = "127.0.0.1:5000" defaultRegistry = "127.0.0.1:5000"
defaultHost = "/usr/local/bin/mesh-host" defaultHost = "/usr/local/bin/mesh-host"
// The unit this project actually packages, in `packaging/`. It said `mesh-host.service`, which // The unit this project actually packages, in `packaging/`. It said `mesh-host.service`, which
@@ -58,7 +58,7 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
1 preflight what has to be true before anything is changed 1 preflight what has to be true before anything is changed
2 load the builder's image, carried in this installer 2 load the builder's image, carried in this installer
3 build the control plane, from its own repository and a commit 3 build the control plane, from its own repository and a commit
4 bundle the substrate, named for this machine 4 bundle the foundation, named for this machine
5 apply raise it 5 apply raise it
6 verify it is up, and the control plane replies 6 verify it is up, and the control plane replies
7 enrol this machine becomes the mesh's first node 7 enrol this machine becomes the mesh's first node
@@ -75,7 +75,7 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
13 base build the shared toolchain and runtime everything with code 13 base build the shared toolchain and runtime everything with code
stands on stands on
14 store build and install postgres — a database provider, which the 14 store build and install postgres — a database provider, which the
substrate's own store is not foundation's own store is not
15 catalogue build and install the module graph 15 catalogue build and install the module graph
16 network choose the private network (--private-network), place this 16 network choose the private network (--private-network), place this
machine as its hub (--endpoint, --site) machine as its hub (--endpoint, --site)
@@ -83,7 +83,7 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
question is which, not whether question is which, not whether
18 extras anything beyond the floor (--extras) 18 extras anything beyond the floor (--extras)
--bundle the substrate template to build this machine's bundle from --bundle the foundation template to build this machine's bundle from
(default ` + defaultTemplate + `) (default ` + defaultTemplate + `)
--out where the produced bundle is written, for a person to read --out where the produced bundle is written, for a person to read
(default ` + defaultOut + `) (default ` + defaultOut + `)
@@ -131,8 +131,8 @@ the same thing that will maintain it, and the control plane a mesh ends up runni
one it built itself, from a repository and a commit it can name and build again. one it built itself, from a repository and a commit it can name and build again.
Genesis is a pivot: a temporary control plane installs the registry that makes it Genesis is a pivot: a temporary control plane installs the registry that makes it
permanent. The temporary one is called temp-mesh-control and the permanent one is permanent. The temporary one is called temp-mesh-controller and the permanent one is
called mesh-control, so they are two containers with two owners and there is nothing called mesh-controller, so they are two containers with two owners and there is nothing
to hand over. to hand over.
Every step is idempotent: run it again after fixing whatever it named, and the steps Every step is idempotent: run it again after fixing whatever it named, and the steps
@@ -217,11 +217,11 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet {
set := flag.NewFlagSet("mesh-bootstrap", flag.ContinueOnError) set := flag.NewFlagSet("mesh-bootstrap", flag.ContinueOnError)
set.SetOutput(os.Stderr) set.SetOutput(os.Stderr)
set.Usage = func() { fmt.Fprint(os.Stderr, usage) } set.Usage = func() { fmt.Fprint(os.Stderr, usage) }
set.StringVar(&opts.Template, "bundle", opts.Template, "the substrate template to build from") set.StringVar(&opts.Template, "bundle", opts.Template, "the foundation template to build from")
set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written") set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written")
set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied") set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied")
set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue, set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue,
"a checkout of the mesh's catalogue; without it this stops after the substrate") "a checkout of the mesh's catalogue; without it this stops after the foundation")
set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository, set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository,
"the repository the control plane is built from, on a mesh that already exists") "the repository the control plane is built from, on a mesh that already exists")
set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref, set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref,
@@ -367,7 +367,7 @@ func hostname() string {
// //
// Plain HTTP, and only at the mesh's own registry: it is reached over the mesh's private network, // Plain HTTP, and only at the mesh's own registry: it is reached over the mesh's private network,
// which is already the encrypted and authenticated thing, and a second layer inside it would be // which is already the encrypted and authenticated thing, and a second layer inside it would be
// certificates to issue and rotate for no property the first does not have (mesh-control's // certificates to issue and rotate for no property the first does not have (mesh-controller's
// `internal/builder` pushes to it on the same reasoning). // `internal/builder` pushes to it on the same reasoning).
// //
// The body is read with a limit. What is asked for is a status and a short JSON answer, and a // The body is read with a limit. What is asked for is a status and a short JSON answer, and a
+2 -2
View File
@@ -57,7 +57,7 @@ func TestAMistypedFlagIsRefusedNotIgnored(t *testing.T) {
} }
func TestAnUnexpectedArgumentIsRefused(t *testing.T) { func TestAnUnexpectedArgumentIsRefused(t *testing.T) {
if _, _, _, err := parseArgs([]string{"bootstrap", "substrate.lock"}); err == nil { if _, _, _, err := parseArgs([]string{"bootstrap", "foundation.lock"}); err == nil {
t.Fatal("a stray argument was ignored rather than refused — the bundle is --bundle") t.Fatal("a stray argument was ignored rather than refused — the bundle is --bundle")
} }
} }
@@ -141,7 +141,7 @@ func TestThePivotsDefaultsAreTheDocumentedOnes(t *testing.T) {
// be a checkout somebody else made, at whatever commit they left it on — and it decides which // be a checkout somebody else made, at whatever commit they left it on — and it decides which
// image the mesh's control plane is pinned to for ever after. // image the mesh's control plane is pinned to for ever after.
if opts.Catalogue != "" { if opts.Catalogue != "" {
t.Errorf("--catalog defaults to %q; without one the installer stops at the substrate", t.Errorf("--catalog defaults to %q; without one the installer stops at the foundation",
opts.Catalogue) opts.Catalogue)
} }
// The machine's own name, because that is what a person already calls it. // The machine's own name, because that is what a person already calls it.
+1 -1
View File
@@ -823,7 +823,7 @@ func sealOpener(statePath string) apply.Unseal {
// carriedPorts is every machine port held by what this host raised from its own bundle. // carriedPorts is every machine port held by what this host raised from its own bundle.
// //
// **What the mesh must assign around** (novox/hq ADR 0038). The substrate is not a module: a node // **What the mesh must assign around** (novox/hq ADR 0038). The foundation is not a module: a node
// raises it before any mesh exists, so the control plane has never heard of the store or the // raises it before any mesh exists, so the control plane has never heard of the store or the
// broker. Told this, it can put a module somewhere else; not told, it hands out a port one of them // broker. Told this, it can put a module somewhere else; not told, it hands out a port one of them
// holds and finds out from a container runtime. // holds and finds out from a container runtime.
+5 -5
View File
@@ -1,17 +1,17 @@
# Examples # Examples
## `substrate-first-node.lock` ## `foundation-first-node.lock`
What a machine must be before a mesh exists — the bootstrap in What a machine must be before a mesh exists — the bootstrap in
[novox/hq `07-the-substrate.md`](https://git.novox.be/novox/hq), whole: [novox/hq `07-the-foundation.md`](https://git.novox.be/novox/hq), whole:
``` ```
0 a container runtime 0 a container runtime
1 the store runs 1 the store runs
2 a database per context `inventory` and `identity` 2 a database per context `inventory` and `identity`
3 those contexts' schemas mesh-control migrate 3 those contexts' schemas mesh-controller migrate
4 the broker runs with a certificate it generated itself 4 the broker runs with a certificate it generated itself
5 the control plane runs mesh-control serve 5 the control plane runs mesh-controller serve
``` ```
**A machine that applies this is a mesh** — one node, with nothing joined to it yet, which is **A machine that applies this is a mesh** — one node, with nothing joined to it yet, which is
@@ -24,7 +24,7 @@ a comment about what something does not do is a comment nobody updates.
Build a host carrying it: Build a host carrying it:
``` ```
make host SYSTEM=arch BUNDLE=examples/substrate-first-node.lock make host SYSTEM=arch BUNDLE=examples/foundation-first-node.lock
``` ```
**The registry address and digests have to be replaced before this is useful.** They are written **The registry address and digests have to be replaced before this is useful.** They are written
+9 -9
View File
@@ -1,6 +1,6 @@
// Package examples checks the bundles shipped in this directory. // Package examples checks the bundles shipped in this directory.
// //
// **Nothing checked them before.** `substrate-first-node.lock` is what a machine becomes when // **Nothing checked them before.** `foundation-first-node.lock` is what a machine becomes when
// there is no mesh to ask — the one declaration applied with nothing to verify it against — and // there is no mesh to ask — the one declaration applied with nothing to verify it against — and
// it was edited by hand and read by nobody but a running host. // it was edited by hand and read by nobody but a running host.
package examples package examples
@@ -16,7 +16,7 @@ import (
func bundle(t *testing.T) *declaration.Declaration { func bundle(t *testing.T) *declaration.Declaration {
t.Helper() t.Helper()
raw, err := os.ReadFile("substrate-first-node.lock") raw, err := os.ReadFile("foundation-first-node.lock")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -27,12 +27,12 @@ func bundle(t *testing.T) *declaration.Declaration {
return d return d
} }
// Defends novox/hq ADR 0028: the substrate supplies the control plane and nothing else. // Defends novox/hq ADR 0028: the foundation supplies the control plane and nothing else.
// //
// The object store was substrate for months on the strength of "it cannot grant itself a bucket", // The object store was foundation for months on the strength of "it cannot grant itself a bucket",
// which answers half the test. The control plane never needed one, and nothing noticed because // which answers half the test. The control plane never needed one, and nothing noticed because
// nothing counted what the bundle holds. // nothing counted what the bundle holds.
func TestTheBundleCarriesTheSubstrateAndTheControlPlaneAndNothingElse(t *testing.T) { func TestTheBundleCarriesTheFoundationAndTheControlPlaneAndNothingElse(t *testing.T) {
var images []string var images []string
for _, r := range bundle(t).Resources { for _, r := range bundle(t).Resources {
c, ok := r.(*declaration.Container) c, ok := r.(*declaration.Container)
@@ -48,7 +48,7 @@ func TestTheBundleCarriesTheSubstrateAndTheControlPlaneAndNothingElse(t *testing
} }
sort.Strings(images) sort.Strings(images)
want := []string{"lavinmq", "mesh-control", "postgres"} want := []string{"lavinmq", "mesh-controller", "postgres"}
if strings.Join(images, ",") != strings.Join(want, ",") { if strings.Join(images, ",") != strings.Join(want, ",") {
t.Fatalf("the bundle carries %v; expected exactly %v.\n\n"+ t.Fatalf("the bundle carries %v; expected exactly %v.\n\n"+
"Adding one is a change to what every first node becomes, and to ADR 0006's "+ "Adding one is a change to what every first node becomes, and to ADR 0006's "+
@@ -57,13 +57,13 @@ func TestTheBundleCarriesTheSubstrateAndTheControlPlaneAndNothingElse(t *testing
} }
// The control plane is in the bundle, and the design overlooked it once by reasoning about // The control plane is in the bundle, and the design overlooked it once by reasoning about
// substrate services rather than counting containers (novox/hq 03-DESIGN/01-to-be/07). // foundation services rather than counting containers (novox/hq 03-DESIGN/01-to-be/07).
func TestTheControlPlaneIsCarriedToo(t *testing.T) { func TestTheControlPlaneIsCarriedToo(t *testing.T) {
for _, r := range bundle(t).Resources { for _, r := range bundle(t).Resources {
if c, ok := r.(*declaration.Container); ok && strings.Contains(c.Image, "mesh-control") { if c, ok := r.(*declaration.Container); ok && strings.Contains(c.Image, "mesh-controller") {
return return
} }
} }
t.Fatal("nothing in the bundle starts the control plane, so the machine would raise a " + t.Fatal("nothing in the bundle starts the control plane, so the machine would raise a " +
"substrate and stop") "foundation and stop")
} }
@@ -1,6 +1,6 @@
// substrate-first-node.lock — what a machine must be before a mesh exists. // foundation-first-node.lock — what a machine must be before a mesh exists.
// //
// The whole bootstrap (novox/hq 03-DESIGN/01-to-be/07-the-substrate.md): a container runtime, a // The whole bootstrap (novox/hq 03-DESIGN/01-to-be/07-the-foundation.md): a container runtime, a
// store, a database per context, those contexts' schemas, the broker, and the control plane // store, a database per context, those contexts' schemas, the broker, and the control plane
// running on top of them. // running on top of them.
// //
@@ -85,7 +85,7 @@
}, },
// Each context owns its own database (novox/hq ADR 0008). A third one is a third database, // Each context owns its own database (novox/hq ADR 0008). A third one is a third database,
// created the same way and named the same way — which is the whole of adding a context to the // created the same way and named the same way — which is the whole of adding a context to the
// bootstrap, and is why the count is not something the substrate has an opinion about. // bootstrap, and is why the count is not something the foundation has an opinion about.
{ {
"id": "licences-database", "id": "licences-database",
"type": "action", "type": "action",
@@ -100,7 +100,7 @@
"-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable", "-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", "-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable",
"-e", "MESH_STORE_LICENCES=postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable", "-e", "MESH_STORE_LICENCES=postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable",
"192.0.2.250:5000/mesh-control@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace",
"migrate"], "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 && docker exec mesh-store psql -U postgres -d licences -tAc \"select to_regclass('public.licence')\" | grep -qx licence"] "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 && docker exec mesh-store psql -U postgres -d licences -tAc \"select to_regclass('public.licence')\" | grep -qx licence"]
}, },
@@ -133,8 +133,8 @@
{ {
"id": "control-plane", "id": "control-plane",
"type": "container", "type": "container",
"name": "mesh-control", "name": "mesh-controller",
"image": "192.0.2.250:5000/mesh-control@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", "image": "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace",
"network": "host", "network": "host",
"args": ["serve"], "args": ["serve"],
"volumes": ["mesh-broker-tls:/broker-tls:ro"], "volumes": ["mesh-broker-tls:/broker-tls:ro"],
+1 -1
View File
@@ -459,7 +459,7 @@ func TestForgettingAUnitThatIsGoneDoesNotStrandTheNode(t *testing.T) {
} }
} }
// --- package, container and action (novox/hq 07-the-substrate.md, ADR 0006, ADR 0005) --- // --- package, container and action (novox/hq 07-the-foundation.md, ADR 0006, ADR 0005) ---
func parseTrusted(t *testing.T, raw string) *declaration.Declaration { func parseTrusted(t *testing.T, raw string) *declaration.Declaration {
t.Helper() t.Helper()
+4 -4
View File
@@ -15,7 +15,7 @@ import (
// Runner is the same runner every applier in this repository takes. // Runner is the same runner every applier in this repository takes.
type Runner = apply.Runner type Runner = apply.Runner
// ApplyBundle raises the substrate, through the host's own apply. // ApplyBundle raises the foundation, through the host's own apply.
// //
// **This calls `internal/apply` rather than running the `mesh-host` binary**, and that is worth // **This calls `internal/apply` rather than running the `mesh-host` binary**, and that is worth
// stating because shelling out would have been easier. The installer and the host must apply a // stating because shelling out would have been easier. The installer and the host must apply a
@@ -25,7 +25,7 @@ type Runner = apply.Runner
// raising, which would make the installer depend on the thing it installs. // raising, which would make the installer depend on the thing it installs.
// //
// It applies under `store.OriginCarried`, which is the same origin `mesh-host reconcile` uses and // It applies under `store.OriginCarried`, which is the same origin `mesh-host reconcile` uses and
// is not a detail: what the substrate raised must be invisible to the removal pass of a // is not a detail: what the foundation raised must be invisible to the removal pass of a
// declaration that later arrives from the control plane, or the first thing the mesh tells this // declaration that later arrives from the control plane, or the first thing the mesh tells this
// node would tear down the mesh (novox/hq 04-ISSUES/010). // node would tear down the mesh (novox/hq 04-ISSUES/010).
// //
@@ -71,12 +71,12 @@ func ApplyBundle(ctx context.Context, o Options, sys system.System, d *declarati
// //
// It cannot happen and it is refused with a sentence rather than a nil dereference. A sealing key // It cannot happen and it is refused with a sentence rather than a nil dereference. A sealing key
// is generated at enrolment (`internal/identity`), and enrolment is something that happens on a // is generated at enrolment (`internal/identity`), and enrolment is something that happens on a
// mesh — which is the thing this program is raising. A substrate bundle carrying a sealed file // mesh — which is the thing this program is raising. A foundation bundle carrying a sealed file
// would be a bundle written for a node that has already joined. // would be a bundle written for a node that has already joined.
func refuseSealed(string) ([]byte, error) { func refuseSealed(string) ([]byte, error) {
return nil, errors.New( return nil, errors.New(
"this bundle contains a file sealed to a node's key, and a machine that has not enrolled " + "this bundle contains a file sealed to a node's key, and a machine that has not enrolled " +
"has no such key. A substrate is applied before any mesh exists, so it can carry no " + "has no such key. A foundation is applied before any mesh exists, so it can carry no " +
"secret the mesh sealed") "secret the mesh sealed")
} }
+3 -3
View File
@@ -78,12 +78,12 @@ func TestASystemNobodyHasBuiltIsRefusedByName(t *testing.T) {
} }
} }
// A substrate is applied before any mesh exists, so it can carry no secret the mesh sealed — there // A foundation is applied before any mesh exists, so it can carry no secret the mesh sealed — there
// is no key to open one with. Refused with a sentence rather than a nil dereference. // is no key to open one with. Refused with a sentence rather than a nil dereference.
func TestASealedFileInASubstrateIsRefusedWithAReason(t *testing.T) { func TestASealedFileInAFoundationIsRefusedWithAReason(t *testing.T) {
_, err := refuseSealed("anything") _, err := refuseSealed("anything")
if err == nil { if err == nil {
t.Fatal("a sealed file in a substrate bundle was accepted") t.Fatal("a sealed file in a foundation bundle was accepted")
} }
if !strings.Contains(err.Error(), "has not enrolled") { if !strings.Contains(err.Error(), "has not enrolled") {
t.Errorf("the refusal does not say why there is no key: %v", err) t.Errorf("the refusal does not say why there is no key: %v", err)
+16 -16
View File
@@ -111,7 +111,7 @@ func failed(step Step, err error) error {
// Options are the things that differ between machines. // Options are the things that differ between machines.
type Options struct { type Options struct {
// Template is the substrate bundle this machine's own bundle is made from. // Template is the foundation bundle this machine's own bundle is made from.
Template string Template string
// Out is where the produced bundle is written, so a person can read what was applied. // Out is where the produced bundle is written, so a person can read what was applied.
Out string Out string
@@ -128,12 +128,12 @@ type Options struct {
// runtime, a control plane opening its stores. // runtime, a control plane opening its stores.
Wait time.Duration Wait time.Duration
// Node is the name this machine is known by in the mesh. Everything after the substrate names // Node is the name this machine is known by in the mesh. Everything after the foundation names
// it: the record, the token, the assignment, the push. // it: the record, the token, the assignment, the push.
Node string Node string
// Catalogue is a checkout of the mesh's catalogue repository, which is where the registry's and // Catalogue is a checkout of the mesh's catalogue repository, which is where the registry's and
// the control plane's manifests are read from. Empty stops the installer after the substrate: // the control plane's manifests are read from. Empty stops the installer after the foundation:
// there is no pivot without manifests, and pretending otherwise would leave a machine that // there is no pivot without manifests, and pretending otherwise would leave a machine that
// looks installed and cannot upgrade itself. // looks installed and cannot upgrade itself.
Catalogue string Catalogue string
@@ -181,7 +181,7 @@ type Options struct {
Extras []string Extras []string
} }
// pivots reports whether this run goes past the substrate. // pivots reports whether this run goes past the foundation.
func (o Options) pivots() bool { return strings.TrimSpace(o.Catalogue) != "" } func (o Options) pivots() bool { return strings.TrimSpace(o.Catalogue) != "" }
// Deps are the ways this program reaches outside itself. Injected so the whole of it can be // Deps are the ways this program reaches outside itself. Injected so the whole of it can be
@@ -245,11 +245,11 @@ type Result struct {
Applied int `json:"applied,omitempty"` Applied int `json:"applied,omitempty"`
Changed bool `json:"changed,omitempty"` Changed bool `json:"changed,omitempty"`
// Running is the substrate's containers, confirmed up. // Running is the foundation's containers, confirmed up.
Running []string `json:"running,omitempty"` Running []string `json:"running,omitempty"`
// Answered is what the temporary control plane said back — not merely that it is up. // Answered is what the temporary control plane said back — not merely that it is up.
Answered string `json:"temporary-control-plane,omitempty"` Answered string `json:"temporary-control-plane,omitempty"`
// Temporary is what the substrate's control plane is called, which is not what the module's is. // Temporary is what the foundation's control plane is called, which is not what the module's is.
Temporary string `json:"temporary-container,omitempty"` Temporary string `json:"temporary-container,omitempty"`
// Node is this machine's name in the mesh, and how it came to be enrolled and heard from. // Node is this machine's name in the mesh, and how it came to be enrolled and heard from.
@@ -289,15 +289,15 @@ type Result struct {
// answer to it is to run this again: re-running is the retry, and it is one a person chooses after // answer to it is to run this again: re-running is the retry, and it is one a person chooses after
// reading which step failed and why. // reading which step failed and why.
// //
// **Genesis is a pivot** (novox/hq ADR 0067). Steps 1 to 5 raise a substrate whose control plane is // **Genesis is a pivot** (novox/hq ADR 0067). Steps 1 to 5 raise a foundation whose control plane is
// named by the digest of its own configuration, because nothing has ever served that image and // named by the digest of its own configuration, because nothing has ever served that image and
// nothing could have. Steps 6 to 10 turn that into a mesh that can maintain itself: this machine // nothing could have. Steps 6 to 10 turn that into a mesh that can maintain itself: this machine
// enrols, the registry module is installed, the carried image is pushed INTO that registry — which // enrols, the registry module is installed, the carried image is pushed INTO that registry — which
// gives it a manifest digest, its first — and the control plane is reinstalled as an ordinary // gives it a manifest digest, its first — and the control plane is reinstalled as an ordinary
// module pinned to it. The temporary one is then dropped from the bundle and the host removes it. // module pinned to it. The temporary one is then dropped from the bundle and the host removes it.
// //
// **What makes the last part expressible is a name.** The substrate's control plane is called // **What makes the last part expressible is a name.** The foundation's control plane is called
// `temp-mesh-control` and the module's is called `mesh-control`. Two containers, two owners: // `temp-mesh-controller` and the module's is called `mesh-controller`. Two containers, two owners:
// nothing is handed over, nothing has to stop being owned without being destroyed, and destruction // nothing is handed over, nothing has to stop being owned without being destroyed, and destruction
// by omission is the right end for something named "temp". // by omission is the right end for something named "temp".
// //
@@ -309,7 +309,7 @@ type Result struct {
// be fixed remotely — so no step may leave one: // be fixed remotely — so no step may leave one:
// //
// 1–3 nothing on the machine but a written file. Re-run: the bundle is produced again. // 1–3 nothing on the machine but a written file. Re-run: the bundle is produced again.
// 4 a partly-raised substrate, recorded in the state file. Re-run: apply converges the rest. // 4 a partly-raised foundation, recorded in the state file. Re-run: apply converges the rest.
// 5 everything up; something did not answer yet. Re-run: it is asked again. // 5 everything up; something did not answer yet. Re-run: it is asked again.
// 6 a node record and possibly a spent token. Re-run: `node list` finds the record, the // 6 a node record and possibly a spent token. Re-run: `node list` finds the record, the
// identity file says whether this machine enrolled, and a fresh token is issued if not. // identity file says whether this machine enrolled, and a fresh token is issued if not.
@@ -458,7 +458,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
o.Out, rewritten.Resources)) o.Out, rewritten.Resources))
// ---- 4. apply ----------------------------------------------------------------------- // ---- 4. apply -----------------------------------------------------------------------
say("apply — raising the substrate") say("apply — raising the foundation")
report, err := ApplyBundle(ctx, o, sys, rewritten.Declaration, d.Run, say) report, err := ApplyBundle(ctx, o, sys, rewritten.Declaration, d.Run, say)
result.Applied, result.Changed = len(report.Outcomes), report.Changed() result.Applied, result.Changed = len(report.Outcomes), report.Changed()
if err != nil { if err != nil {
@@ -472,7 +472,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
} }
// ---- 5. verify ---------------------------------------------------------------------- // ---- 5. verify ----------------------------------------------------------------------
say("verify — the substrate is up, and the control plane replies") say("verify — the foundation is up, and the control plane replies")
verified, err := Verify(ctx, rewritten.Declaration, d.Run, o.Timeout, o.Wait, say) verified, err := Verify(ctx, rewritten.Declaration, d.Run, o.Timeout, o.Wait, say)
result.Running, result.Answered = verified.Running, verified.Answered result.Running, result.Answered = verified.Running, verified.Answered
if err != nil { if err != nil {
@@ -484,7 +484,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
// control plane is named by an image id, which no registry serves, so nothing can ever // control plane is named by an image id, which no registry serves, so nothing can ever
// replace it with a newer one. That is the whole of what the pivot fixes, and it needs // replace it with a newer one. That is the whole of what the pivot fixes, and it needs
// manifests, and manifests come from a checkout somebody has to point this at. // manifests, and manifests come from a checkout somebody has to point this at.
result.Stopped = "no --catalog was given, so this stopped at the substrate. " + result.Stopped = "no --catalog was given, so this stopped at the foundation. " +
"The control plane is named by the digest of its own configuration and no registry " + "The control plane is named by the digest of its own configuration and no registry " +
"serves it, so this mesh cannot yet upgrade itself. Run again with " + "serves it, so this mesh cannot yet upgrade itself. Run again with " +
"--catalog <a checkout of the mesh's catalogue> to finish the pivot; every step " + "--catalog <a checkout of the mesh's catalogue> to finish the pivot; every step " +
@@ -497,7 +497,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
// ---- 6. enrol ------------------------------------------------------------------------- // ---- 6. enrol -------------------------------------------------------------------------
// //
// From here on the mesh is being told things, and the way to tell it anything is to run its // From here on the mesh is being told things, and the way to tell it anything is to run its
// own binary inside its own container. `temporary` is the substrate's control plane; the // own binary inside its own container. `temporary` is the foundation's control plane; the
// module's is a different container with a different name and does not exist yet. // module's is a different container with a different name and does not exist yet.
temporary := controlPlane{container: rewritten.TempName, run: d.Run, timeout: o.Timeout} temporary := controlPlane{container: rewritten.TempName, run: d.Run, timeout: o.Timeout}
@@ -588,9 +588,9 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
} }
// ---- 14. store ------------------------------------------------------------------------ // ---- 14. store ------------------------------------------------------------------------
// A database PROVIDER. The substrate's store is the control plane's own memory and offers // A database PROVIDER. The foundation's store is the control plane's own memory and offers
// nothing to anything; the first thing that wants a database is the catalogue, next. // nothing to anything; the first thing that wants a database is the catalogue, next.
say("store — a database provider, which the substrate's own store is not") say("store — a database provider, which the foundation's own store is not")
if err := InstallFromCatalogue(ctx, o, permanentControl, "postgres", say); err != nil { if err := InstallFromCatalogue(ctx, o, permanentControl, "postgres", say); err != nil {
return result, failed(StepStore, err) return result, failed(StepStore, err)
} }
+2 -2
View File
@@ -24,7 +24,7 @@ func TestAnInstallerWithNothingToBuildRefuses(t *testing.T) {
// A repository without a commit refuses too, because a branch is somebody else's moving target. // A repository without a commit refuses too, because a branch is somebody else's moving target.
func TestABranchIsNotACommit(t *testing.T) { func TestABranchIsNotACommit(t *testing.T) {
err := Source{Repository: "https://example.invalid/mesh-control.git"}.Check() err := Source{Repository: "https://example.invalid/mesh-controller.git"}.Check()
if err == nil { if err == nil {
t.Fatal("a source with no ref was accepted; genesis would have built whatever a branch pointed at") t.Fatal("a source with no ref was accepted; genesis would have built whatever a branch pointed at")
} }
@@ -35,7 +35,7 @@ func TestABranchIsNotACommit(t *testing.T) {
// And a repository with a commit is enough. // And a repository with a commit is enough.
func TestARepositoryAndACommitIsEnough(t *testing.T) { func TestARepositoryAndACommitIsEnough(t *testing.T) {
if err := (Source{Repository: "https://example.invalid/mesh-control.git", Ref: "a1b2c3d4"}).Check(); err != nil { if err := (Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}).Check(); err != nil {
t.Fatalf("a repository and a commit were refused: %v", err) t.Fatalf("a repository and a commit were refused: %v", err)
} }
} }
+20 -20
View File
@@ -14,7 +14,7 @@ import (
// storeFileSuffix is how a manifest asks for a store connection in a file rather than in the // storeFileSuffix is how a manifest asks for a store connection in a file rather than in the
// environment. // environment.
// //
// `MESH_STORE_<CONTEXT>` is what the control plane reads (mesh-control's `internal/store`.Variable) // `MESH_STORE_<CONTEXT>` is what the control plane reads (mesh-controller's `internal/store`.Variable)
// and putting a password in a container's environment puts it in `docker inspect` for ever. So a // and putting a password in a container's environment puts it in `docker inspect` for ever. So a
// module manifest names a file per context and points at it with `…_FILE`; the mesh seals the value // module manifest names a file per context and points at it with `…_FILE`; the mesh seals the value
// into that file on the machine, and nothing but the process reads it. // into that file on the machine, and nothing but the process reads it.
@@ -41,20 +41,20 @@ type Permanent struct {
// **The host performs the replacement, not the control plane** (novox/hq ADR 0067). The temporary // **The host performs the replacement, not the control plane** (novox/hq ADR 0067). The temporary
// control plane composes a declaration naming the registry-pinned image, publishes it, and this // control plane composes a declaration naming the registry-pinned image, publishes it, and this
// node's host creates the container. Nothing is asked to replace itself while running, which is // node's host creates the container. Nothing is asked to replace itself while running, which is
// what makes the whole thing expressible: the container being created is called `mesh-control` and // what makes the whole thing expressible: the container being created is called `mesh-controller` and
// the one composing it is called `temp-mesh-control`, so there are two of them and neither is in // the one composing it is called `temp-mesh-controller`, so there are two of them and neither is in
// the other's way. // the other's way.
// //
// **The store connections are the substrate's, made at genesis, and the mesh cannot invent them.** // **The store connections are the foundation's, made at genesis, and the mesh cannot invent them.**
// Every other secret in a mesh is one the mesh made; these existed before the mesh did — they are // Every other secret in a mesh is one the mesh made; these existed before the mesh did — they are
// the credentials the substrate bundle created the databases with. Generating replacements would // the credentials the foundation bundle created the databases with. Generating replacements would
// put thirty-two random bytes where a working connection string has to be, and the control plane // put thirty-two random bytes where a working connection string has to be, and the control plane
// would come up unable to open a single context. So they go in through `secret accept`, which is // would come up unable to open a single context. So they go in through `secret accept`, which is
// exactly the path for a value the mesh must carry and could not have invented — and they are read // exactly the path for a value the mesh must carry and could not have invented — and they are read
// out of the bundle this installer produced rather than reconstructed, because the bundle is what // out of the bundle this installer produced rather than reconstructed, because the bundle is what
// created them and a second opinion about what a DSN should say is a second chance to be wrong. // created them and a second opinion about what a DSN should say is a second chance to be wrong.
func InstallControlPlane(ctx context.Context, o Options, d Deps, control controlPlane, func InstallControlPlane(ctx context.Context, o Options, d Deps, control controlPlane,
substrate *declaration.Declaration, image string, say func(string)) (Permanent, error) { foundation *declaration.Declaration, image string, say func(string)) (Permanent, error) {
out := Permanent{Image: image} out := Permanent{Image: image}
@@ -63,7 +63,7 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
return out, fmt.Errorf( return out, fmt.Errorf(
"%w\n"+ "%w\n"+
"This is the manifest that makes the control plane an ordinary module. Without it "+ "This is the manifest that makes the control plane an ordinary module. Without it "+
"the machine keeps the temporary control plane the substrate raised, which works "+ "the machine keeps the temporary control plane the foundation raised, which works "+
"and cannot be upgraded — so the install stops here rather than pretending to "+ "and cannot be upgraded — so the install stops here rather than pretending to "+
"have pivoted", err) "have pivoted", err)
} }
@@ -92,7 +92,7 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
} }
// The connections, before the push that would otherwise deliver random bytes for them. // The connections, before the push that would otherwise deliver random bytes for them.
delivered, err := deliverStores(ctx, o, control, pinned, substrate, say) delivered, err := deliverStores(ctx, o, control, pinned, foundation, say)
out.Delivered = delivered out.Delivered = delivered
if err != nil { if err != nil {
return out, err return out, err
@@ -107,7 +107,7 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
} }
// And it answers, which is the same question step 5 asked of the temporary one and for the // And it answers, which is the same question step 5 asked of the temporary one and for the
// same reason: `status` opens all three stores, so a reply proves the sealed connections it // same reason: `status` opens all three stores, so a reply proves the sealed connections it
// was given are the ones the substrate made. Asked of the NEW container — this is the only // was given are the ones the foundation made. Asked of the NEW container — this is the only
// moment in the program where two control planes are running, and asking the wrong one would // moment in the program where two control planes are running, and asking the wrong one would
// report the temporary one's health as the permanent one's. // report the temporary one's health as the permanent one's.
answered, err := waitForTheControlPlane(ctx, control.run, o.Timeout, o.Wait, container, say) answered, err := waitForTheControlPlane(ctx, control.run, o.Timeout, o.Wait, container, say)
@@ -142,7 +142,7 @@ func pinImage(manifest []byte, reference string) ([]byte, int, error) {
} }
// The reference the registry gave back is `<registry>/<repository>@sha256:…`, and what the // The reference the registry gave back is `<registry>/<repository>@sha256:…`, and what the
// manifest holds is `<something>@sha256:0…0`. Replacing only the digest would leave the // manifest holds is `<something>@sha256:0…0`. Replacing only the digest would leave the
// manifest's own repository name in front of it — which may be `mesh-control` with no // manifest's own repository name in front of it — which may be `mesh-controller` with no
// registry, and a runtime would then pull it from the internet. The whole reference moves. // registry, and a runtime would then pull it from the internet. The whole reference moves.
var out bytes.Buffer var out bytes.Buffer
rest := manifest rest := manifest
@@ -215,21 +215,21 @@ func controlPlaneResourceIn(manifest []byte) string {
return "" return ""
} }
// deliverStores carries the substrate's own database connections into the module. // deliverStores carries the foundation's own database connections into the module.
// //
// The pairing is read from the manifest rather than assumed, so that whatever the catalogue calls // The pairing is read from the manifest rather than assumed, so that whatever the catalogue calls
// these secrets is what is delivered. The installer does not guess that the secret holding the // these secrets is what is delivered. The installer does not guess that the secret holding the
// inventory connection is called `inventory`; it follows the manifest from the variable to the // inventory connection is called `inventory`; it follows the manifest from the variable to the
// secret, and a manifest whose two ends do not meet is refused rather than half-delivered. // secret, and a manifest whose two ends do not meet is refused rather than half-delivered.
// //
// **What is delivered is what the substrate already has, and only that.** The mesh generates an // **What is delivered is what the foundation already has, and only that.** The mesh generates an
// own-secret nobody supplied, which is right for something coming into existence and wrong for // own-secret nobody supplied, which is right for something coming into existence and wrong for
// something that already exists. So every variable the module fills from a secret is looked up in // something that already exists. So every variable the module fills from a secret is looked up in
// the substrate's control plane: what it names is accepted, what it does not is left for the mesh // the foundation's control plane: what it names is accepted, what it does not is left for the mesh
// to make. A store connection missing from the substrate is the one exception and is an error — // to make. A store connection missing from the foundation is the one exception and is an error —
// a control plane that cannot open a context is not a control plane. // a control plane that cannot open a context is not a control plane.
func deliverStores(ctx context.Context, o Options, control controlPlane, manifest []byte, func deliverStores(ctx context.Context, o Options, control controlPlane, manifest []byte,
substrate *declaration.Declaration, say func(string)) ([]string, error) { foundation *declaration.Declaration, say func(string)) ([]string, error) {
wanted, err := secretsByVariableIn(manifest) wanted, err := secretsByVariableIn(manifest)
if err != nil { if err != nil {
@@ -246,7 +246,7 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
ControlPlaneModule, storeVariablePrefix, storeVariablePrefix, storeFileSuffix) ControlPlaneModule, storeVariablePrefix, storeVariablePrefix, storeFileSuffix)
} }
temporary, err := controlPlaneIn(substrate) temporary, err := controlPlaneIn(foundation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -260,13 +260,13 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
return delivered, fmt.Errorf( return delivered, fmt.Errorf(
"the %s module wants %s and the bundle this installer produced does not name "+ "the %s module wants %s and the bundle this installer produced does not name "+
"one.\n"+ "one.\n"+
"That connection is the substrate's, created at genesis — the mesh cannot "+ "That connection is the foundation's, created at genesis — the mesh cannot "+
"invent it and the installer will not guess at one", "invent it and the installer will not guess at one",
ControlPlaneModule, variable) ControlPlaneModule, variable)
} }
// Not something the substrate made. The mesh generates its own, which is exactly what // Not something the foundation made. The mesh generates its own, which is exactly what
// an own-secret is for; said so that nothing about the delivery is silent. // an own-secret is for; said so that nothing about the delivery is silent.
say(" the mesh will make " + secret + " — the substrate names no " + variable) say(" the mesh will make " + secret + " — the foundation names no " + variable)
continue continue
} }
@@ -282,7 +282,7 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
return delivered, err return delivered, err
} }
delivered = append(delivered, secret) delivered = append(delivered, secret)
say(" accepted " + secret + " — " + variable + ", as the substrate made it") say(" accepted " + secret + " — " + variable + ", as the foundation made it")
} }
return delivered, nil return delivered, nil
} }
+49 -49
View File
@@ -9,37 +9,37 @@ import (
// Step 9 is where the control plane stops being a special case. These tests defend the two things // Step 9 is where the control plane stops being a special case. These tests defend the two things
// that could go wrong quietly: pinning it to the wrong image, and delivering it store connections // that could go wrong quietly: pinning it to the wrong image, and delivering it store connections
// the mesh invented rather than the ones the substrate actually made. // the mesh invented rather than the ones the foundation actually made.
// theControlPlaneModule is the catalogue's manifest, trimmed to what this installer reads. // theControlPlaneModule is the catalogue's manifest, trimmed to what this installer reads.
// //
// A fixture rather than the file itself, unlike the substrate example the rewrite tests use: the // A fixture rather than the file itself, unlike the foundation example the rewrite tests use: the
// catalogue is a different repository on a different branch, and a test that read it would pass or // catalogue is a different repository on a different branch, and a test that read it would pass or
// fail according to what somebody else had checked out. What it must stay faithful to is the // fail according to what somebody else had checked out. What it must stay faithful to is the
// SHAPE — the placeholder digest, the own-secret per context, the mount from the machine's path to // SHAPE — the placeholder digest, the own-secret per context, the mount from the machine's path to
// the container's, and the environment file that fills what is not a path. // the container's, and the environment file that fills what is not a path.
const theControlPlaneModule = `{ const theControlPlaneModule = `{
"module": "mesh-control", "module": "mesh-controller",
"version": "1", "version": "1",
"slug": "control", "slug": "control",
"capabilities": ["container-runtime"], "capabilities": ["container-runtime"],
"claims": [{"name": "the-control-plane", "scope": "mesh"}], "claims": [{"name": "the-controller", "scope": "mesh"}],
"own-secrets": { "own-secrets": {
"inventory": "/var/lib/mesh/mesh-control/inventory", "inventory": "/var/lib/mesh/mesh-controller/inventory",
"identity": "/var/lib/mesh/mesh-control/identity", "identity": "/var/lib/mesh/mesh-controller/identity",
"licences": "/var/lib/mesh/mesh-control/licences", "licences": "/var/lib/mesh/mesh-controller/licences",
"broker": "/var/lib/mesh/mesh-control/broker", "broker": "/var/lib/mesh/mesh-controller/broker",
"broker-management": "/var/lib/mesh/mesh-control/broker-management" "broker-management": "/var/lib/mesh/mesh-controller/broker-management"
}, },
"resources": [ "resources": [
{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"}, {"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},
{"id": "broker-env", "type": "file", "path": "/var/lib/mesh/mesh-control/broker.env", {"id": "broker-env", "type": "file", "path": "/var/lib/mesh/mesh-controller/broker.env",
"mode": "0600", "mode": "0600",
"content": "MESH_BROKER_AMQP=${secret:broker}\nMESH_BROKER_MANAGEMENT=${secret:broker-management}\nMESH_BROKER_ADDRESS=${machine:at}:5671\n"}, "content": "MESH_BROKER_AMQP=${secret:broker}\nMESH_BROKER_MANAGEMENT=${secret:broker-management}\nMESH_BROKER_ADDRESS=${machine:at}:5671\n"},
{"id": "server", "type": "container", "name": "mesh-control", {"id": "server", "type": "container", "name": "mesh-controller",
"image": "mesh-control@` + placeholderDigest + `", "image": "mesh-controller@` + placeholderDigest + `",
"network": "host", "args": ["serve"], "network": "host", "args": ["serve"],
"env-file": ["/var/lib/mesh/mesh-control/broker.env"], "env-file": ["/var/lib/mesh/mesh-controller/broker.env"],
"env": { "env": {
"MESH_STORE_INVENTORY_FILE": "/run/secrets/inventory", "MESH_STORE_INVENTORY_FILE": "/run/secrets/inventory",
"MESH_STORE_IDENTITY_FILE": "/run/secrets/identity", "MESH_STORE_IDENTITY_FILE": "/run/secrets/identity",
@@ -48,18 +48,18 @@ const theControlPlaneModule = `{
}, },
"volumes": [ "volumes": [
"mesh-broker-tls:/broker-tls:ro", "mesh-broker-tls:/broker-tls:ro",
"/var/lib/mesh/mesh-control/inventory:/run/secrets/inventory:ro", "/var/lib/mesh/mesh-controller/inventory:/run/secrets/inventory:ro",
"/var/lib/mesh/mesh-control/identity:/run/secrets/identity:ro", "/var/lib/mesh/mesh-controller/identity:/run/secrets/identity:ro",
"/var/lib/mesh/mesh-control/licences:/run/secrets/licences:ro" "/var/lib/mesh/mesh-controller/licences:/run/secrets/licences:ro"
]} ]}
] ]
}` }`
const pushedReference = "127.0.0.1:5000/mesh-control@sha256:" + const pushedReference = "127.0.0.1:5000/mesh-controller@sha256:" +
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
// **The whole reference moves, not only the digest.** The manifest's placeholder names a // **The whole reference moves, not only the digest.** The manifest's placeholder names a
// repository too, and replacing sixty-four zeros inside it would leave `mesh-control@sha256:…` // repository too, and replacing sixty-four zeros inside it would leave `mesh-controller@sha256:…`
// with no registry in front — which a runtime would go to the internet for, and this mesh's // with no registry in front — which a runtime would go to the internet for, and this mesh's
// control plane exists in no public registry by design. // control plane exists in no public registry by design.
func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) { func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
@@ -73,7 +73,7 @@ func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
if !strings.Contains(string(pinned), `"image": "`+pushedReference+`"`) { if !strings.Contains(string(pinned), `"image": "`+pushedReference+`"`) {
t.Errorf("the manifest does not name the pushed image:\n%s", pinned) t.Errorf("the manifest does not name the pushed image:\n%s", pinned)
} }
if strings.Contains(string(pinned), `"mesh-control@sha256:`) { if strings.Contains(string(pinned), `"mesh-controller@sha256:`) {
t.Errorf("the digest was replaced and the manifest's own repository name was left in "+ t.Errorf("the digest was replaced and the manifest's own repository name was left in "+
"front of it, so nothing says which registry serves it:\n%s", pinned) "front of it, so nothing says which registry serves it:\n%s", pinned)
} }
@@ -95,10 +95,10 @@ func TestAManifestAlreadyPinnedByHandIsRefused(t *testing.T) {
// otherwise be left half pinned, and fail inside an apply rather than here. // otherwise be left half pinned, and fail inside an apply rather than here.
func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) { func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
twice := strings.Replace(theControlPlaneModule, twice := strings.Replace(theControlPlaneModule,
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"},`, `{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},`,
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"}, `{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},
{"id": "migrate", "type": "container", "name": "mesh-control-migrate", "run-once": true, {"id": "migrate", "type": "container", "name": "mesh-controller-migrate", "run-once": true,
"image": "mesh-control@`+placeholderDigest+`", "args": ["migrate"]},`, 1) "image": "mesh-controller@`+placeholderDigest+`", "args": ["migrate"]},`, 1)
pinned, places, err := pinImage([]byte(twice), pushedReference) pinned, places, err := pinImage([]byte(twice), pushedReference)
if err != nil { if err != nil {
@@ -112,8 +112,8 @@ func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
} }
} }
// **The connections are the substrate's, and they are read out of the bundle that made them.** // **The connections are the foundation's, and they are read out of the bundle that made them.**
// The mesh cannot invent them: they are the credentials the substrate created the databases with, // The mesh cannot invent them: they are the credentials the foundation created the databases with,
// and thirty-two random bytes in their place would leave the control plane unable to open a single // and thirty-two random bytes in their place would leave the control plane unable to open a single
// context. The pairing is read from the manifest so that whatever the catalogue calls these // context. The pairing is read from the manifest so that whatever the catalogue calls these
// secrets is what is delivered. // secrets is what is delivered.
@@ -141,38 +141,38 @@ func TestTheStoreConnectionsComeFromTheBundleThatMadeThem(t *testing.T) {
t.Error("the address the mesh composes from the machine was treated as a secret") t.Error("the address the mesh composes from the machine was treated as a secret")
} }
// The values are the substrate's own, taken from the produced bundle rather than composed. // The values are the foundation's own, taken from the produced bundle rather than composed.
rewritten, err := Rewrite(theRealBundle(t), held) rewritten, err := Rewrite(theRealBundle(t), held)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
runtime := &asked{answer: aMeshThatAgrees(nil)} runtime := &asked{answer: aMeshThatAgrees(nil)}
control := controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second} control := controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second}
delivered, err := deliverStores(context.Background(), Options{Node: "anchor"}, control, delivered, err := deliverStores(context.Background(), Options{Node: "anchor"}, control,
[]byte(theControlPlaneModule), rewritten.Declaration, func(string) {}) []byte(theControlPlaneModule), rewritten.Declaration, func(string) {})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Three stores and both halves of the broker: everything the substrate made and nothing else. // Three stores and both halves of the broker: everything the foundation made and nothing else.
if len(delivered) != 5 { if len(delivered) != 5 {
t.Fatalf("%d values were delivered, and the substrate names five: %v", t.Fatalf("%d values were delivered, and the foundation names five: %v",
len(delivered), delivered) len(delivered), delivered)
} }
for _, secret := range delivered { for _, secret := range delivered {
if !runtime.ran("secret accept anchor mesh-control " + secret + " --from") { if !runtime.ran("secret accept anchor mesh-controller " + secret + " --from") {
t.Errorf("%s was not accepted through `secret accept`: %v", secret, runtime.commands) t.Errorf("%s was not accepted through `secret accept`: %v", secret, runtime.commands)
} }
} }
} }
// A secret the substrate did not make is left for the mesh to make, and said so. Every other // A secret the foundation did not make is left for the mesh to make, and said so. Every other
// secret in a mesh is one the mesh made; `secret accept` is only for what predates the mesh. // secret in a mesh is one the mesh made; `secret accept` is only for what predates the mesh.
func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) { func TestASecretTheFoundationNeverMadeIsLeftToTheMesh(t *testing.T) {
extra := strings.Replace(theControlPlaneModule, extra := strings.Replace(theControlPlaneModule,
`"broker": "/var/lib/mesh/mesh-control/broker",`, `"broker": "/var/lib/mesh/mesh-controller/broker",`,
`"broker": "/var/lib/mesh/mesh-control/broker", `"broker": "/var/lib/mesh/mesh-controller/broker",
"something-new": "/var/lib/mesh/mesh-control/something-new",`, 1) "something-new": "/var/lib/mesh/mesh-controller/something-new",`, 1)
extra = strings.Replace(extra, extra = strings.Replace(extra,
`"content": "MESH_BROKER_AMQP=${secret:broker}\n`, `"content": "MESH_BROKER_AMQP=${secret:broker}\n`,
`"content": "MESH_SOMETHING_NEW=${secret:something-new}\nMESH_BROKER_AMQP=${secret:broker}\n`, 1) `"content": "MESH_SOMETHING_NEW=${secret:something-new}\nMESH_BROKER_AMQP=${secret:broker}\n`, 1)
@@ -182,7 +182,7 @@ func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
runtime := &asked{answer: aMeshThatAgrees(nil)} runtime := &asked{answer: aMeshThatAgrees(nil)}
control := controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second} control := controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second}
var said []string var said []string
delivered, err := deliverStores(context.Background(), Options{Node: "anchor"}, control, delivered, err := deliverStores(context.Background(), Options{Node: "anchor"}, control,
@@ -192,7 +192,7 @@ func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) {
} }
for _, secret := range delivered { for _, secret := range delivered {
if secret == "something-new" { if secret == "something-new" {
t.Error("a value the substrate never made was accepted as though it had") t.Error("a value the foundation never made was accepted as though it had")
} }
} }
if !strings.Contains(strings.Join(said, "\n"), "the mesh will make something-new") { if !strings.Contains(strings.Join(said, "\n"), "the mesh will make something-new") {
@@ -205,8 +205,8 @@ func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) {
// be — which presents as a control plane that will not start, three steps from the cause. // be — which presents as a control plane that will not start, three steps from the cause.
func TestAConnectionFileNothingWritesIsRefused(t *testing.T) { func TestAConnectionFileNothingWritesIsRefused(t *testing.T) {
mismatched := strings.Replace(theControlPlaneModule, mismatched := strings.Replace(theControlPlaneModule,
`"inventory": "/var/lib/mesh/mesh-control/inventory",`, `"inventory": "/var/lib/mesh/mesh-controller/inventory",`,
`"inventory": "/var/lib/mesh/mesh-control/somewhere-else",`, 1) `"inventory": "/var/lib/mesh/mesh-controller/somewhere-else",`, 1)
_, err := secretsByVariableIn([]byte(mismatched)) _, err := secretsByVariableIn([]byte(mismatched))
if err == nil { if err == nil {
@@ -226,11 +226,11 @@ func TestAManifestWantingNoStoresIsRefusedWithTheShapeItShouldHave(t *testing.T)
t.Fatal(err) t.Fatal(err)
} }
runtime := &asked{answer: aMeshThatAgrees(nil)} runtime := &asked{answer: aMeshThatAgrees(nil)}
control := controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second} control := controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second}
bare := `{"module":"mesh-control","version":"1","resources":[ bare := `{"module":"mesh-controller","version":"1","resources":[
{"id":"container","type":"container","name":"mesh-control", {"id":"container","type":"container","name":"mesh-controller",
"image":"mesh-control@` + placeholderDigest + `"}]}` "image":"mesh-controller@` + placeholderDigest + `"}]}`
_, err = deliverStores(context.Background(), Options{Node: "anchor"}, control, _, err = deliverStores(context.Background(), Options{Node: "anchor"}, control,
[]byte(bare), rewritten.Declaration, func(string) {}) []byte(bare), rewritten.Declaration, func(string) {})
@@ -244,13 +244,13 @@ func TestAManifestWantingNoStoresIsRefusedWithTheShapeItShouldHave(t *testing.T)
// The permanent control plane is asked a question, not merely looked at — the same question the // The permanent control plane is asked a question, not merely looked at — the same question the
// temporary one was asked at step 5, and for the same reason: `status` opens all three stores, so // temporary one was asked at step 5, and for the same reason: `status` opens all three stores, so
// a reply proves the sealed connections it was given are the ones the substrate made. // a reply proves the sealed connections it was given are the ones the foundation made.
func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) { func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) {
runtime := &asked{answer: aMeshThatAgrees(map[string]string{ runtime := &asked{answer: aMeshThatAgrees(map[string]string{
"module list": "", "module list": "",
"exec mesh-control /mesh-control": "1 node, 0 waiting\n", "exec mesh-controller /mesh-controller": "1 node, 0 waiting\n",
})} })}
control := controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second} control := controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second}
rewritten, err := Rewrite(theRealBundle(t), held) rewritten, err := Rewrite(theRealBundle(t), held)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -265,11 +265,11 @@ func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) {
if out.Answered != "1 node, 0 waiting" { if out.Answered != "1 node, 0 waiting" {
t.Errorf("the permanent control plane's reply is reported as %q", out.Answered) t.Errorf("the permanent control plane's reply is reported as %q", out.Answered)
} }
if !runtime.ran("docker exec mesh-control " + controlPlaneBinary + " status") { if !runtime.ran("docker exec mesh-controller " + controlPlaneBinary + " status") {
t.Errorf("the permanent control plane was never asked anything: %v", runtime.commands) t.Errorf("the permanent control plane was never asked anything: %v", runtime.commands)
} }
// And the module was registered with the digest, not with the placeholder. // And the module was registered with the digest, not with the placeholder.
if !runtime.ran("module add /mesh-control-module.json") { if !runtime.ran("module add /mesh-controller-module.json") {
t.Errorf("the module was never registered: %v", runtime.commands) t.Errorf("the module was never registered: %v", runtime.commands)
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// hereIs what `node list` says about a machine the mesh has heard from recently. // hereIs what `node list` says about a machine the mesh has heard from recently.
// //
// mesh-control prints one of three words per node: "here", "never spoken", or "out of touch <age>". // mesh-controller prints one of three words per node: "here", "never spoken", or "out of touch <age>".
// The installer waits for the first, and it is the only honest proof that the host agent is // The installer waits for the first, and it is the only honest proof that the host agent is
// running: an enrolled machine whose host is not running looks exactly like an enrolled machine // running: an enrolled machine whose host is not running looks exactly like an enrolled machine
// whose host has crashed, and both look exactly like a successful install until the first push // whose host has crashed, and both look exactly like a successful install until the first push
+6 -6
View File
@@ -70,7 +70,7 @@ func TestAMachineThatHasAlreadyEnrolledIsNotEnrolledAgain(t *testing.T) {
out, err := Enrol(context.Background(), Options{ out, err := Enrol(context.Background(), Options{
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second, Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
Host: "/usr/local/bin/mesh-host", HostInBackground: true, Host: "/usr/local/bin/mesh-host", HostInBackground: true,
}, arch(t), controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, }, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -109,7 +109,7 @@ func TestAMeshThatHasHeardFromAMachineWithNoAgentStartsOne(t *testing.T) {
out, err := Enrol(context.Background(), Options{ out, err := Enrol(context.Background(), Options{
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second, Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
Host: "/usr/local/bin/mesh-host", HostInBackground: true, Host: "/usr/local/bin/mesh-host", HostInBackground: true,
}, arch(t), controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, }, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -135,7 +135,7 @@ func TestAMachineEnrolledUnderAnotherNameIsRefused(t *testing.T) {
_, err := Enrol(context.Background(), Options{ _, err := Enrol(context.Background(), Options{
Node: "anchor", State: alreadyEnrolled(t, "somewhere-else"), Timeout: time.Second, Node: "anchor", State: alreadyEnrolled(t, "somewhere-else"), Timeout: time.Second,
}, arch(t), controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, }, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err == nil { if err == nil {
t.Fatal("a machine already enrolled as something else was enrolled again") t.Fatal("a machine already enrolled as something else was enrolled again")
@@ -173,7 +173,7 @@ func TestAHostThatIsRunningAndUnheardOfIsNotAnInstall(t *testing.T) {
_, err := Enrol(context.Background(), Options{ _, err := Enrol(context.Background(), Options{
Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service", Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service",
Timeout: time.Second, Wait: 0, Timeout: time.Second, Wait: 0,
}, arch(t), controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, }, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err == nil { if err == nil {
t.Fatal("a node the mesh has never heard from was reported enrolled and running") t.Fatal("a node the mesh has never heard from was reported enrolled and running")
@@ -202,7 +202,7 @@ func TestAMachineWithNoHostServiceIsRefusedRatherThanGivenOne(t *testing.T) {
_, err := Enrol(context.Background(), Options{ _, err := Enrol(context.Background(), Options{
Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service", Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service",
Timeout: time.Second, Wait: 0, Timeout: time.Second, Wait: 0,
}, arch(t), controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, }, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err == nil { if err == nil {
t.Fatal("a machine with no host service was reported as having a running host") t.Fatal("a machine with no host service was reported as having a running host")
@@ -222,7 +222,7 @@ func TestAMachineWithNoNameIsRefusedBeforeAnythingIsAsked(t *testing.T) {
return "", fmt.Errorf("nothing should have been asked") return "", fmt.Errorf("nothing should have been asked")
}} }}
_, err := Enrol(context.Background(), Options{Timeout: time.Second}, arch(t), _, err := Enrol(context.Background(), Options{Timeout: time.Second}, arch(t),
controlPlane{container: "temp-mesh-control", run: runtime.run}, func(string) {}) controlPlane{container: "temp-mesh-controller", run: runtime.run}, func(string) {})
if err == nil { if err == nil {
t.Fatal("a machine with no name was enrolled") t.Fatal("a machine with no name was enrolled")
} }
+1 -1
View File
@@ -120,7 +120,7 @@ func loadImage(ctx context.Context, run Runner, saved []byte, dryRun bool, say f
// Through a file rather than through stdin: the runner this repository shares runs a command // Through a file rather than through stdin: the runner this repository shares runs a command
// and captures its output, and giving it a second mouth for one caller would change every // and captures its output, and giving it a second mouth for one caller would change every
// applier's contract for the sake of one step (internal/apply's Runner). // applier's contract for the sake of one step (internal/apply's Runner).
tarball, err := os.CreateTemp("", "mesh-control-*.tar") tarball, err := os.CreateTemp("", "mesh-controller-*.tar")
if err != nil { if err != nil {
return loaded, fmt.Errorf("nowhere to put the carried image while loading it: %w", err) return loaded, fmt.Errorf("nowhere to put the carried image while loading it: %w", err)
} }
+8 -8
View File
@@ -41,12 +41,12 @@ func (a *asked) ran(fragment string) bool {
return false return false
} }
// savedImageFixture builds what `docker save` produces, tagged `mesh-control:test` unless a test // savedImageFixture builds what `docker save` produces, tagged `mesh-controller:test` unless a test
// asks for something else. Pass no tags for an archive saved without one. // asks for something else. Pass no tags for an archive saved without one.
func savedImageFixture(t *testing.T, digest string, tags ...string) []byte { func savedImageFixture(t *testing.T, digest string, tags ...string) []byte {
t.Helper() t.Helper()
if tags == nil { if tags == nil {
tags = []string{"mesh-control:test"} tags = []string{"mesh-controller:test"}
} }
entries, err := json.Marshal([]struct { entries, err := json.Marshal([]struct {
Config string Config string
@@ -75,7 +75,7 @@ func savedImageFixture(t *testing.T, digest string, tags ...string) []byte {
// fixtureDigest is what the ARCHIVE calls the image, and runtimeDigest is what a runtime calls it // fixtureDigest is what the ARCHIVE calls the image, and runtimeDigest is what a runtime calls it
// after loading the same bytes. They differ on purpose, because they differ in reality: an image // after loading the same bytes. They differ on purpose, because they differ in reality: an image
// id is the digest of the image's configuration, and a runtime rewrites that configuration as it // id is the digest of the image's configuration, and a runtime rewrites that configuration as it
// loads. Measured on a live raise, `mesh-control:development` was `sha256:b86bb81c…` on the // loads. Measured on a live raise, `mesh-controller:development` was `sha256:b86bb81c…` on the
// workstation that saved it and `sha256:2dc21904…` on the machine that loaded it. // workstation that saved it and `sha256:2dc21904…` on the machine that loaded it.
const ( const (
fixtureDigest = "3333333333333333333333333333333333333333333333333333333333333333" fixtureDigest = "3333333333333333333333333333333333333333333333333333333333333333"
@@ -107,7 +107,7 @@ func TestTheIdComesFromTheRuntimeAndNotFromTheArchive(t *testing.T) {
// Loaded — and stored under a configuration of the runtime's own making. // Loaded — and stored under a configuration of the runtime's own making.
return "sha256:" + runtimeDigest + "\n", nil return "sha256:" + runtimeDigest + "\n", nil
case len(args) > 0 && args[0] == "load": case len(args) > 0 && args[0] == "load":
return "Loaded image: mesh-control:test\n", nil return "Loaded image: mesh-controller:test\n", nil
} }
return "", fmt.Errorf("unexpected command: %v", args) return "", fmt.Errorf("unexpected command: %v", args)
} }
@@ -128,7 +128,7 @@ func TestTheIdComesFromTheRuntimeAndNotFromTheArchive(t *testing.T) {
t.Error("a real run reported its id as a prediction") t.Error("a real run reported its id as a prediction")
} }
// The runtime was asked BY THE TAG, which is the only name that survives the transfer. // The runtime was asked BY THE TAG, which is the only name that survives the transfer.
if !runtime.ran("docker image inspect --format {{.Id}} mesh-control:test") { if !runtime.ran("docker image inspect --format {{.Id}} mesh-controller:test") {
t.Errorf("the runtime was never asked what the tag resolves to: %v", runtime.commands) t.Errorf("the runtime was never asked what the tag resolves to: %v", runtime.commands)
} }
// And the difference is said out loud, or somebody comparing this against `docker images` on // And the difference is said out loud, or somebody comparing this against `docker images` on
@@ -183,7 +183,7 @@ func TestALoadThatLeftNothingBehindIsAFailure(t *testing.T) {
if len(args) > 1 && args[0] == "image" && args[1] == "inspect" { if len(args) > 1 && args[0] == "image" && args[1] == "inspect" {
return "", errors.New("Error: No such image") return "", errors.New("Error: No such image")
} }
return "Loaded image: mesh-control:test\n", nil return "Loaded image: mesh-controller:test\n", nil
}} }}
_, err := loadImage(context.Background(), runtime.run, _, err := loadImage(context.Background(), runtime.run,
@@ -192,7 +192,7 @@ func TestALoadThatLeftNothingBehindIsAFailure(t *testing.T) {
t.Fatal("a load that left nothing on the machine was reported as success") t.Fatal("a load that left nothing on the machine was reported as success")
} }
// Named by the tag, because that is what was asked about and what is missing. // Named by the tag, because that is what was asked about and what is missing.
if !strings.Contains(err.Error(), "mesh-control:test") { if !strings.Contains(err.Error(), "mesh-controller:test") {
t.Errorf("the failure does not say what this machine holds nothing of: %v", err) t.Errorf("the failure does not say what this machine holds nothing of: %v", err)
} }
} }
@@ -298,7 +298,7 @@ func TestAnInstallerCarryingNoImageSaysSoRatherThanRaisingHalfAMesh(t *testing.T
// check anything against, so it is checked where the refusal can say whose mistake it is. // check anything against, so it is checked where the refusal can say whose mistake it is.
func TestARuntimeAnsweringSomethingThatIsNotAnImageIdIsRefused(t *testing.T) { func TestARuntimeAnsweringSomethingThatIsNotAnImageIdIsRefused(t *testing.T) {
for _, nonsense := range []string{ for _, nonsense := range []string{
"mesh-control:test", "mesh-controller:test",
"sha256:" + strings.Repeat("9", 63), "sha256:" + strings.Repeat("9", 63),
"<no value>", "<no value>",
} { } {
+1 -1
View File
@@ -80,7 +80,7 @@ func installModule(ctx context.Context, o Options, control controlPlane, module
// Split out because one module needs something in between: the control plane's own store // Split out because one module needs something in between: the control plane's own store
// connections have to be accepted before its declaration is composed, or the mesh would seal // connections have to be accepted before its declaration is composed, or the mesh would seal
// thirty-two random bytes into the file it expects a connection string in and the container would // thirty-two random bytes into the file it expects a connection string in and the container would
// come up unable to open anything (mesh-control's `secret accept`, and what it exists for). // come up unable to open anything (mesh-controller's `secret accept`, and what it exists for).
// //
// **`module add` is run every time and is not skipped when the module is already known.** It is an // **`module add` is run every time and is not skipped when the module is already known.** It is an
// upsert on the manifest, and the manifest is exactly what changes between runs — step 9 registers // upsert on the manifest, and the manifest is exactly what changes between runs — step 9 registers
+13 -13
View File
@@ -14,7 +14,7 @@ import (
// The base (mesh-tools) resolves the SDK by version from the mesh's package registry rather than // The base (mesh-tools) resolves the SDK by version from the mesh's package registry rather than
// cloning it from a git URL (novox/hq ADR 0076, issue 053). So the registry has to answer, and the // cloning it from a git URL (novox/hq ADR 0076, issue 053). So the registry has to answer, and the
// SDK has to be in it, before the base build runs. That is a pivot like the control plane's: gitea's // SDK has to be in it, before the base build runs. That is a pivot like the control plane's: gitea's
// SERVER is raised directly here, on the substrate's own postgres, and adopted as an ordinary module // SERVER is raised directly here, on the foundation's own postgres, and adopted as an ordinary module
// only after the base exists (which is what lets its provisioner image — built on the base — run). // only after the base exists (which is what lets its provisioner image — built on the base — run).
// //
// Nothing here is the steady state. It is the smallest set of acts that puts a working npm registry // Nothing here is the steady state. It is the smallest set of acts that puts a working npm registry
@@ -22,9 +22,9 @@ import (
// and the SDK published under it. The gitea MODULE, installed after the base, takes all of this over. // and the SDK published under it. The gitea MODULE, installed after the base, takes all of this over.
const ( const (
// substrateStore is the substrate's postgres container — the mesh's own memory, raised from the // foundationStore is the foundation's postgres container — the mesh's own memory, raised from the
// bundle. gitea's bootstrap database lives here too, so a mesh runs one postgres (issue 051). // bundle. gitea's bootstrap database lives here too, so a mesh runs one postgres (issue 051).
substrateStore = "mesh-store" foundationStore = "mesh-store"
// giteaBootstrap is the gitea server raised directly at genesis, before gitea is a module. // giteaBootstrap is the gitea server raised directly at genesis, before gitea is a module.
giteaBootstrap = "mesh-gitea-server" giteaBootstrap = "mesh-gitea-server"
// giteaImage is the same upstream image the gitea module runs, pinned identically so the module // giteaImage is the same upstream image the gitea module runs, pinned identically so the module
@@ -39,7 +39,7 @@ const (
// builderGiteaUser is the gitea account the builder publishes and pulls with at genesis. It is // builderGiteaUser is the gitea account the builder publishes and pulls with at genesis. It is
// the `as` the builder's static package binding names. // the `as` the builder's static package binding names.
builderGiteaUser = "mesh-builder" builderGiteaUser = "mesh-builder"
// giteaDBRole/giteaDBName is gitea's own database in the substrate store. // giteaDBRole/giteaDBName is gitea's own database in the foundation store.
giteaDBRole = "mesh_gitea" giteaDBRole = "mesh_gitea"
giteaDBName = "mesh_gitea" giteaDBName = "mesh_gitea"
// giteaPort is where the raised server answers on the machine. // giteaPort is where the raised server answers on the machine.
@@ -59,7 +59,7 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
return err return err
} }
say(" seeding gitea's database in the substrate store") say(" seeding gitea's database in the foundation store")
if err := seedGiteaDatabase(ctx, run, o.Timeout, dbPassword, say); err != nil { if err := seedGiteaDatabase(ctx, run, o.Timeout, dbPassword, say); err != nil {
return err return err
} }
@@ -106,8 +106,8 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
return nil return nil
} }
// seedGiteaDatabase creates gitea's role and database inside the substrate postgres, the same way // seedGiteaDatabase creates gitea's role and database inside the foundation postgres, the same way
// the substrate creates its own — psql run through the store container (the map's Route B). The role // the foundation creates its own — psql run through the store container (the map's Route B). The role
// is created before the database because the database is owned by it. Both are tolerant of already // is created before the database because the database is owned by it. Both are tolerant of already
// existing, so a re-run changes nothing. // existing, so a re-run changes nothing.
func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, password string, func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, password string,
@@ -119,7 +119,7 @@ func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, p
// transaction and \gexec does not parse through -c. The password is base64url, so it carries no // transaction and \gexec does not parse through -c. The password is base64url, so it carries no
// quote or backslash to escape inside a SQL literal. // quote or backslash to escape inside a SQL literal.
psql := func(sql string) (string, error) { psql := func(sql string) (string, error) {
return run(asking, "docker", "exec", substrateStore, "psql", "-U", "postgres", "-tAc", sql) return run(asking, "docker", "exec", foundationStore, "psql", "-U", "postgres", "-tAc", sql)
} }
// The role: create it, and if it is already there (create fails) reset its password so a re-run // The role: create it, and if it is already there (create fails) reset its password so a re-run
@@ -128,7 +128,7 @@ func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, p
if _, err := psql(create); err != nil { if _, err := psql(create); err != nil {
alter := fmt.Sprintf("ALTER ROLE %s LOGIN PASSWORD '%s'", giteaDBRole, password) alter := fmt.Sprintf("ALTER ROLE %s LOGIN PASSWORD '%s'", giteaDBRole, password)
if _, err := psql(alter); err != nil { if _, err := psql(alter); err != nil {
return fmt.Errorf("could not create gitea's role in %s: %w", substrateStore, err) return fmt.Errorf("could not create gitea's role in %s: %w", foundationStore, err)
} }
} }
@@ -136,17 +136,17 @@ func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, p
// second create is an error rather than a no-op. // second create is an error rather than a no-op.
present, err := psql(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", giteaDBName)) present, err := psql(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", giteaDBName))
if err != nil { if err != nil {
return fmt.Errorf("could not check for gitea's database in %s: %w", substrateStore, err) return fmt.Errorf("could not check for gitea's database in %s: %w", foundationStore, err)
} }
if strings.TrimSpace(present) != "1" { if strings.TrimSpace(present) != "1" {
if _, err := psql(fmt.Sprintf("CREATE DATABASE %s OWNER %s", giteaDBName, giteaDBRole)); err != nil { if _, err := psql(fmt.Sprintf("CREATE DATABASE %s OWNER %s", giteaDBName, giteaDBRole)); err != nil {
return fmt.Errorf("could not create gitea's database in %s: %w", substrateStore, err) return fmt.Errorf("could not create gitea's database in %s: %w", foundationStore, err)
} }
} }
return nil return nil
} }
// raiseGiteaServer starts the gitea server container against the substrate store. It joins the // raiseGiteaServer starts the gitea server container against the foundation store. It joins the
// store's network namespace so `127.0.0.1:5432` reaches postgres, and publishes its own port on the // store's network namespace so `127.0.0.1:5432` reaches postgres, and publishes its own port on the
// machine so the builder and this installer can reach it. Started if absent, left alone if present. // machine so the builder and this installer can reach it. Started if absent, left alone if present.
func raiseGiteaServer(ctx context.Context, run Runner, timeout time.Duration, dbPassword string, func raiseGiteaServer(ctx context.Context, run Runner, timeout time.Duration, dbPassword string,
@@ -179,7 +179,7 @@ func raiseGiteaServer(ctx context.Context, run Runner, timeout time.Duration, db
} }
args := append([]string{ args := append([]string{
"run", "-d", "--name", giteaBootstrap, "run", "-d", "--name", giteaBootstrap,
// Host network, like the control plane: it reaches the substrate store on the machine's // Host network, like the control plane: it reaches the foundation store on the machine's
// loopback (where the store publishes 5432) and answers on the machine's own 3000, which is // loopback (where the store publishes 5432) and answers on the machine's own 3000, which is
// where mesh-bootstrap and the builder's build containers look for it. // where mesh-bootstrap and the builder's build containers look for it.
"--network", "host", "--network", "host",
+4 -4
View File
@@ -26,7 +26,7 @@ func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte
// 1. Does this installer carry what it claims to? // 1. Does this installer carry what it claims to?
// //
// Asked before the machine is touched, for the same reason `mesh-host bundle` exists: a host // Asked before the machine is touched, for the same reason `mesh-host bundle` exists: a host
// that carries no substrate must say so when somebody asks, not on a first node // that carries no foundation must say so when somebody asks, not on a first node
// (internal/bundle). An installer built without an image would otherwise get a machine as far // (internal/bundle). An installer built without an image would otherwise get a machine as far
// as a running store and a running broker and stop. // as a running store and a running broker and stop.
if image.IsEmpty() { if image.IsEmpty() {
@@ -67,13 +67,13 @@ func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte
} }
say(fmt.Sprintf(" builder %s carried (the archive calls it %s)", tag, carriedID)) say(fmt.Sprintf(" builder %s carried (the archive calls it %s)", tag, carriedID))
// 2. Is the template there, and is it a substrate? // 2. Is the template there, and is it a foundation?
template, err := os.ReadFile(o.Template) template, err := os.ReadFile(o.Template)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"the bundle template could not be read: %w\n"+ "the bundle template could not be read: %w\n"+
"It is what this machine will be asked to be, so there is nothing to do without "+ "It is what this machine will be asked to be, so there is nothing to do without "+
"it. Point --bundle at one; mesh-host's examples/substrate-first-node.lock is "+ "it. Point --bundle at one; mesh-host's examples/foundation-first-node.lock is "+
"the shape", err) "the shape", err)
} }
// Parsed here as well as at the rewrite, because a template that is not a declaration should // Parsed here as well as at the rewrite, because a template that is not a declaration should
@@ -120,7 +120,7 @@ func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte
// with the id of the image this installer carries. Whatever the slot held is therefore never // with the id of the image this installer carries. Whatever the slot held is therefore never
// pulled, never fetched, and never reached; requiring it to be reachable refuses a correct // pulled, never fetched, and never reached; requiring it to be reachable refuses a correct
// install because of a string that is about to be thrown away. Found on the first real run: the // install because of a string that is about to be thrown away. Found on the first real run: the
// lab's template still carried `192.0.2.250:5000/mesh-control@…`, the address of a registry that // lab's template still carried `192.0.2.250:5000/mesh-controller@…`, the address of a registry that
// no longer exists, and preflight timed out dialling it. // no longer exists, and preflight timed out dialling it.
for _, host := range registriesIn(parsed) { for _, host := range registriesIn(parsed) {
dialing, cancel := context.WithTimeout(ctx, o.Timeout) dialing, cancel := context.WithTimeout(ctx, o.Timeout)
+3 -3
View File
@@ -82,7 +82,7 @@ func TestOnlyTheRegistriesTheBundleNamesAreAskedAbout(t *testing.T) {
strings.Repeat("7", 64) + `"}, strings.Repeat("7", 64) + `"},
{"id":"broker","type":"container","name":"mesh-broker","image":"192.0.2.250:5000/lavinmq@sha256:` + {"id":"broker","type":"container","name":"mesh-broker","image":"192.0.2.250:5000/lavinmq@sha256:` +
strings.Repeat("8", 64) + `"}, strings.Repeat("8", 64) + `"},
{"id":"control-plane","type":"container","name":"mesh-control","image":"` + held + `"} {"id":"control-plane","type":"container","name":"mesh-controller","image":"` + held + `"}
]}`)) ]}`))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -111,7 +111,7 @@ func TestTheControlPlanesOwnRegistryIsNeverAskedAbout(t *testing.T) {
parsed, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[ parsed, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[
{"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` + {"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` +
strings.Repeat("7", 64) + `"}, strings.Repeat("7", 64) + `"},
{"id":"control-plane","type":"container","name":"mesh-control","image":"192.0.2.250:5000/mesh-control@sha256:` + {"id":"control-plane","type":"container","name":"mesh-controller","image":"192.0.2.250:5000/mesh-controller@sha256:` +
strings.Repeat("8", 64) + `"} strings.Repeat("8", 64) + `"}
]}`)) ]}`))
if err != nil { if err != nil {
@@ -137,7 +137,7 @@ func TestWhereAnImageWouldBeFetchedFrom(t *testing.T) {
{"postgres@sha256:" + strings.Repeat("a", 64), DefaultRegistry, true}, {"postgres@sha256:" + strings.Repeat("a", 64), DefaultRegistry, true},
{"cloudamqp/lavinmq@sha256:" + strings.Repeat("a", 64), DefaultRegistry, true}, {"cloudamqp/lavinmq@sha256:" + strings.Repeat("a", 64), DefaultRegistry, true},
{"192.0.2.250:5000/postgres@sha256:" + strings.Repeat("a", 64), "192.0.2.250:5000", true}, {"192.0.2.250:5000/postgres@sha256:" + strings.Repeat("a", 64), "192.0.2.250:5000", true},
{"localhost/mesh-control@sha256:" + strings.Repeat("a", 64), "localhost:443", true}, {"localhost/mesh-controller@sha256:" + strings.Repeat("a", 64), "localhost:443", true},
{"registry.example.com/a/b@sha256:" + strings.Repeat("a", 64), "registry.example.com:443", true}, {"registry.example.com/a/b@sha256:" + strings.Repeat("a", 64), "registry.example.com:443", true},
// Held by this machine. Nothing serves it, and nothing can. // Held by this machine. Nothing serves it, and nothing can.
{"sha256:" + strings.Repeat("a", 64), "", false}, {"sha256:" + strings.Repeat("a", 64), "", false},
+5 -5
View File
@@ -9,19 +9,19 @@ import (
) )
// ControlPlaneRepository is what the control plane's image is called in the mesh's own registry. // ControlPlaneRepository is what the control plane's image is called in the mesh's own registry.
const ControlPlaneRepository = "mesh-control" const ControlPlaneRepository = "mesh-controller"
// genesisTag is the tag the first push uses. // genesisTag is the tag the first push uses.
// //
// A tag is not a pin and is never what anything is deployed from — the digest the registry assigns // A tag is not a pin and is never what anything is deployed from — the digest the registry assigns
// is (novox/hq ADR 0006). This exists so a person reading `/v2/mesh-control/tags/list` can see // is (novox/hq ADR 0006). This exists so a person reading `/v2/mesh-controller/tags/list` can see
// which image this mesh started from, and so the push has something to name. Everything downstream // which image this mesh started from, and so the push has something to name. Everything downstream
// uses the digest that comes back. // uses the digest that comes back.
const genesisTag = "genesis" const genesisTag = "genesis"
// Published is what step 8 did. // Published is what step 8 did.
type Published struct { type Published struct {
// Reference is `<registry>/mesh-control@sha256:…` — the first manifest digest this image has // Reference is `<registry>/mesh-controller@sha256:…` — the first manifest digest this image has
// ever had, and the thing that makes the control plane an ordinary module. // ever had, and the thing that makes the control plane an ordinary module.
Reference string Reference string
// Tagged is where it was pushed, tag and all. // Tagged is where it was pushed, tag and all.
@@ -34,7 +34,7 @@ type Published struct {
// //
// **This is the pivot's hinge.** Every image must be pinned by digest, and a digest a pin can mean // **This is the pivot's hinge.** Every image must be pinned by digest, and a digest a pin can mean
// is one a REGISTRY assigned when something was pushed to it. The control plane's image is built // is one a REGISTRY assigned when something was pushed to it. The control plane's image is built
// from source and pushed nowhere, so it has none — which is why the substrate names it by the // from source and pushed nowhere, so it has none — which is why the foundation names it by the
// digest of its own configuration, and why that is legal exactly where nothing could have served // digest of its own configuration, and why that is legal exactly where nothing could have served
// one. The moment this push completes, that stops being true: the image has a manifest digest, so // one. The moment this push completes, that stops being true: the image has a manifest digest, so
// the control plane can be named the way every other module is named, so the mesh can build and // the control plane can be named the way every other module is named, so the mesh can build and
@@ -42,7 +42,7 @@ type Published struct {
// upgrade itself, which is the check novox/hq ADR 0067 states: after installing, the running // upgrade itself, which is the check novox/hq ADR 0067 states: after installing, the running
// control plane must be pinned by a digest the mesh's own registry assigned, not by an image id. // control plane must be pinned by a digest the mesh's own registry assigned, not by an image id.
// //
// **It mirrors mesh-control's `internal/builder`.PublishImage rather than importing it.** Tag, // **It mirrors mesh-controller's `internal/builder`.PublishImage rather than importing it.** Tag,
// push, read back `RepoDigests`, refuse anything without `@sha256:` — the same four steps, because // push, read back `RepoDigests`, refuse anything without `@sha256:` — the same four steps, because
// there is exactly one right way to learn what a registry will serve something as, and it is to // there is exactly one right way to learn what a registry will serve something as, and it is to
// ask the registry. It is not imported because that code is tier 2: the host and its installer // ask the registry. It is not imported because that code is tier 2: the host and its installer
+9 -9
View File
@@ -41,7 +41,7 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
if !pushed { if !pushed {
return http.StatusNotFound, "", nil return http.StatusNotFound, "", nil
} }
return http.StatusOK, `{"name":"mesh-control","tags":["genesis"]}`, nil return http.StatusOK, `{"name":"mesh-controller","tags":["genesis"]}`, nil
}, },
func(_ string, args []string) (string, error) { func(_ string, args []string) (string, error) {
switch args[0] { switch args[0] {
@@ -51,7 +51,7 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
pushed = true pushed = true
return "", nil return "", nil
case "inspect": case "inspect":
return `["127.0.0.1:5000/mesh-control@sha256:` + strings.Repeat("a", 64) + `"]`, nil return `["127.0.0.1:5000/mesh-controller@sha256:` + strings.Repeat("a", 64) + `"]`, nil
} }
return "", fmt.Errorf("unexpected: %v", args) return "", fmt.Errorf("unexpected: %v", args)
}) })
@@ -63,10 +63,10 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
if out.Already { if out.Already {
t.Error("an image no registry held was reported as already published") t.Error("an image no registry held was reported as already published")
} }
if !strings.HasPrefix(out.Reference, "127.0.0.1:5000/mesh-control@sha256:") { if !strings.HasPrefix(out.Reference, "127.0.0.1:5000/mesh-controller@sha256:") {
t.Errorf("the control plane is pinned as %q", out.Reference) t.Errorf("the control plane is pinned as %q", out.Reference)
} }
if !runtime.ran("docker push 127.0.0.1:5000/mesh-control:genesis") { if !runtime.ran("docker push 127.0.0.1:5000/mesh-controller:genesis") {
t.Errorf("nothing was pushed: %v", runtime.commands) t.Errorf("nothing was pushed: %v", runtime.commands)
} }
} }
@@ -77,11 +77,11 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
func TestAnImageTheRegistryAlreadyServesIsNotPushedAgain(t *testing.T) { func TestAnImageTheRegistryAlreadyServesIsNotPushedAgain(t *testing.T) {
o, d, runtime := publishing(t, o, d, runtime := publishing(t,
func(string) (int, string, error) { func(string) (int, string, error) {
return http.StatusOK, `{"name":"mesh-control","tags":["genesis"]}`, nil return http.StatusOK, `{"name":"mesh-controller","tags":["genesis"]}`, nil
}, },
func(_ string, args []string) (string, error) { func(_ string, args []string) (string, error) {
if args[0] == "inspect" { if args[0] == "inspect" {
return `["127.0.0.1:5000/mesh-control@sha256:` + strings.Repeat("b", 64) + `"]`, nil return `["127.0.0.1:5000/mesh-controller@sha256:` + strings.Repeat("b", 64) + `"]`, nil
} }
return "", fmt.Errorf("unexpected: %v", args) return "", fmt.Errorf("unexpected: %v", args)
}) })
@@ -103,8 +103,8 @@ func TestAnImageTheRegistryAlreadyServesIsNotPushedAgain(t *testing.T) {
// listed first — which would pin this mesh's control plane to somebody else's registry, silently, // listed first — which would pin this mesh's control plane to somebody else's registry, silently,
// which is the dependency the whole pivot exists to remove. // which is the dependency the whole pivot exists to remove.
func TestTheDigestComesFromThisMeshsOwnRegistry(t *testing.T) { func TestTheDigestComesFromThisMeshsOwnRegistry(t *testing.T) {
elsewhere := "some.other.registry/mesh-control@sha256:" + strings.Repeat("c", 64) elsewhere := "some.other.registry/mesh-controller@sha256:" + strings.Repeat("c", 64)
ours := "127.0.0.1:5000/mesh-control@sha256:" + strings.Repeat("d", 64) ours := "127.0.0.1:5000/mesh-controller@sha256:" + strings.Repeat("d", 64)
o, d, _ := publishing(t, o, d, _ := publishing(t,
func(string) (int, string, error) { func(string) (int, string, error) {
@@ -167,7 +167,7 @@ func TestATagIsNotAPin(t *testing.T) {
}, },
func(_ string, args []string) (string, error) { func(_ string, args []string) (string, error) {
if args[0] == "inspect" { if args[0] == "inspect" {
return `["127.0.0.1:5000/mesh-control:genesis"]`, nil return `["127.0.0.1:5000/mesh-controller:genesis"]`, nil
} }
return "", nil return "", nil
}) })
+2 -2
View File
@@ -44,7 +44,7 @@ type Registry struct {
// //
// **No credentials, and that is deliberate.** The registry is reached over the mesh's own private // **No credentials, and that is deliberate.** The registry is reached over the mesh's own private
// network, which is already the encrypted and authenticated thing; a second layer inside it would // network, which is already the encrypted and authenticated thing; a second layer inside it would
// be certificates to issue and rotate for no property the first does not have (mesh-control's // be certificates to issue and rotate for no property the first does not have (mesh-controller's
// `internal/builder`, which pushes to it the same way). So there is nothing here to configure and // `internal/builder`, which pushes to it the same way). So there is nothing here to configure and
// nothing to seal — which is also why step 8 can push without the mesh having issued anything. // nothing to seal — which is also why step 8 can push without the mesh having issued anything.
// //
@@ -136,7 +136,7 @@ func waitForTheRegistry(ctx context.Context, d Deps, o Options, say func(string)
// waitForContainer waits for a container the mesh was asked to create to be running. // waitForContainer waits for a container the mesh was asked to create to be running.
// //
// Unlike the substrate's own verify, this one waits: the mesh applies through a node's host, over // Unlike the foundation's own verify, this one waits: the mesh applies through a node's host, over
// the broker, asynchronously. A push that the control plane accepted has not yet happened on the // the broker, asynchronously. A push that the control plane accepted has not yet happened on the
// machine, and refusing on the first look would refuse every correct install. // machine, and refusing on the first look would refuse every correct install.
func waitForContainer(ctx context.Context, run Runner, probe, wait time.Duration, name string, func waitForContainer(ctx context.Context, run Runner, probe, wait time.Duration, name string,
+7 -7
View File
@@ -18,7 +18,7 @@ import (
// catalogueWith writes a fake catalogue checkout holding one module's manifest. // catalogueWith writes a fake catalogue checkout holding one module's manifest.
// //
// A fixture here rather than the real catalogue, unlike the substrate example the rewrite tests // A fixture here rather than the real catalogue, unlike the foundation example the rewrite tests
// use: the catalogue is a different repository on a different branch, and a test that read it // use: the catalogue is a different repository on a different branch, and a test that read it
// would pass or fail according to what somebody else had checked out. // would pass or fail according to what somebody else had checked out.
func catalogueWith(t *testing.T, module, manifest string) string { func catalogueWith(t *testing.T, module, manifest string) string {
@@ -100,7 +100,7 @@ func TestARegistryContainerThatIsUpIsNotARegistryThatServes(t *testing.T) {
_, err := InstallRegistry(context.Background(), _, err := InstallRegistry(context.Background(),
installing(t, catalogueWith(t, RegistryModule, upstreamRegistryManifest)), installing(t, catalogueWith(t, RegistryModule, upstreamRegistryManifest)),
deps, controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, deps, controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err == nil { if err == nil {
t.Fatal("a registry whose container is up and which answers 500 was accepted") t.Fatal("a registry whose container is up and which answers 500 was accepted")
@@ -124,7 +124,7 @@ func TestARegistryThatAnswersIsAccepted(t *testing.T) {
out, err := InstallRegistry(context.Background(), out, err := InstallRegistry(context.Background(),
installing(t, catalogueWith(t, RegistryModule, upstreamRegistryManifest)), installing(t, catalogueWith(t, RegistryModule, upstreamRegistryManifest)),
deps, controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}, deps, controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
func(string) {}) func(string) {})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -159,7 +159,7 @@ func TestARegistryManifestThatWantsBuildingIsRefused(t *testing.T) {
runtime := &asked{answer: aMeshThatAgrees(nil)} runtime := &asked{answer: aMeshThatAgrees(nil)}
_, err := InstallRegistry(context.Background(), _, err := InstallRegistry(context.Background(),
installing(t, catalogueWith(t, RegistryModule, wants)), installing(t, catalogueWith(t, RegistryModule, wants)),
Deps{Run: runtime.run}, controlPlane{container: "temp-mesh-control", run: runtime.run}, Deps{Run: runtime.run}, controlPlane{container: "temp-mesh-controller", run: runtime.run},
func(string) {}) func(string) {})
if err == nil { if err == nil {
t.Fatal("a registry manifest naming an image the mesh would have to build was accepted") t.Fatal("a registry manifest naming an image the mesh would have to build was accepted")
@@ -178,7 +178,7 @@ func TestACatalogueThatIsNotThereIsSaidPlainly(t *testing.T) {
runtime := &asked{answer: aMeshThatAgrees(nil)} runtime := &asked{answer: aMeshThatAgrees(nil)}
_, err := InstallRegistry(context.Background(), _, err := InstallRegistry(context.Background(),
installing(t, filepath.Join(t.TempDir(), "nowhere")), installing(t, filepath.Join(t.TempDir(), "nowhere")),
Deps{Run: runtime.run}, controlPlane{container: "temp-mesh-control", run: runtime.run}, Deps{Run: runtime.run}, controlPlane{container: "temp-mesh-controller", run: runtime.run},
func(string) {}) func(string) {})
if err == nil { if err == nil {
t.Fatal("a catalogue that does not exist was accepted") t.Fatal("a catalogue that does not exist was accepted")
@@ -188,7 +188,7 @@ func TestACatalogueThatIsNotThereIsSaidPlainly(t *testing.T) {
} }
} }
// A refusal from the control plane is repeated verbatim. mesh-control refuses in paragraphs — // A refusal from the control plane is repeated verbatim. mesh-controller refuses in paragraphs —
// "nothing provides route, wanted by registry" — and an installer that reported "exit status 1" // "nothing provides route, wanted by registry" — and an installer that reported "exit status 1"
// would throw away the only thing a person can act on. // would throw away the only thing a person can act on.
func TestWhatTheMeshRefusedIsRepeated(t *testing.T) { func TestWhatTheMeshRefusedIsRepeated(t *testing.T) {
@@ -202,7 +202,7 @@ func TestWhatTheMeshRefusedIsRepeated(t *testing.T) {
_, err := InstallRegistry(context.Background(), _, err := InstallRegistry(context.Background(),
installing(t, catalogueWith(t, RegistryModule, upstreamRegistryManifest)), installing(t, catalogueWith(t, RegistryModule, upstreamRegistryManifest)),
Deps{Run: runtime.run}, controlPlane{container: "temp-mesh-control", run: runtime.run, Deps{Run: runtime.run}, controlPlane{container: "temp-mesh-controller", run: runtime.run,
timeout: time.Second}, func(string) {}) timeout: time.Second}, func(string) {})
if err == nil { if err == nil {
t.Fatal("a push the mesh refused was reported as successful") t.Fatal("a push the mesh refused was reported as successful")
+4 -4
View File
@@ -32,8 +32,8 @@ type Retired struct {
// bundle is applied again, and the removal pass does what it does for every other resource that // bundle is applied again, and the removal pass does what it does for every other resource that
// leaves a declaration. // leaves a declaration.
// //
// That is the whole of why the rename at step 3 mattered. Had the substrate and the module both // That is the whole of why the rename at step 3 mattered. Had the foundation and the module both
// called their container `mesh-control`, this apply would have removed the module's container — // called their container `mesh-controller`, this apply would have removed the module's container —
// the host would have been asked to take away something it believed it owned, and it would have // the host would have been asked to take away something it believed it owned, and it would have
// been right. Two names, two owners, and the removal is unambiguous. // been right. Two names, two owners, and the removal is unambiguous.
// //
@@ -63,7 +63,7 @@ func RetireTheTemporaryControlPlane(ctx context.Context, o Options, sys system.S
// Textual, for the reason the rewrite at step 3 is textual: the produced bundle is meant to be // Textual, for the reason the rewrite at step 3 is textual: the produced bundle is meant to be
// READ, and a person coming to a machine after a pivot should be able to open the file the // READ, and a person coming to a machine after a pivot should be able to open the file the
// installer applied and see the substrate they recognise with the control plane gone from it. // installer applied and see the foundation they recognise with the control plane gone from it.
// Re-serialising a parsed declaration would drop every comment in it. // Re-serialising a parsed declaration would drop every comment in it.
bundle, err := removeResource(produced, ControlPlaneID) bundle, err := removeResource(produced, ControlPlaneID)
if err != nil { if err != nil {
@@ -124,7 +124,7 @@ func RetireTheTemporaryControlPlane(ctx context.Context, o Options, sys system.S
// removeResource takes one resource out of a bundle's text, comments and all. // removeResource takes one resource out of a bundle's text, comments and all.
// //
// It walks the `resources` array counting braces, skipping over strings and comments so that a // It walks the `resources` array counting braces, skipping over strings and comments so that a
// `//` inside a connection string is not read as the start of one — the substrate's own bundle // `//` inside a connection string is not read as the start of one — the foundation's own bundle
// contains `postgres://…` several times, and a scanner that did not know the difference would // contains `postgres://…` several times, and a scanner that did not know the difference would
// treat the rest of the line as a comment and lose a brace. // treat the rest of the line as a comment and lose a brace.
// //
+5 -5
View File
@@ -46,7 +46,7 @@ func TestTheTemporaryControlPlaneLeavesTheBundleAndNothingElseDoes(t *testing.T)
t.Error("the bundle still declares a control plane") t.Error("the bundle still declares a control plane")
} }
// The store and the broker are still exactly what they were. A retirement that took the // The store and the broker are still exactly what they were. A retirement that took the
// substrate with it would leave the machine with a module and nothing under it. // foundation with it would leave the machine with a module and nothing under it.
for id, name := range containerNames(before) { for id, name := range containerNames(before) {
if id == ControlPlaneID { if id == ControlPlaneID {
continue continue
@@ -57,7 +57,7 @@ func TestTheTemporaryControlPlaneLeavesTheBundleAndNothingElseDoes(t *testing.T)
} }
} }
// **A `//` inside a string is not a comment.** The substrate's own bundle carries // **A `//` inside a string is not a comment.** The foundation's own bundle carries
// `postgres://…` several times, and a scanner that read the rest of those lines as a comment // `postgres://…` several times, and a scanner that read the rest of those lines as a comment
// would lose braces and cut the wrong thing out — silently, because what it produced would still // would lose braces and cut the wrong thing out — silently, because what it produced would still
// look like a file. // look like a file.
@@ -144,7 +144,7 @@ func TestAContainerStillThereAfterRemovalIsNotGone(t *testing.T) {
stillThere := &asked{answer: func(_ string, _ []string) (string, error) { stillThere := &asked{answer: func(_ string, _ []string) (string, error) {
return "true running\n", nil return "true running\n", nil
}} }}
gone, err := isGone(context.Background(), stillThere.run, time.Second, 0, "temp-mesh-control") gone, err := isGone(context.Background(), stillThere.run, time.Second, 0, "temp-mesh-controller")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -153,9 +153,9 @@ func TestAContainerStillThereAfterRemovalIsNotGone(t *testing.T) {
} }
removed := &asked{answer: func(_ string, _ []string) (string, error) { removed := &asked{answer: func(_ string, _ []string) (string, error) {
return "", errors.New("No such object: temp-mesh-control") return "", errors.New("No such object: temp-mesh-controller")
}} }}
gone, err = isGone(context.Background(), removed.run, time.Second, 0, "temp-mesh-control") gone, err = isGone(context.Background(), removed.run, time.Second, 0, "temp-mesh-controller")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+19 -19
View File
@@ -19,27 +19,27 @@ import (
// broker and no mesh. // broker and no mesh.
const ControlPlaneID = "control-plane" const ControlPlaneID = "control-plane"
// TempPrefix is what the substrate's control plane is renamed with. // TempPrefix is what the foundation's control plane is renamed with.
// //
// **This is the whole of how a carried resource becomes a declared one.** The substrate raises a // **This is the whole of how a carried resource becomes a declared one.** The foundation raises a
// control plane and a module later declares one, and for a moment both exist — which looked like a // control plane and a module later declares one, and for a moment both exist — which looked like a
// handover problem needing a way for the host to stop owning something without destroying it. It is // handover problem needing a way for the host to stop owning something without destroying it. It is
// not one. The temporary control plane is called `temp-mesh-control` and the permanent one is // not one. The temporary control plane is called `temp-mesh-controller` and the permanent one is
// called `mesh-control`: two containers, two owners, nothing shared and nothing to hand over. At // called `mesh-controller`: two containers, two owners, nothing shared and nothing to hand over. At
// the end the temporary one is dropped from the bundle and the host removes it, which is exactly // the end the temporary one is dropped from the bundle and the host removes it, which is exactly
// what should happen to something named "temp" (novox/hq ADR 0067). // what should happen to something named "temp" (novox/hq ADR 0067).
// //
// The name is also the audit. After the pivot, a machine running `mesh-control` and not // The name is also the audit. After the pivot, a machine running `mesh-controller` and not
// `temp-mesh-control` has completed it; one running both stopped in the middle; one running only // `temp-mesh-controller` has completed it; one running both stopped in the middle; one running only
// the temp has not started. That is readable from `docker ps` by somebody who knows nothing else. // the temp has not started. That is readable from `docker ps` by somebody who knows nothing else.
const TempPrefix = "temp-" const TempPrefix = "temp-"
// ControlPlaneModule is the module the permanent control plane is installed as, and the name its // ControlPlaneModule is the module the permanent control plane is installed as, and the name its
// container takes — the name the substrate's own control plane gives up here so that it can. // container takes — the name the foundation's own control plane gives up here so that it can.
// //
// Declared beside the rename rather than beside the step that uses it, because this is where the // Declared beside the rename rather than beside the step that uses it, because this is where the
// two names are decided together and where the reason for both of them is written down. // two names are decided together and where the reason for both of them is written down.
const ControlPlaneModule = "mesh-control" const ControlPlaneModule = "mesh-controller"
// brokerAddressVar is what a token tells an enrolling node to dial. // brokerAddressVar is what a token tells an enrolling node to dial.
// //
@@ -85,11 +85,11 @@ type Rewritten struct {
// Rewrite produces the bundle this machine will apply from the template it was given. // Rewrite produces the bundle this machine will apply from the template it was given.
// //
// **Two substitutions, and both are textual.** The control plane's image becomes the id of the image // **Two substitutions, and both are textual.** The control plane's image becomes the id of the image
// this machine now holds, and its container is renamed `temp-mesh-control`; nothing else changes. // this machine now holds, and its container is renamed `temp-mesh-controller`; nothing else changes.
// The rename is what makes the pivot expressible at all — see TempPrefix. Textual rather than // The rename is what makes the pivot expressible at all — see TempPrefix. Textual rather than
// parse-and-re-serialise because // parse-and-re-serialise because
// the produced file has to be *read* — a person getting a machine working must be able to open it, // the produced file has to be *read* — a person getting a machine working must be able to open it,
// see the substrate they recognise, and see exactly one thing different. Re-serialising a parsed // see the foundation they recognise, and see exactly one thing different. Re-serialising a parsed
// declaration would drop every comment in the template, and those comments are where the reasons // declaration would drop every comment in the template, and those comments are where the reasons
// live. // live.
// //
@@ -182,7 +182,7 @@ func Rewrite(template []byte, imageID string) (Rewritten, error) {
if produced.Name != out.TempName { if produced.Name != out.TempName {
return Rewritten{}, fmt.Errorf( return Rewritten{}, fmt.Errorf(
"the produced bundle still calls the control plane's container %q, not %q. The "+ "the produced bundle still calls the control plane's container %q, not %q. The "+
"permanent one is a module and takes the plain name, so a substrate that kept it "+ "permanent one is a module and takes the plain name, so a foundation that kept it "+
"would put two owners on one container", produced.Name, out.TempName) "would put two owners on one container", produced.Name, out.TempName)
} }
@@ -209,7 +209,7 @@ func Rewrite(template []byte, imageID string) (Rewritten, error) {
return Rewritten{}, fmt.Errorf( return Rewritten{}, fmt.Errorf(
"renaming the control plane's container also renamed %q, from %q to %q. Only the "+ "renaming the control plane's container also renamed %q, from %q to %q. Only the "+
"control plane moves out of the way; every other container keeps the name the "+ "control plane moves out of the way; every other container keeps the name the "+
"substrate gave it", id, wasName[id], name) "foundation gave it", id, wasName[id], name)
} }
sortStrings(out.Kept) sortStrings(out.Kept)
return out, nil return out, nil
@@ -217,15 +217,15 @@ func Rewrite(template []byte, imageID string) (Rewritten, error) {
// renameContainer changes one container's name in the bundle's text. // renameContainer changes one container's name in the bundle's text.
// //
// **The quoted name, not the bare word.** `mesh-control` also appears inside the image reference // **The quoted name, not the bare word.** `mesh-controller` also appears inside the image reference
// the template carries (`…/mesh-control@sha256:…`) and could appear inside a command line; a bare // the template carries (`…/mesh-controller@sha256:…`) and could appear inside a command line; a bare
// substitution would catch those too. What is wanted is a JSON string that IS the name, so the // substitution would catch those too. What is wanted is a JSON string that IS the name, so the
// quotes are part of what is matched — `"mesh-control"` matches the container's `name` and an // quotes are part of what is matched — `"mesh-controller"` matches the container's `name` and an
// action's `in`, which are exactly the places the name means the container, and nothing else. // action's `in`, which are exactly the places the name means the container, and nothing else.
// //
// It refuses when the text does not contain what the parse says is there, for the same reason the // It refuses when the text does not contain what the parse says is there, for the same reason the
// image substitution does: the two would then be reading different things, and a rename that // image substitution does: the two would then be reading different things, and a rename that
// replaced nothing and reported success would leave the module and the substrate fighting over one // replaced nothing and reported success would leave the module and the foundation fighting over one
// container three steps later. // container three steps later.
func renameContainer(bundle []byte, from, to string) ([]byte, error) { func renameContainer(bundle []byte, from, to string) ([]byte, error) {
if from == to { if from == to {
@@ -235,7 +235,7 @@ func renameContainer(bundle []byte, from, to string) ([]byte, error) {
if bytes.Count(bundle, quoted) == 0 { if bytes.Count(bundle, quoted) == 0 {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"the control plane's container is called %q according to the parsed template, and %s "+ "the control plane's container is called %q according to the parsed template, and %s "+
"is not in the file. Nothing was renamed, and the substrate would raise a "+ "is not in the file. Nothing was renamed, and the foundation would raise a "+
"container the module also wants", from, quoted) "container the module also wants", from, quoted)
} }
return bytes.ReplaceAll(bundle, quoted, []byte(`"`+to+`"`)), nil return bytes.ReplaceAll(bundle, quoted, []byte(`"`+to+`"`)), nil
@@ -257,7 +257,7 @@ func controlPlaneIn(d *declaration.Declaration) (*declaration.Container, error)
} }
return nil, fmt.Errorf( return nil, fmt.Errorf(
"this bundle names no %q, so there is no control plane to give this machine's image to. "+ "this bundle names no %q, so there is no control plane to give this machine's image to. "+
"A substrate without one raises a store and a broker and no mesh. It declares: %s", "A foundation without one raises a store and a broker and no mesh. It declares: %s",
ControlPlaneID, strings.Join(identities(d), ", ")) ControlPlaneID, strings.Join(identities(d), ", "))
} }
@@ -316,7 +316,7 @@ func sortStrings(values []string) {
// writeBundleFile puts the produced bundle where a person can read it, creating the directory it // writeBundleFile puts the produced bundle where a person can read it, creating the directory it
// lives in. // lives in.
// //
// 0644, and that is deliberate: this file names an image and describes a substrate, and it holds // 0644, and that is deliberate: this file names an image and describes a foundation, and it holds
// the bootstrap credentials the template happens to carry — which are the same ones anybody can // the bootstrap credentials the template happens to carry — which are the same ones anybody can
// read in the template itself. It is meant to be read. What must not be world-readable is the // read in the template itself. It is meant to be read. What must not be world-readable is the
// node's identity, and that lives elsewhere and is written elsewhere (`internal/identity`). // node's identity, and that lives elsewhere and is written elsewhere (`internal/identity`).
+27 -27
View File
@@ -15,16 +15,16 @@ const (
otherHeld = "sha256:2222222222222222222222222222222222222222222222222222222222222222" otherHeld = "sha256:2222222222222222222222222222222222222222222222222222222222222222"
) )
// theRealBundle is this repository's own substrate example, used rather than a fixture. // theRealBundle is this repository's own foundation example, used rather than a fixture.
// //
// A fixture would agree with whatever this code does. The example is what an installer is actually // A fixture would agree with whatever this code does. The example is what an installer is actually
// pointed at, it names the control plane twice, and it is the file that changes when the substrate // pointed at, it names the control plane twice, and it is the file that changes when the foundation
// changes — so a rewrite that stops working on it is a rewrite that has stopped working. // changes — so a rewrite that stops working on it is a rewrite that has stopped working.
func theRealBundle(t *testing.T) []byte { func theRealBundle(t *testing.T) []byte {
t.Helper() t.Helper()
raw, err := os.ReadFile("../../examples/substrate-first-node.lock") raw, err := os.ReadFile("../../examples/foundation-first-node.lock")
if err != nil { if err != nil {
t.Fatalf("reading the substrate example: %v", err) t.Fatalf("reading the foundation example: %v", err)
} }
return raw return raw
} }
@@ -48,7 +48,7 @@ func TestTheControlPlaneIsNamedByTheImageThisMachineHolds(t *testing.T) {
// **Every place the bundle names that image, not only the container.** // **Every place the bundle names that image, not only the container.**
// //
// The substrate names the control plane's image twice: the container that runs `serve`, and the // The foundation names the control plane's image twice: the container that runs `serve`, and the
// action that runs `migrate` to create the contexts' schemas. Rewriting only the container leaves // action that runs `migrate` to create the contexts' schemas. Rewriting only the container leaves
// the migration pointing at an image no registry serves, and the apply dies in the middle — after // the migration pointing at an image no registry serves, and the apply dies in the middle — after
// the store is up and before the broker. This is the test that would have caught that. // the store is up and before the broker. This is the test that would have caught that.
@@ -60,7 +60,7 @@ func TestEveryPlaceTheBundleNamesTheControlPlaneIsRewritten(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if out.Places < 2 { if out.Places < 2 {
t.Fatalf("the control plane's image was found in %d place(s); the substrate names it in "+ t.Fatalf("the control plane's image was found in %d place(s); the foundation names it in "+
"the container AND in the migration action", out.Places) "the container AND in the migration action", out.Places)
} }
if remaining := strings.Count(string(out.Bundle), out.Was); remaining != 0 { if remaining := strings.Count(string(out.Bundle), out.Was); remaining != 0 {
@@ -86,7 +86,7 @@ func TestPostgresAndTheBrokerAreLeftExactlyAsTheyWere(t *testing.T) {
for _, id := range []string{"store", "broker"} { for _, id := range []string{"store", "broker"} {
image, named := produced[id] image, named := produced[id]
if !named { if !named {
t.Fatalf("the substrate example no longer declares a %q container", id) t.Fatalf("the foundation example no longer declares a %q container", id)
} }
// Compared against the template's own text rather than against an expectation written // Compared against the template's own text rather than against an expectation written
// here: what is being defended is "unchanged", and the template is the only thing that // here: what is being defended is "unchanged", and the template is the only thing that
@@ -126,7 +126,7 @@ func TestABundleThatNamesNoControlPlaneIsRefused(t *testing.T) {
func TestAControlPlaneThatIsNotAContainerIsRefused(t *testing.T) { func TestAControlPlaneThatIsNotAContainerIsRefused(t *testing.T) {
template := []byte(`{"declaration":1,"resources":[ template := []byte(`{"declaration":1,"resources":[
{"id":"control-plane","type":"package","package":"mesh-control"} {"id":"control-plane","type":"package","package":"mesh-controller"}
]}`) ]}`)
if _, err := Rewrite(template, held); err == nil { if _, err := Rewrite(template, held); err == nil {
t.Fatal("a control plane declared as a package was accepted, and a package has no image") t.Fatal("a control plane declared as a package was accepted, and a package has no image")
@@ -175,7 +175,7 @@ func TestANewImageReplacesAnOlderHeldOne(t *testing.T) {
} }
// The produced bundle is meant to be READ. Re-serialising a parsed declaration would drop every // The produced bundle is meant to be READ. Re-serialising a parsed declaration would drop every
// comment in the template, and the substrate example is mostly comments — each one recording why a // comment in the template, and the foundation example is mostly comments — each one recording why a
// resource is the way it is, several of them paid for in the lab. // resource is the way it is, several of them paid for in the lab.
func TestTheProducedBundleKeepsTheTemplatesComments(t *testing.T) { func TestTheProducedBundleKeepsTheTemplatesComments(t *testing.T) {
template := theRealBundle(t) template := theRealBundle(t)
@@ -194,11 +194,11 @@ func TestTheProducedBundleKeepsTheTemplatesComments(t *testing.T) {
func TestSomethingThatIsNotAnImageIdIsRefused(t *testing.T) { func TestSomethingThatIsNotAnImageIdIsRefused(t *testing.T) {
for _, bad := range []string{ for _, bad := range []string{
"", "",
"mesh-control:latest", "mesh-controller:latest",
"sha256:abc", "sha256:abc",
"sha256:" + strings.Repeat("1", 63), "sha256:" + strings.Repeat("1", 63),
"sha256:" + strings.Repeat("g", 64), "sha256:" + strings.Repeat("g", 64),
"mesh-control@sha256:" + strings.Repeat("1", 64), "mesh-controller@sha256:" + strings.Repeat("1", 64),
} { } {
if _, err := Rewrite(theRealBundle(t), bad); err == nil { if _, err := Rewrite(theRealBundle(t), bad); err == nil {
t.Errorf("image id %q was accepted", bad) t.Errorf("image id %q was accepted", bad)
@@ -216,7 +216,7 @@ func TestTheAddressNodesWillDialIsReportedAndNotRewritten(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if out.BrokerAddress == "" { if out.BrokerAddress == "" {
t.Fatal("the substrate example no longer says what address enrolling nodes will dial") t.Fatal("the foundation example no longer says what address enrolling nodes will dial")
} }
if !strings.Contains(string(out.Bundle), out.BrokerAddress) { if !strings.Contains(string(out.Bundle), out.BrokerAddress) {
t.Errorf("the produced bundle no longer carries %q — it was rewritten, and nothing here "+ t.Errorf("the produced bundle no longer carries %q — it was rewritten, and nothing here "+
@@ -228,21 +228,21 @@ func TestTheAddressNodesWillDialIsReportedAndNotRewritten(t *testing.T) {
// The rename, which is what makes genesis a pivot rather than a handover (novox/hq ADR 0067). // The rename, which is what makes genesis a pivot rather than a handover (novox/hq ADR 0067).
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
// **This is the test that dissolves the blocker.** The substrate raises a control plane and a // **This is the test that dissolves the blocker.** The foundation raises a control plane and a
// module later declares one; if both are called `mesh-control` then for one moment two owners hold // module later declares one; if both are called `mesh-controller` then for one moment two owners hold
// one container, and the host — which tracks what it owns — has no way to stop owning something // one container, and the host — which tracks what it owns — has no way to stop owning something
// without destroying it. Nothing here invents such a mechanism. The substrate's container is // without destroying it. Nothing here invents such a mechanism. The foundation's container is
// called `temp-mesh-control` instead, and there are simply two containers. // called `temp-mesh-controller` instead, and there are simply two containers.
func TestTheSubstratesControlPlaneMovesOutOfTheModulesWay(t *testing.T) { func TestTheFoundationsControlPlaneMovesOutOfTheModulesWay(t *testing.T) {
out, err := Rewrite(theRealBundle(t), held) out, err := Rewrite(theRealBundle(t), held)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !out.Renamed { if !out.Renamed {
t.Error("the rewrite reported nothing renamed, and the template named it mesh-control") t.Error("the rewrite reported nothing renamed, and the template named it mesh-controller")
} }
if out.TempName != "temp-mesh-control" { if out.TempName != "temp-mesh-controller" {
t.Errorf("the substrate's control plane is called %q", out.TempName) t.Errorf("the foundation's control plane is called %q", out.TempName)
} }
control, err := controlPlaneIn(out.Declaration) control, err := controlPlaneIn(out.Declaration)
if err != nil { if err != nil {
@@ -260,22 +260,22 @@ func TestTheSubstratesControlPlaneMovesOutOfTheModulesWay(t *testing.T) {
} }
} }
// The image reference contains the string `mesh-control` too, and it is not a container name. A // The image reference contains the string `mesh-controller` too, and it is not a container name. A
// substitution that caught it would produce `…/temp-mesh-control@sha256:…`, which no registry // substitution that caught it would produce `…/temp-mesh-controller@sha256:…`, which no registry
// serves — and it would be found inside a pull rather than here. // serves — and it would be found inside a pull rather than here.
func TestTheImageReferenceIsNotMistakenForTheContainerName(t *testing.T) { func TestTheImageReferenceIsNotMistakenForTheContainerName(t *testing.T) {
out, err := Rewrite(theRealBundle(t), held) out, err := Rewrite(theRealBundle(t), held)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if strings.Contains(string(out.Bundle), TempPrefix+"mesh-control@") || if strings.Contains(string(out.Bundle), TempPrefix+"mesh-controller@") ||
strings.Contains(string(out.Bundle), "/"+TempPrefix+"mesh-control") { strings.Contains(string(out.Bundle), "/"+TempPrefix+"mesh-controller") {
t.Error("the rename reached inside an image reference") t.Error("the rename reached inside an image reference")
} }
} }
// Everything else keeps the name the substrate gave it. The store and the broker are containers // Everything else keeps the name the foundation gave it. The store and the broker are containers
// too, and a rename that moved them would leave a machine whose substrate the host cannot find. // too, and a rename that moved them would leave a machine whose foundation the host cannot find.
func TestRenamingTheControlPlaneLeavesEveryOtherContainerAlone(t *testing.T) { func TestRenamingTheControlPlaneLeavesEveryOtherContainerAlone(t *testing.T) {
before, err := declaration.ParseFileTrusted(theRealBundle(t)) before, err := declaration.ParseFileTrusted(theRealBundle(t))
if err != nil { if err != nil {
@@ -309,7 +309,7 @@ func TestRewritingABundleThisAlreadyProducedRenamesNothing(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if second.Renamed { if second.Renamed {
t.Error("a bundle already naming temp-mesh-control was renamed again") t.Error("a bundle already naming temp-mesh-controller was renamed again")
} }
if second.TempName != first.TempName { if second.TempName != first.TempName {
t.Errorf("the second pass calls it %q and the first called it %q", t.Errorf("the second pass calls it %q and the first called it %q",
+6 -6
View File
@@ -13,13 +13,13 @@ import (
// //
// **Through `docker exec`, not over a network.** The control plane listens on nothing — `serve` is // **Through `docker exec`, not over a network.** The control plane listens on nothing — `serve` is
// a broker consumer, and every administrative verb is a subcommand of the same binary that opens // a broker consumer, and every administrative verb is a subcommand of the same binary that opens
// the stores directly (mesh-control's own usage). So the way to tell a mesh anything, from the // the stores directly (mesh-controller's own usage). So the way to tell a mesh anything, from the
// machine the mesh is on, is to run its binary inside its own container. That is also what the lab // machine the mesh is on, is to run its binary inside its own container. That is also what the lab
// does, and having the installer and the lab drive the mesh identically is the point: the lab is // does, and having the installer and the lab drive the mesh identically is the point: the lab is
// meant to exercise the installer, not a second procedure that resembles it. // meant to exercise the installer, not a second procedure that resembles it.
// //
// It carries which container, because the whole pivot turns on there being two of them: the // It carries which container, because the whole pivot turns on there being two of them: the
// substrate's `temp-mesh-control` for steps 6 to 9, and the module's `mesh-control` afterwards. // foundation's `temp-mesh-controller` for steps 6 to 9, and the module's `mesh-controller` afterwards.
type controlPlane struct { type controlPlane struct {
container string container string
run Runner run Runner
@@ -35,9 +35,9 @@ func (c controlPlane) within(timeout time.Duration) controlPlane {
return c return c
} }
// tell runs a mesh-control subcommand and gives back what it said. // tell runs a mesh-controller subcommand and gives back what it said.
// //
// The failure carries the command AND the output. A mesh-control refusal is a paragraph explaining // The failure carries the command AND the output. A mesh-controller refusal is a paragraph explaining
// what is wrong — "nothing provides route, wanted by registry" — and an installer that reported // what is wrong — "nothing provides route, wanted by registry" — and an installer that reported
// only "exit status 1" would throw away the one thing a person needs. // only "exit status 1" would throw away the one thing a person needs.
func (c controlPlane) tell(ctx context.Context, args ...string) (string, error) { func (c controlPlane) tell(ctx context.Context, args ...string) (string, error) {
@@ -83,7 +83,7 @@ func (c controlPlane) carry(ctx context.Context, local, remote string) error {
// crash-looped on material it never received. There is no shell in the image to chown it with. // crash-looped on material it never received. There is no shell in the image to chown it with.
// //
// What goes through here is a module manifest and a store connection string. The connection is the // What goes through here is a module manifest and a store connection string. The connection is the
// same value the produced bundle already holds in the clear — a substrate names its own bootstrap // same value the produced bundle already holds in the clear — a foundation names its own bootstrap
// credentials, and at genesis there is nowhere else for them to be — so this widens nothing. The // credentials, and at genesis there is nowhere else for them to be — so this widens nothing. The
// file on the machine is removed at once, and the copy inside the container goes when the // file on the machine is removed at once, and the copy inside the container goes when the
// container does, which for the temporary control plane is step 10. // container does, which for the temporary control plane is step 10.
@@ -107,7 +107,7 @@ func indent(s string) string {
// //
// Line-and-word rather than a substring search, because these listings are columns and a // Line-and-word rather than a substring search, because these listings are columns and a
// substring match would find `registry` inside `registry-mirror` and report a module installed // substring match would find `registry` inside `registry-mirror` and report a module installed
// that is not. Every one of mesh-control's `list` verbs prints the name first on the line. // that is not. Every one of mesh-controller's `list` verbs prints the name first on the line.
func mentions(listing, name string) bool { func mentions(listing, name string) bool {
for _, line := range strings.Split(listing, "\n") { for _, line := range strings.Split(listing, "\n") {
first, _, _ := strings.Cut(strings.TrimSpace(line), " ") first, _, _ := strings.Cut(strings.TrimSpace(line), " ")
+5 -5
View File
@@ -13,14 +13,14 @@ import (
// //
// A path rather than a shell command, because the image is `FROM scratch` and holds one static // A path rather than a shell command, because the image is `FROM scratch` and holds one static
// binary and nothing else — no shell to invoke, nothing to interpret a command line // binary and nothing else — no shell to invoke, nothing to interpret a command line
// (mesh-control's Dockerfile, novox/hq ADR 0006). That is a property of the image this installer // (mesh-controller's Dockerfile, novox/hq ADR 0006). That is a property of the image this installer
// carries, which is why the path can be written down here. // carries, which is why the path can be written down here.
const controlPlaneBinary = "/mesh-control" const controlPlaneBinary = "/mesh-controller"
// answerEvery is how often the control plane is asked again while it is starting. // answerEvery is how often the control plane is asked again while it is starting.
var answerEvery = 2 * time.Second var answerEvery = 2 * time.Second
// Verified is what the substrate was found to be. // Verified is what the foundation was found to be.
type Verified struct { type Verified struct {
// Running is every long-running container the bundle declares, confirmed up. // Running is every long-running container the bundle declares, confirmed up.
Running []string Running []string
@@ -29,7 +29,7 @@ type Verified struct {
Answered string Answered string
} }
// Verify proves the substrate is up and the control plane replies. // Verify proves the foundation is up and the control plane replies.
// //
// **A container that is up is not a control plane that replies**, and this project has paid for // **A container that is up is not a control plane that replies**, and this project has paid for
// that distinction more than once: a runtime reports a container running from the moment the // that distinction more than once: a runtime reports a container running from the moment the
@@ -146,7 +146,7 @@ func containerRunning(ctx context.Context, run Runner, probe time.Duration, name
// //
// A run-once step has exited by design and a scheduled step has deliberately never been started // A run-once step has exited by design and a scheduled step has deliberately never been started
// (novox/hq ADR 0052, ADR 0053), so asking either of them to be running would be asking the // (novox/hq ADR 0052, ADR 0053), so asking either of them to be running would be asking the
// substrate to be something other than what it declared. // foundation to be something other than what it declared.
func longRunning(d *declaration.Declaration) []string { func longRunning(d *declaration.Declaration) []string {
var names []string var names []string
for _, r := range d.Resources { for _, r := range d.Resources {
+15 -15
View File
@@ -11,7 +11,7 @@ import (
"github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/declaration"
) )
func substrate(t *testing.T) *declaration.Declaration { func foundation(t *testing.T) *declaration.Declaration {
t.Helper() t.Helper()
out, err := Rewrite(theRealBundle(t), held) out, err := Rewrite(theRealBundle(t), held)
if err != nil { if err != nil {
@@ -39,12 +39,12 @@ func TestAContainerThatIsUpIsNotAControlPlaneThatReplies(t *testing.T) {
return "", fmt.Errorf("unexpected command: %v", args) return "", fmt.Errorf("unexpected command: %v", args)
}} }}
_, err := Verify(context.Background(), substrate(t), runtime.run, _, err := Verify(context.Background(), foundation(t), runtime.run,
time.Second, 0, func(string) {}) time.Second, 0, func(string) {})
if err == nil { if err == nil {
t.Fatal("every container was running, nothing answered, and the substrate was reported up") t.Fatal("every container was running, nothing answered, and the foundation was reported up")
} }
for _, wanted := range []string{"mesh-control", "Running is not replying", "docker logs"} { for _, wanted := range []string{"mesh-controller", "Running is not replying", "docker logs"} {
if !strings.Contains(err.Error(), wanted) { if !strings.Contains(err.Error(), wanted) {
t.Errorf("the failure does not mention %q:\n%v", wanted, err) t.Errorf("the failure does not mention %q:\n%v", wanted, err)
} }
@@ -65,14 +65,14 @@ func TestAControlPlaneThatSaysNothingHasNotAnswered(t *testing.T) {
return " \n", nil return " \n", nil
}} }}
if _, err := Verify(context.Background(), substrate(t), runtime.run, if _, err := Verify(context.Background(), foundation(t), runtime.run,
time.Second, 0, func(string) {}); err == nil { time.Second, 0, func(string) {}); err == nil {
t.Fatal("a control plane that exited zero without saying anything was accepted") t.Fatal("a control plane that exited zero without saying anything was accepted")
} }
} }
// The substrate answering is the whole point, and what it said is reported rather than asserted. // The foundation answering is the whole point, and what it said is reported rather than asserted.
func TestASubstrateThatIsUpAndAnsweringIsAccepted(t *testing.T) { func TestAFoundationThatIsUpAndAnsweringIsAccepted(t *testing.T) {
runtime := &asked{answer: func(_ string, args []string) (string, error) { runtime := &asked{answer: func(_ string, args []string) (string, error) {
if args[0] == "inspect" { if args[0] == "inspect" {
return "true running\n", nil return "true running\n", nil
@@ -80,16 +80,16 @@ func TestASubstrateThatIsUpAndAnsweringIsAccepted(t *testing.T) {
return "1 node, 0 waiting\n", nil return "1 node, 0 waiting\n", nil
}} }}
verified, err := Verify(context.Background(), substrate(t), runtime.run, verified, err := Verify(context.Background(), foundation(t), runtime.run,
time.Second, 0, func(string) {}) time.Second, 0, func(string) {})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Three long-running containers: the store, the broker and the TEMPORARY control plane. The // Three long-running containers: the store, the broker and the TEMPORARY control plane. The
// run-once and scheduled shapes are excluded on purpose — a step that has exited is not a // run-once and scheduled shapes are excluded on purpose — a step that has exited is not a
// fault. The name is `temp-mesh-control` because the permanent one is a module and takes the // fault. The name is `temp-mesh-controller` because the permanent one is a module and takes the
// plain name (novox/hq ADR 0067), which is what makes the two of them coexist at all. // plain name (novox/hq ADR 0067), which is what makes the two of them coexist at all.
want := []string{"mesh-store", "mesh-broker", "temp-mesh-control"} want := []string{"mesh-store", "mesh-broker", "temp-mesh-controller"}
if len(verified.Running) != len(want) { if len(verified.Running) != len(want) {
t.Fatalf("confirmed %v running, want %v", verified.Running, want) t.Fatalf("confirmed %v running, want %v", verified.Running, want)
} }
@@ -122,7 +122,7 @@ func TestAControlPlaneThatIsStillStartingIsWaitedFor(t *testing.T) {
return "1 node\n", nil return "1 node\n", nil
}} }}
if _, err := Verify(context.Background(), substrate(t), runtime.run, if _, err := Verify(context.Background(), foundation(t), runtime.run,
time.Second, time.Second, func(string) {}); err != nil { time.Second, time.Second, func(string) {}); err != nil {
t.Fatalf("a control plane that answered on the third ask was refused: %v", err) t.Fatalf("a control plane that answered on the third ask was refused: %v", err)
} }
@@ -141,10 +141,10 @@ func TestAContainerThatExitedIsNamedWithItsState(t *testing.T) {
return "", fmt.Errorf("unexpected command: %v", args) return "", fmt.Errorf("unexpected command: %v", args)
}} }}
_, err := Verify(context.Background(), substrate(t), runtime.run, _, err := Verify(context.Background(), foundation(t), runtime.run,
time.Second, 0, func(string) {}) time.Second, 0, func(string) {})
if err == nil { if err == nil {
t.Fatal("a container that had exited was reported as part of a running substrate") t.Fatal("a container that had exited was reported as part of a running foundation")
} }
if !strings.Contains(err.Error(), "mesh-broker") || !strings.Contains(err.Error(), "exited") { if !strings.Contains(err.Error(), "mesh-broker") || !strings.Contains(err.Error(), "exited") {
t.Errorf("the failure does not say which container is in what state: %v", err) t.Errorf("the failure does not say which container is in what state: %v", err)
@@ -160,11 +160,11 @@ func TestTheControlPlaneIsAskedByRunningItsOwnBinary(t *testing.T) {
} }
return "1 node\n", nil return "1 node\n", nil
}} }}
if _, err := Verify(context.Background(), substrate(t), runtime.run, if _, err := Verify(context.Background(), foundation(t), runtime.run,
time.Second, 0, func(string) {}); err != nil { time.Second, 0, func(string) {}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !runtime.ran("docker exec " + TempPrefix + "mesh-control " + controlPlaneBinary + " status") { if !runtime.ran("docker exec " + TempPrefix + "mesh-controller " + controlPlaneBinary + " status") {
t.Errorf("the control plane was never asked anything: %v", runtime.commands) t.Errorf("the control plane was never asked anything: %v", runtime.commands)
} }
} }
+4 -4
View File
@@ -27,13 +27,13 @@ import (
// nothing and reporting success — a host that silently did nothing on a first node would look // nothing and reporting success — a host that silently did nothing on a first node would look
// exactly like one that worked. // exactly like one that worked.
// //
//go:embed substrate-arch.lock //go:embed foundation-arch.lock
var archLock []byte var archLock []byte
//go:embed substrate-alpine.lock //go:embed foundation-alpine.lock
var alpineLock []byte var alpineLock []byte
//go:embed substrate-android.lock //go:embed foundation-android.lock
var androidLock []byte var androidLock []byte
var locks = map[string][]byte{ var locks = map[string][]byte{
@@ -52,7 +52,7 @@ func Raw(system string) []byte { return locks[system] }
// IsEmpty reports whether anything was built in. A bundle of only comments and whitespace is // IsEmpty reports whether anything was built in. A bundle of only comments and whitespace is
// empty for this purpose: a placeholder is a comment, and treating it as content would mean a // empty for this purpose: a placeholder is a comment, and treating it as content would mean a
// host claims to carry a substrate it does not. // host claims to carry a foundation it does not.
func IsEmpty(system string) bool { func IsEmpty(system string) bool {
for _, line := range strings.Split(string(locks[system]), "\n") { for _, line := range strings.Split(string(locks[system]), "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
+1 -1
View File
@@ -13,7 +13,7 @@ func TestADefaultBuildCarriesNothingAndSaysSo(t *testing.T) {
// success would look exactly like a host that raised a first node — and the difference // success would look exactly like a host that raised a first node — and the difference
// would surface as a mesh that never came up, with nothing to point at. // would surface as a mesh that never came up, with nothing to point at.
if !IsEmpty("arch") { if !IsEmpty("arch") {
t.Fatal("the default build claims to carry a substrate") t.Fatal("the default build claims to carry a foundation")
} }
_, err := Load("arch") _, err := Load("arch")
if !errors.Is(err, ErrEmpty) { if !errors.Is(err, ErrEmpty) {
@@ -1,4 +1,4 @@
// substrate-alpine.lock — the pinned tier-1 descriptor the ALPINE host carries. // foundation-alpine.lock — the pinned tier-1 descriptor the ALPINE host carries.
// //
// Per system, because its CONTENTS are: this one names apk packages and OpenRC services where // Per system, because its CONTENTS are: this one names apk packages and OpenRC services where
// the arch bundle names pacman packages and systemd units (novox/hq ADR 0005). // the arch bundle names pacman packages and systemd units (novox/hq ADR 0005).
@@ -1,10 +1,10 @@
// substrate-android.lock — deliberately not a bundle. // foundation-android.lock — deliberately not a bundle.
// //
// An android host cannot raise a mesh, and this file says so rather than being an empty // An android host cannot raise a mesh, and this file says so rather than being an empty
// placeholder waiting to be filled in. // placeholder waiting to be filled in.
// //
// The substrate is a container runtime, a store and the control plane (novox/hq // The foundation is a container runtime, a store and the control plane (novox/hq
// 07-the-substrate.md). An android host implements `file`, `directory` and `action` and refuses // 07-the-foundation.md). An android host implements `file`, `directory` and `action` and refuses
// `package`, `container` and `service` (ADR 0005) — so every step of the bootstrap is a shape it // `package`, `container` and `service` (ADR 0005) — so every step of the bootstrap is a shape it
// does not have. No amount of filling this in changes that. // does not have. No amount of filling this in changes that.
// //
@@ -1,4 +1,4 @@
// substrate-arch.lock — the pinned tier-1 descriptor the ARCH host carries. // foundation-arch.lock — the pinned tier-1 descriptor the ARCH host carries.
// //
// Per system, because its CONTENTS are: package names, unit names and service names all differ // Per system, because its CONTENTS are: package names, unit names and service names all differ
// (novox/hq ADR 0005). The mechanism is shared; what it names is not. // (novox/hq ADR 0005). The mechanism is shared; what it names is not.
+1 -1
View File
@@ -1076,7 +1076,7 @@ func vocabulary() string {
// ParseFileTrusted reads a declaration from a file somebody handed this host. // ParseFileTrusted reads a declaration from a file somebody handed this host.
// //
// The same as ParseTrusted, and it allows whole-line `//` comments first. A pinned, hand-authored // The same as ParseTrusted, and it allows whole-line `//` comments first. A pinned, hand-authored
// artefact that nobody can annotate is one nobody can review — the substrate bundle is mostly // artefact that nobody can annotate is one nobody can review — the foundation bundle is mostly
// explanation of why each digest is what it is. // explanation of why each digest is what it is.
// //
// **Only for a file, never for the link.** Over the link the format stays exactly JSON, because // **Only for a file, never for the link.** Over the link the format stays exactly JSON, because
+5 -5
View File
@@ -157,7 +157,7 @@ func TestAnEmptyDeclarationIsAMistake(t *testing.T) {
refusalFor(t, `{"declaration":1,"resources":[]}`) refusalFor(t, `{"declaration":1,"resources":[]}`)
} }
// --- the vocabulary the substrate bootstrap needs (novox/hq 07-the-substrate.md) --- // --- the vocabulary the foundation bootstrap needs (novox/hq 07-the-foundation.md) ---
func TestAnActionOverTheLinkIsRefused(t *testing.T) { func TestAnActionOverTheLinkIsRefused(t *testing.T) {
// novox/hq ADR 0005. The link may push declarations of known shape and never a command to // novox/hq ADR 0005. The link may push declarations of known shape and never a command to
@@ -229,7 +229,7 @@ func TestAnImageMustBePinnedByDigest(t *testing.T) {
func TestAnImageTheMachineHoldsIsNamedByItsOwnDigest(t *testing.T) { func TestAnImageTheMachineHoldsIsNamedByItsOwnDigest(t *testing.T) {
held := "sha256:" + strings.Repeat("b", 64) held := "sha256:" + strings.Repeat("b", 64)
if _, err := ParseTrusted([]byte(`{"declaration":1,"resources":[ if _, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"control","type":"container","name":"mesh-control","image":"` + held + `"} {"id":"control","type":"container","name":"mesh-controller","image":"` + held + `"}
]}`)); err != nil { ]}`)); err != nil {
t.Errorf("an image named by its own digest was refused: %v", err) t.Errorf("an image named by its own digest was refused: %v", err)
} }
@@ -238,7 +238,7 @@ func TestAnImageTheMachineHoldsIsNamedByItsOwnDigest(t *testing.T) {
// whichever the runtime happened to match first. // whichever the runtime happened to match first.
for _, bad := range []string{"sha256:abc", "sha256:", "sha256:" + strings.Repeat("b", 63)} { for _, bad := range []string{"sha256:abc", "sha256:", "sha256:" + strings.Repeat("b", 63)} {
if _, err := ParseTrusted([]byte(`{"declaration":1,"resources":[ if _, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"control","type":"container","name":"mesh-control","image":"` + bad + `"} {"id":"control","type":"container","name":"mesh-controller","image":"` + bad + `"}
]}`)); err == nil { ]}`)); err == nil {
t.Errorf("image id %q was accepted and is not a digest", bad) t.Errorf("image id %q was accepted and is not a digest", bad)
} }
@@ -267,7 +267,7 @@ func TestAFieldTheNewTypesDoNotUseIsRefused(t *testing.T) {
} }
func TestTheVocabularyIsTheElevenShapesTheMeshNeeds(t *testing.T) { func TestTheVocabularyIsTheElevenShapesTheMeshNeeds(t *testing.T) {
// Six of them the bootstrap uses (novox/hq 07-the-substrate.md), and removing one is a // Six of them the bootstrap uses (novox/hq 07-the-foundation.md), and removing one is a
// failing test rather than a discovery during a first-node install. // failing test rather than a discovery during a first-node install.
// //
// Two were added on 2026-08-30 and the count is asserted precisely because adding one is a // Two were added on 2026-08-30 and the count is asserted precisely because adding one is a
@@ -364,7 +364,7 @@ func TestSomethingInsideAValueIsNotAComment(t *testing.T) {
// parses as the declaration and the rest is never looked at. The machine applies something, // parses as the declaration and the rest is never looked at. The machine applies something,
// reports success, and what it applied is not what the file says. // reports success, and what it applied is not what the file says.
// //
// Not hypothetical: a test harness appended a line to the substrate bundle by accident, every // Not hypothetical: a test harness appended a line to the foundation bundle by accident, every
// apply kept working, and nothing said so for the entire time it was wrong. // apply kept working, and nothing said so for the entire time it was wrong.
func TestSomethingAfterTheDeclarationIsRefused(t *testing.T) { func TestSomethingAfterTheDeclarationIsRefused(t *testing.T) {
good := `{"declaration":1,"resources":[{"id":"a","type":"file","path":"/tmp/a",` + good := `{"declaration":1,"resources":[{"id":"a","type":"file","path":"/tmp/a",` +
+1 -1
View File
@@ -2,7 +2,7 @@ This is not a saved image. It is the placeholder that keeps this repository buil
A release build replaces this file with the output of `docker save` and puts it back afterwards: A release build replaces this file with the output of `docker save` and puts it back afterwards:
make bootstrap IMAGE=mesh-control:<version> make bootstrap IMAGE=mesh-controller:<version>
An installer built with this file present carries no control plane, and says so in preflight An installer built with this file present carries no control plane, and says so in preflight
rather than getting a machine part-way to being a mesh and stopping. rather than getting a machine part-way to being a mesh and stopping.
+1 -1
View File
@@ -58,7 +58,7 @@ var saved []byte
var ErrEmpty = errors.New( var ErrEmpty = errors.New(
"this mesh-bootstrap carries no builder image, so it cannot raise a mesh. A release build " + "this mesh-bootstrap carries no builder image, so it cannot raise a mesh. A release build " +
"embeds one: `make bootstrap IMAGE=<image>` in the mesh-host repository, where <image> " + "embeds one: `make bootstrap IMAGE=<image>` in the mesh-host repository, where <image> " +
"is a mesh-builder image already built from the mesh-control source") "is a mesh-builder image already built from the mesh-controller source")
// IsEmpty reports whether anything was built in. // IsEmpty reports whether anything was built in.
// //
+3 -3
View File
@@ -72,11 +72,11 @@ func TestTheArchivesOwnIdIsReadFromTheSavedFile(t *testing.T) {
// does not. // does not.
func TestTheSavedTagsAreRead(t *testing.T) { func TestTheSavedTagsAreRead(t *testing.T) {
saved := savedImage(t, map[string]string{ saved := savedImage(t, map[string]string{
"manifest.json": manifest(t, strings.Repeat("b", 64)+".json", "mesh-control:v1"), "manifest.json": manifest(t, strings.Repeat("b", 64)+".json", "mesh-controller:v1"),
}) })
got := Tags(saved) got := Tags(saved)
if len(got) != 1 || got[0] != "mesh-control:v1" { if len(got) != 1 || got[0] != "mesh-controller:v1" {
t.Errorf("tags = %v, want [mesh-control:v1]", got) t.Errorf("tags = %v, want [mesh-controller:v1]", got)
} }
} }
+1 -1
View File
@@ -60,7 +60,7 @@ type Report struct {
// Carried are the machine's ports held by what this host raised from its own bundle. // Carried are the machine's ports held by what this host raised from its own bundle.
// //
// **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node // **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node
// raises its substrate before any mesh exists, so the control plane has never heard of the // raises its foundation before any mesh exists, so the control plane has never heard of the
// store or the broker — and would hand a module a port one of them holds, discovering it only // store or the broker — and would hand a module a port one of them holds, discovering it only
// when a container runtime refused to start. // when a container runtime refused to start.
// //
+3 -3
View File
@@ -35,7 +35,7 @@ type Applied struct {
Type string `json:"type"` Type string `json:"type"`
// Origin is who asked for this: the bundle this host carries, or the mesh. // Origin is who asked for this: the bundle this host carries, or the mesh.
// //
// Recorded because the two must not remove each other. A node raises its own substrate from // Recorded because the two must not remove each other. A node raises its own foundation from
// the bundle before any mesh exists, then enrols and is sent declarations — and a // the bundle before any mesh exists, then enrols and is sent declarations — and a
// declaration naming two resources would otherwise remove the store, the broker and the // declaration naming two resources would otherwise remove the store, the broker and the
// control plane, which is 04-ISSUES/010 and happened on the first end-to-end run. // control plane, which is 04-ISSUES/010 and happened on the first end-to-end run.
@@ -47,7 +47,7 @@ type Applied struct {
// Holds are the machine's own ports this resource occupies. // Holds are the machine's own ports this resource occupies.
// //
// **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node // **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node
// raises its substrate from the bundle before any mesh exists, so the control plane has never // raises its foundation from the bundle before any mesh exists, so the control plane has never
// heard of the store, the broker or the control plane's own container — and a module assigned // heard of the store, the broker or the control plane's own container — and a module assigned
// afterwards would be given a port one of them already holds, and would be told so by a // afterwards would be given a port one of them already holds, and would be told so by a
// container runtime rather than by anything that could have prevented it. // container runtime rather than by anything that could have prevented it.
@@ -226,7 +226,7 @@ const (
// //
// State written before origins existed was all bundle-applied: a host had no other way to be // State written before origins existed was all bundle-applied: a host had no other way to be
// told anything. Guessing wrong in the other direction would have a first upgrade remove the // told anything. Guessing wrong in the other direction would have a first upgrade remove the
// substrate, which is the fault this field exists to prevent. // foundation, which is the fault this field exists to prevent.
func originOf(r Applied) string { func originOf(r Applied) string {
if r.Origin == "" { if r.Origin == "" {
return OriginCarried return OriginCarried
+2 -2
View File
@@ -136,7 +136,7 @@ func TestNothingIsAnOrphanWhenEverythingIsDeclared(t *testing.T) {
} }
func TestADeclarationDoesNotOrphanWhatTheBundleRaised(t *testing.T) { func TestADeclarationDoesNotOrphanWhatTheBundleRaised(t *testing.T) {
// 04-ISSUES/010. A first node raises its substrate from the bundle it carries, then enrols // 04-ISSUES/010. A first node raises its foundation from the bundle it carries, then enrols
// and is sent a declaration naming two resources. Before origins, that removed the store, the // and is sent a declaration naming two resources. Before origins, that removed the store, the
// broker and the control plane that had sent it — the mesh deleting itself over the link the // broker and the control plane that had sent it — the mesh deleting itself over the link the
// message arrived on, in under a second, on the first end-to-end run. // message arrived on, in under a second, on the first end-to-end run.
@@ -175,7 +175,7 @@ func TestTheBundleDoesNotOrphanWhatTheMeshDeclared(t *testing.T) {
func TestStateWrittenBeforeOriginsExistedIsTreatedAsCarried(t *testing.T) { func TestStateWrittenBeforeOriginsExistedIsTreatedAsCarried(t *testing.T) {
// Every resource a host had applied before this field existed came from its bundle, because // Every resource a host had applied before this field existed came from its bundle, because
// there was no other way to tell it anything. Guessing the other way would have the first // there was no other way to tell it anything. Guessing the other way would have the first
// declaration remove the substrate — which is the fault this exists to prevent, arriving // declaration remove the foundation — which is the fault this exists to prevent, arriving
// through the upgrade that fixes it. // through the upgrade that fixes it.
s := State{Resources: []Applied{{ID: "store", Type: "container", Target: "mesh-store"}}} s := State{Resources: []Applied{{ID: "store", Type: "container", Target: "mesh-store"}}}
+1 -1
View File
@@ -35,7 +35,7 @@ import (
// while disconnected, reconcile already happens on start, and the mesh already reports *last // while disconnected, reconcile already happens on start, and the mesh already reports *last
// heard from* rather than alarming on silence. // heard from* rather than alarming on silence.
// //
// It also **cannot be the first node** — every step of raising a substrate is a shape it // It also **cannot be the first node** — every step of raising a foundation is a shape it
// refuses — and its bundle says so rather than being an empty placeholder. // refuses — and its bundle says so rather than being an empty placeholder.
type android struct{} type android struct{}