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:
@@ -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 {
|
||||
t.Helper()
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// Runner is the same runner every applier in this repository takes.
|
||||
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
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
func refuseSealed(string) ([]byte, error) {
|
||||
return nil, errors.New(
|
||||
"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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
func TestASealedFileInASubstrateIsRefusedWithAReason(t *testing.T) {
|
||||
func TestASealedFileInAFoundationIsRefusedWithAReason(t *testing.T) {
|
||||
_, err := refuseSealed("anything")
|
||||
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") {
|
||||
t.Errorf("the refusal does not say why there is no key: %v", err)
|
||||
|
||||
@@ -111,7 +111,7 @@ func failed(step Step, err error) error {
|
||||
|
||||
// Options are the things that differ between machines.
|
||||
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
|
||||
// Out is where the produced bundle is written, so a person can read what was applied.
|
||||
Out string
|
||||
@@ -128,12 +128,12 @@ type Options struct {
|
||||
// runtime, a control plane opening its stores.
|
||||
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.
|
||||
Node string
|
||||
|
||||
// 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
|
||||
// looks installed and cannot upgrade itself.
|
||||
Catalogue string
|
||||
@@ -181,7 +181,7 @@ type Options struct {
|
||||
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) != "" }
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
// Answered is what the temporary control plane said back — not merely that it is up.
|
||||
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"`
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// **What makes the last part expressible is a name.** The substrate's control plane is called
|
||||
// `temp-mesh-control` and the module's is called `mesh-control`. Two containers, two owners:
|
||||
// **What makes the last part expressible is a name.** The foundation's control plane is called
|
||||
// `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
|
||||
// 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:
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
@@ -458,7 +458,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
o.Out, rewritten.Resources))
|
||||
|
||||
// ---- 4. apply -----------------------------------------------------------------------
|
||||
say("apply — raising the substrate")
|
||||
say("apply — raising the foundation")
|
||||
report, err := ApplyBundle(ctx, o, sys, rewritten.Declaration, d.Run, say)
|
||||
result.Applied, result.Changed = len(report.Outcomes), report.Changed()
|
||||
if err != nil {
|
||||
@@ -472,7 +472,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
}
|
||||
|
||||
// ---- 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)
|
||||
result.Running, result.Answered = verified.Running, verified.Answered
|
||||
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
|
||||
// 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.
|
||||
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 " +
|
||||
"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 " +
|
||||
@@ -497,7 +497,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
// ---- 6. enrol -------------------------------------------------------------------------
|
||||
//
|
||||
// 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.
|
||||
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 ------------------------------------------------------------------------
|
||||
// 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.
|
||||
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 {
|
||||
return result, failed(StepStore, err)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
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 {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// storeFileSuffix is how a manifest asks for a store connection in a file rather than in the
|
||||
// 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
|
||||
// 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.
|
||||
@@ -41,20 +41,20 @@ type Permanent struct {
|
||||
// **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
|
||||
// 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
|
||||
// the one composing it is called `temp-mesh-control`, so there are two of them and neither is in
|
||||
// what makes the whole thing expressible: the container being created is called `mesh-controller` and
|
||||
// the one composing it is called `temp-mesh-controller`, so there are two of them and neither is in
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
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}
|
||||
|
||||
@@ -63,7 +63,7 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
|
||||
return out, fmt.Errorf(
|
||||
"%w\n"+
|
||||
"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 "+
|
||||
"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.
|
||||
delivered, err := deliverStores(ctx, o, control, pinned, substrate, say)
|
||||
delivered, err := deliverStores(ctx, o, control, pinned, foundation, say)
|
||||
out.Delivered = delivered
|
||||
if err != nil {
|
||||
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
|
||||
// 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
|
||||
// report the temporary one's health as the permanent one's.
|
||||
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
|
||||
// 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.
|
||||
var out bytes.Buffer
|
||||
rest := manifest
|
||||
@@ -215,21 +215,21 @@ func controlPlaneResourceIn(manifest []byte) string {
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// to make. A store connection missing from the substrate is the one exception and is an error —
|
||||
// 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 foundation is the one exception and is an error —
|
||||
// a control plane that cannot open a context is not a control plane.
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -246,7 +246,7 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
|
||||
ControlPlaneModule, storeVariablePrefix, storeVariablePrefix, storeFileSuffix)
|
||||
}
|
||||
|
||||
temporary, err := controlPlaneIn(substrate)
|
||||
temporary, err := controlPlaneIn(foundation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -260,13 +260,13 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
|
||||
return delivered, fmt.Errorf(
|
||||
"the %s module wants %s and the bundle this installer produced does not name "+
|
||||
"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",
|
||||
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.
|
||||
say(" the mesh will make " + secret + " — the substrate names no " + variable)
|
||||
say(" the mesh will make " + secret + " — the foundation names no " + variable)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
|
||||
return delivered, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -9,37 +9,37 @@ import (
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// the container's, and the environment file that fills what is not a path.
|
||||
const theControlPlaneModule = `{
|
||||
"module": "mesh-control",
|
||||
"module": "mesh-controller",
|
||||
"version": "1",
|
||||
"slug": "control",
|
||||
"capabilities": ["container-runtime"],
|
||||
"claims": [{"name": "the-control-plane", "scope": "mesh"}],
|
||||
"claims": [{"name": "the-controller", "scope": "mesh"}],
|
||||
"own-secrets": {
|
||||
"inventory": "/var/lib/mesh/mesh-control/inventory",
|
||||
"identity": "/var/lib/mesh/mesh-control/identity",
|
||||
"licences": "/var/lib/mesh/mesh-control/licences",
|
||||
"broker": "/var/lib/mesh/mesh-control/broker",
|
||||
"broker-management": "/var/lib/mesh/mesh-control/broker-management"
|
||||
"inventory": "/var/lib/mesh/mesh-controller/inventory",
|
||||
"identity": "/var/lib/mesh/mesh-controller/identity",
|
||||
"licences": "/var/lib/mesh/mesh-controller/licences",
|
||||
"broker": "/var/lib/mesh/mesh-controller/broker",
|
||||
"broker-management": "/var/lib/mesh/mesh-controller/broker-management"
|
||||
},
|
||||
"resources": [
|
||||
{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"},
|
||||
{"id": "broker-env", "type": "file", "path": "/var/lib/mesh/mesh-control/broker.env",
|
||||
{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},
|
||||
{"id": "broker-env", "type": "file", "path": "/var/lib/mesh/mesh-controller/broker.env",
|
||||
"mode": "0600",
|
||||
"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",
|
||||
"image": "mesh-control@` + placeholderDigest + `",
|
||||
{"id": "server", "type": "container", "name": "mesh-controller",
|
||||
"image": "mesh-controller@` + placeholderDigest + `",
|
||||
"network": "host", "args": ["serve"],
|
||||
"env-file": ["/var/lib/mesh/mesh-control/broker.env"],
|
||||
"env-file": ["/var/lib/mesh/mesh-controller/broker.env"],
|
||||
"env": {
|
||||
"MESH_STORE_INVENTORY_FILE": "/run/secrets/inventory",
|
||||
"MESH_STORE_IDENTITY_FILE": "/run/secrets/identity",
|
||||
@@ -48,18 +48,18 @@ const theControlPlaneModule = `{
|
||||
},
|
||||
"volumes": [
|
||||
"mesh-broker-tls:/broker-tls:ro",
|
||||
"/var/lib/mesh/mesh-control/inventory:/run/secrets/inventory:ro",
|
||||
"/var/lib/mesh/mesh-control/identity:/run/secrets/identity:ro",
|
||||
"/var/lib/mesh/mesh-control/licences:/run/secrets/licences:ro"
|
||||
"/var/lib/mesh/mesh-controller/inventory:/run/secrets/inventory:ro",
|
||||
"/var/lib/mesh/mesh-controller/identity:/run/secrets/identity: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"
|
||||
|
||||
// **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
|
||||
// control plane exists in no public registry by design.
|
||||
func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
|
||||
@@ -73,7 +73,7 @@ func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
|
||||
if !strings.Contains(string(pinned), `"image": "`+pushedReference+`"`) {
|
||||
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 "+
|
||||
"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.
|
||||
func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
|
||||
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-control", "mode": "0700"},
|
||||
{"id": "migrate", "type": "container", "name": "mesh-control-migrate", "run-once": true,
|
||||
"image": "mesh-control@`+placeholderDigest+`", "args": ["migrate"]},`, 1)
|
||||
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},`,
|
||||
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},
|
||||
{"id": "migrate", "type": "container", "name": "mesh-controller-migrate", "run-once": true,
|
||||
"image": "mesh-controller@`+placeholderDigest+`", "args": ["migrate"]},`, 1)
|
||||
|
||||
pinned, places, err := pinImage([]byte(twice), pushedReference)
|
||||
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 mesh cannot invent them: they are the credentials the substrate created the databases with,
|
||||
// **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 foundation created the databases with,
|
||||
// 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
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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,
|
||||
[]byte(theControlPlaneModule), rewritten.Declaration, func(string) {})
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) {
|
||||
func TestASecretTheFoundationNeverMadeIsLeftToTheMesh(t *testing.T) {
|
||||
extra := strings.Replace(theControlPlaneModule,
|
||||
`"broker": "/var/lib/mesh/mesh-control/broker",`,
|
||||
`"broker": "/var/lib/mesh/mesh-control/broker",
|
||||
"something-new": "/var/lib/mesh/mesh-control/something-new",`, 1)
|
||||
`"broker": "/var/lib/mesh/mesh-controller/broker",`,
|
||||
`"broker": "/var/lib/mesh/mesh-controller/broker",
|
||||
"something-new": "/var/lib/mesh/mesh-controller/something-new",`, 1)
|
||||
extra = strings.Replace(extra,
|
||||
`"content": "MESH_BROKER_AMQP=${secret:broker}\n`,
|
||||
`"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)
|
||||
}
|
||||
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
|
||||
delivered, err := deliverStores(context.Background(), Options{Node: "anchor"}, control,
|
||||
@@ -192,7 +192,7 @@ func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) {
|
||||
}
|
||||
for _, secret := range delivered {
|
||||
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") {
|
||||
@@ -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.
|
||||
func TestAConnectionFileNothingWritesIsRefused(t *testing.T) {
|
||||
mismatched := strings.Replace(theControlPlaneModule,
|
||||
`"inventory": "/var/lib/mesh/mesh-control/inventory",`,
|
||||
`"inventory": "/var/lib/mesh/mesh-control/somewhere-else",`, 1)
|
||||
`"inventory": "/var/lib/mesh/mesh-controller/inventory",`,
|
||||
`"inventory": "/var/lib/mesh/mesh-controller/somewhere-else",`, 1)
|
||||
|
||||
_, err := secretsByVariableIn([]byte(mismatched))
|
||||
if err == nil {
|
||||
@@ -226,11 +226,11 @@ func TestAManifestWantingNoStoresIsRefusedWithTheShapeItShouldHave(t *testing.T)
|
||||
t.Fatal(err)
|
||||
}
|
||||
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":[
|
||||
{"id":"container","type":"container","name":"mesh-control",
|
||||
"image":"mesh-control@` + placeholderDigest + `"}]}`
|
||||
bare := `{"module":"mesh-controller","version":"1","resources":[
|
||||
{"id":"container","type":"container","name":"mesh-controller",
|
||||
"image":"mesh-controller@` + placeholderDigest + `"}]}`
|
||||
|
||||
_, err = deliverStores(context.Background(), Options{Node: "anchor"}, control,
|
||||
[]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
|
||||
// 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) {
|
||||
runtime := &asked{answer: aMeshThatAgrees(map[string]string{
|
||||
"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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -265,11 +265,11 @@ func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) {
|
||||
if out.Answered != "1 node, 0 waiting" {
|
||||
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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestAMachineThatHasAlreadyEnrolledIsNotEnrolledAgain(t *testing.T) {
|
||||
out, err := Enrol(context.Background(), Options{
|
||||
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
|
||||
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) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -109,7 +109,7 @@ func TestAMeshThatHasHeardFromAMachineWithNoAgentStartsOne(t *testing.T) {
|
||||
out, err := Enrol(context.Background(), Options{
|
||||
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
|
||||
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) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -135,7 +135,7 @@ func TestAMachineEnrolledUnderAnotherNameIsRefused(t *testing.T) {
|
||||
|
||||
_, err := Enrol(context.Background(), Options{
|
||||
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) {})
|
||||
if err == nil {
|
||||
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{
|
||||
Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service",
|
||||
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) {})
|
||||
if err == nil {
|
||||
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{
|
||||
Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service",
|
||||
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) {})
|
||||
if err == nil {
|
||||
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")
|
||||
}}
|
||||
_, 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 {
|
||||
t.Fatal("a machine with no name was enrolled")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// 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).
|
||||
tarball, err := os.CreateTemp("", "mesh-control-*.tar")
|
||||
tarball, err := os.CreateTemp("", "mesh-controller-*.tar")
|
||||
if err != nil {
|
||||
return loaded, fmt.Errorf("nowhere to put the carried image while loading it: %w", err)
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ func (a *asked) ran(fragment string) bool {
|
||||
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.
|
||||
func savedImageFixture(t *testing.T, digest string, tags ...string) []byte {
|
||||
t.Helper()
|
||||
if tags == nil {
|
||||
tags = []string{"mesh-control:test"}
|
||||
tags = []string{"mesh-controller:test"}
|
||||
}
|
||||
entries, err := json.Marshal([]struct {
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
const (
|
||||
fixtureDigest = "3333333333333333333333333333333333333333333333333333333333333333"
|
||||
@@ -107,7 +107,7 @@ func TestTheIdComesFromTheRuntimeAndNotFromTheArchive(t *testing.T) {
|
||||
// Loaded — and stored under a configuration of the runtime's own making.
|
||||
return "sha256:" + runtimeDigest + "\n", nil
|
||||
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)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func TestTheIdComesFromTheRuntimeAndNotFromTheArchive(t *testing.T) {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
// 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" {
|
||||
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,
|
||||
@@ -192,7 +192,7 @@ func TestALoadThatLeftNothingBehindIsAFailure(t *testing.T) {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
func TestARuntimeAnsweringSomethingThatIsNotAnImageIdIsRefused(t *testing.T) {
|
||||
for _, nonsense := range []string{
|
||||
"mesh-control:test",
|
||||
"mesh-controller:test",
|
||||
"sha256:" + strings.Repeat("9", 63),
|
||||
"<no value>",
|
||||
} {
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// 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
|
||||
// upsert on the manifest, and the manifest is exactly what changes between runs — step 9 registers
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// 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
|
||||
// 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).
|
||||
//
|
||||
// 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.
|
||||
|
||||
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).
|
||||
substrateStore = "mesh-store"
|
||||
foundationStore = "mesh-store"
|
||||
// giteaBootstrap is the gitea server raised directly at genesis, before gitea is a module.
|
||||
giteaBootstrap = "mesh-gitea-server"
|
||||
// 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
|
||||
// the `as` the builder's static package binding names.
|
||||
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"
|
||||
giteaDBName = "mesh_gitea"
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -106,8 +106,8 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedGiteaDatabase creates gitea's role and database inside the substrate postgres, the same way
|
||||
// the substrate creates its own — psql run through the store container (the map's Route B). The role
|
||||
// seedGiteaDatabase creates gitea's role and database inside the foundation postgres, the same way
|
||||
// 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
|
||||
// existing, so a re-run changes nothing.
|
||||
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
|
||||
// quote or backslash to escape inside a SQL literal.
|
||||
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
|
||||
@@ -128,7 +128,7 @@ func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, p
|
||||
if _, err := psql(create); err != nil {
|
||||
alter := fmt.Sprintf("ALTER ROLE %s LOGIN PASSWORD '%s'", giteaDBRole, password)
|
||||
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.
|
||||
present, err := psql(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", giteaDBName))
|
||||
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 _, 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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,
|
||||
@@ -179,7 +179,7 @@ func raiseGiteaServer(ctx context.Context, run Runner, timeout time.Duration, db
|
||||
}
|
||||
args := append([]string{
|
||||
"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
|
||||
// where mesh-bootstrap and the builder's build containers look for it.
|
||||
"--network", "host",
|
||||
|
||||
@@ -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?
|
||||
//
|
||||
// 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
|
||||
// as a running store and a running broker and stop.
|
||||
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))
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"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. 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)
|
||||
}
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
for _, host := range registriesIn(parsed) {
|
||||
dialing, cancel := context.WithTimeout(ctx, o.Timeout)
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestOnlyTheRegistriesTheBundleNamesAreAskedAbout(t *testing.T) {
|
||||
strings.Repeat("7", 64) + `"},
|
||||
{"id":"broker","type":"container","name":"mesh-broker","image":"192.0.2.250:5000/lavinmq@sha256:` +
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
@@ -111,7 +111,7 @@ func TestTheControlPlanesOwnRegistryIsNeverAskedAbout(t *testing.T) {
|
||||
parsed, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[
|
||||
{"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` +
|
||||
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) + `"}
|
||||
]}`))
|
||||
if err != nil {
|
||||
@@ -137,7 +137,7 @@ func TestWhereAnImageWouldBeFetchedFrom(t *testing.T) {
|
||||
{"postgres@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},
|
||||
{"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},
|
||||
// Held by this machine. Nothing serves it, and nothing can.
|
||||
{"sha256:" + strings.Repeat("a", 64), "", false},
|
||||
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// uses the digest that comes back.
|
||||
const genesisTag = "genesis"
|
||||
|
||||
// Published is what step 8 did.
|
||||
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.
|
||||
Reference string
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
@@ -42,7 +42,7 @@ type Published struct {
|
||||
// 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.
|
||||
//
|
||||
// **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
|
||||
// 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
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
|
||||
if !pushed {
|
||||
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) {
|
||||
switch args[0] {
|
||||
@@ -51,7 +51,7 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
|
||||
pushed = true
|
||||
return "", nil
|
||||
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)
|
||||
})
|
||||
@@ -63,10 +63,10 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
|
||||
if out.Already {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -77,11 +77,11 @@ func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
|
||||
func TestAnImageTheRegistryAlreadyServesIsNotPushedAgain(t *testing.T) {
|
||||
o, d, runtime := publishing(t,
|
||||
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) {
|
||||
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)
|
||||
})
|
||||
@@ -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,
|
||||
// which is the dependency the whole pivot exists to remove.
|
||||
func TestTheDigestComesFromThisMeshsOwnRegistry(t *testing.T) {
|
||||
elsewhere := "some.other.registry/mesh-control@sha256:" + strings.Repeat("c", 64)
|
||||
ours := "127.0.0.1:5000/mesh-control@sha256:" + strings.Repeat("d", 64)
|
||||
elsewhere := "some.other.registry/mesh-controller@sha256:" + strings.Repeat("c", 64)
|
||||
ours := "127.0.0.1:5000/mesh-controller@sha256:" + strings.Repeat("d", 64)
|
||||
|
||||
o, d, _ := publishing(t,
|
||||
func(string) (int, string, error) {
|
||||
@@ -167,7 +167,7 @@ func TestATagIsNotAPin(t *testing.T) {
|
||||
},
|
||||
func(_ string, args []string) (string, error) {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ type Registry struct {
|
||||
//
|
||||
// **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
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// 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,
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
// 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
|
||||
// would pass or fail according to what somebody else had checked out.
|
||||
func catalogueWith(t *testing.T, module, manifest string) string {
|
||||
@@ -100,7 +100,7 @@ func TestARegistryContainerThatIsUpIsNotARegistryThatServes(t *testing.T) {
|
||||
|
||||
_, err := InstallRegistry(context.Background(),
|
||||
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) {})
|
||||
if err == nil {
|
||||
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(),
|
||||
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) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -159,7 +159,7 @@ func TestARegistryManifestThatWantsBuildingIsRefused(t *testing.T) {
|
||||
runtime := &asked{answer: aMeshThatAgrees(nil)}
|
||||
_, err := InstallRegistry(context.Background(),
|
||||
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) {})
|
||||
if err == nil {
|
||||
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)}
|
||||
_, err := InstallRegistry(context.Background(),
|
||||
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) {})
|
||||
if err == nil {
|
||||
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"
|
||||
// would throw away the only thing a person can act on.
|
||||
func TestWhatTheMeshRefusedIsRepeated(t *testing.T) {
|
||||
@@ -202,7 +202,7 @@ func TestWhatTheMeshRefusedIsRepeated(t *testing.T) {
|
||||
|
||||
_, err := InstallRegistry(context.Background(),
|
||||
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) {})
|
||||
if err == nil {
|
||||
t.Fatal("a push the mesh refused was reported as successful")
|
||||
|
||||
@@ -32,8 +32,8 @@ type Retired struct {
|
||||
// bundle is applied again, and the removal pass does what it does for every other resource that
|
||||
// leaves a declaration.
|
||||
//
|
||||
// That is the whole of why the rename at step 3 mattered. Had the substrate and the module both
|
||||
// called their container `mesh-control`, this apply would have removed the module's container —
|
||||
// That is the whole of why the rename at step 3 mattered. Had the foundation and the module both
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
bundle, err := removeResource(produced, ControlPlaneID)
|
||||
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.
|
||||
//
|
||||
// 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
|
||||
// treat the rest of the line as a comment and lose a brace.
|
||||
//
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestTheTemporaryControlPlaneLeavesTheBundleAndNothingElseDoes(t *testing.T)
|
||||
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
|
||||
// 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) {
|
||||
if id == ControlPlaneID {
|
||||
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
|
||||
// would lose braces and cut the wrong thing out — silently, because what it produced would still
|
||||
// look like a file.
|
||||
@@ -144,7 +144,7 @@ func TestAContainerStillThereAfterRemovalIsNotGone(t *testing.T) {
|
||||
stillThere := &asked{answer: func(_ string, _ []string) (string, error) {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -153,9 +153,9 @@ func TestAContainerStillThereAfterRemovalIsNotGone(t *testing.T) {
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -19,27 +19,27 @@ import (
|
||||
// broker and no mesh.
|
||||
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
|
||||
// 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
|
||||
// called `mesh-control`: two containers, two owners, nothing shared and nothing to hand over. At
|
||||
// not one. The temporary control plane is called `temp-mesh-controller` and the permanent one is
|
||||
// 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
|
||||
// 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
|
||||
// `temp-mesh-control` has completed it; one running both stopped in the middle; one running only
|
||||
// The name is also the audit. After the pivot, a machine running `mesh-controller` and not
|
||||
// `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.
|
||||
const TempPrefix = "temp-"
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
@@ -85,11 +85,11 @@ type Rewritten struct {
|
||||
// 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
|
||||
// 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
|
||||
// parse-and-re-serialise because
|
||||
// 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
|
||||
// live.
|
||||
//
|
||||
@@ -182,7 +182,7 @@ func Rewrite(template []byte, imageID string) (Rewritten, error) {
|
||||
if produced.Name != out.TempName {
|
||||
return Rewritten{}, fmt.Errorf(
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ func Rewrite(template []byte, imageID string) (Rewritten, error) {
|
||||
return Rewritten{}, fmt.Errorf(
|
||||
"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 "+
|
||||
"substrate gave it", id, wasName[id], name)
|
||||
"foundation gave it", id, wasName[id], name)
|
||||
}
|
||||
sortStrings(out.Kept)
|
||||
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.
|
||||
//
|
||||
// **The quoted name, not the bare word.** `mesh-control` also appears inside the image reference
|
||||
// the template carries (`…/mesh-control@sha256:…`) and could appear inside a command line; a bare
|
||||
// **The quoted name, not the bare word.** `mesh-controller` also appears inside the image reference
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
func renameContainer(bundle []byte, from, to string) ([]byte, error) {
|
||||
if from == to {
|
||||
@@ -235,7 +235,7 @@ func renameContainer(bundle []byte, from, to string) ([]byte, error) {
|
||||
if bytes.Count(bundle, quoted) == 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"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)
|
||||
}
|
||||
return bytes.ReplaceAll(bundle, quoted, []byte(`"`+to+`"`)), nil
|
||||
@@ -257,7 +257,7 @@ func controlPlaneIn(d *declaration.Declaration) (*declaration.Container, error)
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"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), ", "))
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ func sortStrings(values []string) {
|
||||
// writeBundleFile puts the produced bundle where a person can read it, creating the directory it
|
||||
// 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
|
||||
// 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`).
|
||||
|
||||
@@ -15,16 +15,16 @@ const (
|
||||
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
|
||||
// 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.
|
||||
func theRealBundle(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("../../examples/substrate-first-node.lock")
|
||||
raw, err := os.ReadFile("../../examples/foundation-first-node.lock")
|
||||
if err != nil {
|
||||
t.Fatalf("reading the substrate example: %v", err)
|
||||
t.Fatalf("reading the foundation example: %v", err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestTheControlPlaneIsNamedByTheImageThisMachineHolds(t *testing.T) {
|
||||
|
||||
// **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
|
||||
// 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.
|
||||
@@ -60,7 +60,7 @@ func TestEveryPlaceTheBundleNamesTheControlPlaneIsRewritten(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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"} {
|
||||
image, named := produced[id]
|
||||
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
|
||||
// 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) {
|
||||
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 {
|
||||
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
|
||||
// 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.
|
||||
func TestTheProducedBundleKeepsTheTemplatesComments(t *testing.T) {
|
||||
template := theRealBundle(t)
|
||||
@@ -194,11 +194,11 @@ func TestTheProducedBundleKeepsTheTemplatesComments(t *testing.T) {
|
||||
func TestSomethingThatIsNotAnImageIdIsRefused(t *testing.T) {
|
||||
for _, bad := range []string{
|
||||
"",
|
||||
"mesh-control:latest",
|
||||
"mesh-controller:latest",
|
||||
"sha256:abc",
|
||||
"sha256:" + strings.Repeat("1", 63),
|
||||
"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 {
|
||||
t.Errorf("image id %q was accepted", bad)
|
||||
@@ -216,7 +216,7 @@ func TestTheAddressNodesWillDialIsReportedAndNotRewritten(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
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).
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// **This is the test that dissolves the blocker.** The substrate raises a control plane and a
|
||||
// module later declares one; if both are called `mesh-control` then for one moment two owners hold
|
||||
// **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-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
|
||||
// without destroying it. Nothing here invents such a mechanism. The substrate's container is
|
||||
// called `temp-mesh-control` instead, and there are simply two containers.
|
||||
func TestTheSubstratesControlPlaneMovesOutOfTheModulesWay(t *testing.T) {
|
||||
// without destroying it. Nothing here invents such a mechanism. The foundation's container is
|
||||
// called `temp-mesh-controller` instead, and there are simply two containers.
|
||||
func TestTheFoundationsControlPlaneMovesOutOfTheModulesWay(t *testing.T) {
|
||||
out, err := Rewrite(theRealBundle(t), held)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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" {
|
||||
t.Errorf("the substrate's control plane is called %q", out.TempName)
|
||||
if out.TempName != "temp-mesh-controller" {
|
||||
t.Errorf("the foundation's control plane is called %q", out.TempName)
|
||||
}
|
||||
control, err := controlPlaneIn(out.Declaration)
|
||||
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
|
||||
// substitution that caught it would produce `…/temp-mesh-control@sha256:…`, which no registry
|
||||
// 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-controller@sha256:…`, which no registry
|
||||
// serves — and it would be found inside a pull rather than here.
|
||||
func TestTheImageReferenceIsNotMistakenForTheContainerName(t *testing.T) {
|
||||
out, err := Rewrite(theRealBundle(t), held)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(out.Bundle), TempPrefix+"mesh-control@") ||
|
||||
strings.Contains(string(out.Bundle), "/"+TempPrefix+"mesh-control") {
|
||||
if strings.Contains(string(out.Bundle), TempPrefix+"mesh-controller@") ||
|
||||
strings.Contains(string(out.Bundle), "/"+TempPrefix+"mesh-controller") {
|
||||
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
|
||||
// too, and a rename that moved them would leave a machine whose substrate the host cannot find.
|
||||
// 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 foundation the host cannot find.
|
||||
func TestRenamingTheControlPlaneLeavesEveryOtherContainerAlone(t *testing.T) {
|
||||
before, err := declaration.ParseFileTrusted(theRealBundle(t))
|
||||
if err != nil {
|
||||
@@ -309,7 +309,7 @@ func TestRewritingABundleThisAlreadyProducedRenamesNothing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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 {
|
||||
t.Errorf("the second pass calls it %q and the first called it %q",
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
//
|
||||
// **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
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// 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 {
|
||||
container string
|
||||
run Runner
|
||||
@@ -35,9 +35,9 @@ func (c controlPlane) within(timeout time.Duration) controlPlane {
|
||||
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
|
||||
// only "exit status 1" would throw away the one thing a person needs.
|
||||
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.
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
@@ -107,7 +107,7 @@ func indent(s string) string {
|
||||
//
|
||||
// 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
|
||||
// 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 {
|
||||
for _, line := range strings.Split(listing, "\n") {
|
||||
first, _, _ := strings.Cut(strings.TrimSpace(line), " ")
|
||||
|
||||
@@ -13,14 +13,14 @@ import (
|
||||
//
|
||||
// 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
|
||||
// (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.
|
||||
const controlPlaneBinary = "/mesh-control"
|
||||
const controlPlaneBinary = "/mesh-controller"
|
||||
|
||||
// answerEvery is how often the control plane is asked again while it is starting.
|
||||
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 {
|
||||
// Running is every long-running container the bundle declares, confirmed up.
|
||||
Running []string
|
||||
@@ -29,7 +29,7 @@ type Verified struct {
|
||||
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
|
||||
// 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
|
||||
// (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 {
|
||||
var names []string
|
||||
for _, r := range d.Resources {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
func substrate(t *testing.T) *declaration.Declaration {
|
||||
func foundation(t *testing.T) *declaration.Declaration {
|
||||
t.Helper()
|
||||
out, err := Rewrite(theRealBundle(t), held)
|
||||
if err != nil {
|
||||
@@ -39,12 +39,12 @@ func TestAContainerThatIsUpIsNotAControlPlaneThatReplies(t *testing.T) {
|
||||
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) {})
|
||||
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) {
|
||||
t.Errorf("the failure does not mention %q:\n%v", wanted, err)
|
||||
}
|
||||
@@ -65,14 +65,14 @@ func TestAControlPlaneThatSaysNothingHasNotAnswered(t *testing.T) {
|
||||
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 {
|
||||
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.
|
||||
func TestASubstrateThatIsUpAndAnsweringIsAccepted(t *testing.T) {
|
||||
// The foundation answering is the whole point, and what it said is reported rather than asserted.
|
||||
func TestAFoundationThatIsUpAndAnsweringIsAccepted(t *testing.T) {
|
||||
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
||||
if args[0] == "inspect" {
|
||||
return "true running\n", nil
|
||||
@@ -80,16 +80,16 @@ func TestASubstrateThatIsUpAndAnsweringIsAccepted(t *testing.T) {
|
||||
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) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 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
|
||||
// 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.
|
||||
want := []string{"mesh-store", "mesh-broker", "temp-mesh-control"}
|
||||
want := []string{"mesh-store", "mesh-broker", "temp-mesh-controller"}
|
||||
if len(verified.Running) != len(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
|
||||
}}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}}
|
||||
|
||||
_, err := Verify(context.Background(), substrate(t), runtime.run,
|
||||
_, err := Verify(context.Background(), foundation(t), runtime.run,
|
||||
time.Second, 0, func(string) {})
|
||||
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") {
|
||||
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
|
||||
}}
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@ import (
|
||||
// nothing and reporting success — a host that silently did nothing on a first node would look
|
||||
// exactly like one that worked.
|
||||
//
|
||||
//go:embed substrate-arch.lock
|
||||
//go:embed foundation-arch.lock
|
||||
var archLock []byte
|
||||
|
||||
//go:embed substrate-alpine.lock
|
||||
//go:embed foundation-alpine.lock
|
||||
var alpineLock []byte
|
||||
|
||||
//go:embed substrate-android.lock
|
||||
//go:embed foundation-android.lock
|
||||
var androidLock []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
|
||||
// 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 {
|
||||
for _, line := range strings.Split(string(locks[system]), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestADefaultBuildCarriesNothingAndSaysSo(t *testing.T) {
|
||||
// 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.
|
||||
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")
|
||||
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
|
||||
// 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
|
||||
// placeholder waiting to be filled in.
|
||||
//
|
||||
// The substrate 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
|
||||
// The foundation is a container runtime, a store and the control plane (novox/hq
|
||||
// 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
|
||||
// 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
|
||||
// (novox/hq ADR 0005). The mechanism is shared; what it names is not.
|
||||
@@ -1076,7 +1076,7 @@ func vocabulary() string {
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// **Only for a file, never for the link.** Over the link the format stays exactly JSON, because
|
||||
|
||||
@@ -157,7 +157,7 @@ func TestAnEmptyDeclarationIsAMistake(t *testing.T) {
|
||||
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) {
|
||||
// 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) {
|
||||
held := "sha256:" + strings.Repeat("b", 64)
|
||||
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 {
|
||||
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.
|
||||
for _, bad := range []string{"sha256:abc", "sha256:", "sha256:" + strings.Repeat("b", 63)} {
|
||||
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 {
|
||||
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) {
|
||||
// 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.
|
||||
//
|
||||
// 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,
|
||||
// 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.
|
||||
func TestSomethingAfterTheDeclarationIsRefused(t *testing.T) {
|
||||
good := `{"declaration":1,"resources":[{"id":"a","type":"file","path":"/tmp/a",` +
|
||||
|
||||
@@ -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:
|
||||
|
||||
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
|
||||
rather than getting a machine part-way to being a mesh and stopping.
|
||||
|
||||
@@ -58,7 +58,7 @@ var saved []byte
|
||||
var ErrEmpty = errors.New(
|
||||
"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> " +
|
||||
"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.
|
||||
//
|
||||
|
||||
@@ -72,11 +72,11 @@ func TestTheArchivesOwnIdIsReadFromTheSavedFile(t *testing.T) {
|
||||
// does not.
|
||||
func TestTheSavedTagsAreRead(t *testing.T) {
|
||||
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)
|
||||
if len(got) != 1 || got[0] != "mesh-control:v1" {
|
||||
t.Errorf("tags = %v, want [mesh-control:v1]", got)
|
||||
if len(got) != 1 || got[0] != "mesh-controller:v1" {
|
||||
t.Errorf("tags = %v, want [mesh-controller:v1]", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ type Report struct {
|
||||
// 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
|
||||
// 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
|
||||
// when a container runtime refused to start.
|
||||
//
|
||||
|
||||
@@ -35,7 +35,7 @@ type Applied struct {
|
||||
Type string `json:"type"`
|
||||
// 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
|
||||
// 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.
|
||||
@@ -47,7 +47,7 @@ type Applied struct {
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
@@ -226,7 +226,7 @@ const (
|
||||
//
|
||||
// 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
|
||||
// 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 {
|
||||
if r.Origin == "" {
|
||||
return OriginCarried
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestNothingIsAnOrphanWhenEverythingIsDeclared(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
|
||||
// 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.
|
||||
@@ -175,7 +175,7 @@ func TestTheBundleDoesNotOrphanWhatTheMeshDeclared(t *testing.T) {
|
||||
func TestStateWrittenBeforeOriginsExistedIsTreatedAsCarried(t *testing.T) {
|
||||
// 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
|
||||
// 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.
|
||||
s := State{Resources: []Applied{{ID: "store", Type: "container", Target: "mesh-store"}}}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
// while disconnected, reconcile already happens on start, and the mesh already reports *last
|
||||
// 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.
|
||||
type android struct{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user