mesh/merge-gate head 37947ca4eed1 merged into trunk d8e785bb4b4b — pass: the change touches no module of the mesh's graph
mesh/repo-check head 37947ca4eed1 merged into trunk d8e785bb4b4b — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The installer could not raise a first node: the old bundle's control plane had no MESH_BUS_NATS and crash-looped, and the NATS bundle was refused because rootsecrets only knew the previous broker's guest:guest@ template. One bundle now, the NATS one. rootsecrets makes the controller's bus account and puts it in both placeholders (the address and the bcrypt hash in the carried user list), kept with its hash so a re-run writes the same bundle. Preflight refuses a template naming no bus connection it knows, naming the placeholder, instead of waiting out the verify step. The broker step adopts the bus as the nats module and refuses, by name, a catalogue without it or a server that is not the foundation's container.
322 lines
12 KiB
Go
322 lines
12 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// Each test names the decision it defends (novox/hq ADR 0017).
|
|
|
|
const (
|
|
held = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
|
otherHeld = "sha256:2222222222222222222222222222222222222222222222222222222222222222"
|
|
)
|
|
|
|
// 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 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/foundation-first-node.lock")
|
|
if err != nil {
|
|
t.Fatalf("reading the foundation example: %v", err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func TestTheControlPlaneIsNamedByTheImageThisMachineHolds(t *testing.T) {
|
|
out, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !out.Changed {
|
|
t.Error("the rewrite reported nothing changed, and the template named a registry image")
|
|
}
|
|
control, err := controlPlaneIn(out.Declaration)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if control.Image != held {
|
|
t.Errorf("the control plane is %q, want %q", control.Image, held)
|
|
}
|
|
}
|
|
|
|
// **Every place the bundle names that image, not only the container.**
|
|
//
|
|
// 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.
|
|
func TestEveryPlaceTheBundleNamesTheControlPlaneIsRewritten(t *testing.T) {
|
|
template := theRealBundle(t)
|
|
|
|
out, err := Rewrite(template, held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Places < 2 {
|
|
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 {
|
|
t.Errorf("the produced bundle still names %q in %d place(s)", out.Was, remaining)
|
|
}
|
|
if got := strings.Count(string(out.Bundle), held); got != out.Places {
|
|
t.Errorf("the produced bundle names the held image %d time(s), and %d were replaced",
|
|
got, out.Places)
|
|
}
|
|
}
|
|
|
|
// Third-party images are somebody else's, at somebody else's registry, and the installer has no
|
|
// business touching them (novox/hq ADR 0006).
|
|
func TestPostgresAndTheBrokerAreLeftExactlyAsTheyWere(t *testing.T) {
|
|
template := theRealBundle(t)
|
|
|
|
out, err := Rewrite(template, held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
produced := containerImages(out.Declaration)
|
|
for _, id := range []string{"store", "broker"} {
|
|
image, named := produced[id]
|
|
if !named {
|
|
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
|
|
// knows what it said.
|
|
if !strings.Contains(string(template), `"image": "`+image+`"`) {
|
|
t.Errorf("%s is now %q, which the template does not say", id, image)
|
|
}
|
|
if strings.HasPrefix(image, "sha256:") {
|
|
t.Errorf("%s was rewritten to an image this machine holds, and nothing holds it", id)
|
|
}
|
|
}
|
|
|
|
// And the claim in the report is the same claim, so a person reading it is reading evidence.
|
|
if len(out.Kept) != 2 {
|
|
t.Errorf("the rewrite reports %d untouched image(s): %v", len(out.Kept), out.Kept)
|
|
}
|
|
}
|
|
|
|
// A bundle with no control plane raises a store and a broker and no mesh. Refused, because
|
|
// applying it would succeed and leave a machine that looks bootstrapped.
|
|
func TestABundleThatNamesNoControlPlaneIsRefused(t *testing.T) {
|
|
template := []byte(`{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` +
|
|
strings.Repeat("7", 64) + `"}
|
|
]}`)
|
|
|
|
_, err := Rewrite(template, held)
|
|
if err == nil {
|
|
t.Fatal("a bundle with no control plane was rewritten and would have been applied")
|
|
}
|
|
// The refusal has to be actionable: it says what the bundle DID declare, so somebody can see
|
|
// they pointed it at the wrong file or misspelled the id.
|
|
if !strings.Contains(err.Error(), ControlPlaneID) || !strings.Contains(err.Error(), "store") {
|
|
t.Errorf("the refusal names neither what was wanted nor what was there: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAControlPlaneThatIsNotAContainerIsRefused(t *testing.T) {
|
|
template := []byte(`{"declaration":1,"resources":[
|
|
{"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")
|
|
}
|
|
}
|
|
|
|
// Idempotence. This program is run over and over while somebody gets a machine working, and the
|
|
// second run must be able to say the bundle already names this image rather than reporting a
|
|
// rewrite it did not perform.
|
|
func TestRewritingABundleThatAlreadyNamesTheImageChangesNothing(t *testing.T) {
|
|
first, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
second, err := Rewrite(first.Bundle, held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if second.Changed {
|
|
t.Error("re-running the rewrite reported a change, and the image was already the one held")
|
|
}
|
|
if string(second.Bundle) != string(first.Bundle) {
|
|
t.Error("re-running the rewrite produced different bytes")
|
|
}
|
|
if second.Was != held {
|
|
t.Errorf("the second run reports it replaced %q; it replaced nothing", second.Was)
|
|
}
|
|
}
|
|
|
|
// A DIFFERENT image, though, must move — the ordinary case of a new control plane being installed
|
|
// over an old one. "Already correct" must not be the same code path as "already ran".
|
|
func TestANewImageReplacesAnOlderHeldOne(t *testing.T) {
|
|
first, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := Rewrite(first.Bundle, otherHeld)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !second.Changed || second.Was != held || second.Now != otherHeld {
|
|
t.Errorf("a new image did not replace the old one: changed=%v was=%q now=%q",
|
|
second.Changed, second.Was, second.Now)
|
|
}
|
|
}
|
|
|
|
// The produced bundle is meant to be READ. Re-serialising a parsed declaration would drop every
|
|
// 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)
|
|
out, err := Rewrite(template, held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const remembered = "A filter before anything listens"
|
|
if !strings.Contains(string(out.Bundle), remembered) {
|
|
t.Errorf("the produced bundle lost the template's comments; %q is gone", remembered)
|
|
}
|
|
}
|
|
|
|
// The installer must refuse its own bad input in its own words, rather than writing a bundle and
|
|
// letting the host refuse a declaration somebody did not write.
|
|
func TestSomethingThatIsNotAnImageIdIsRefused(t *testing.T) {
|
|
for _, bad := range []string{
|
|
"",
|
|
"mesh-controller:latest",
|
|
"sha256:abc",
|
|
"sha256:" + strings.Repeat("1", 63),
|
|
"sha256:" + strings.Repeat("g", 64),
|
|
"mesh-controller@sha256:" + strings.Repeat("1", 64),
|
|
} {
|
|
if _, err := Rewrite(theRealBundle(t), bad); err == nil {
|
|
t.Errorf("image id %q was accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The address every enrolment token will carry is reported and never invented. It differs per
|
|
// machine and it is silently fatal when wrong: a node enrols against a dead address and nothing
|
|
// says so until it fails to come back.
|
|
func TestTheAddressNodesWillDialIsReportedAndNotRewritten(t *testing.T) {
|
|
template := theRealBundle(t)
|
|
out, err := Rewrite(template, held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.BrokerAddress == "" {
|
|
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 "+
|
|
"knows this machine's address", out.BrokerAddress)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// 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 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 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-controller")
|
|
}
|
|
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 {
|
|
t.Fatal(err)
|
|
}
|
|
if control.Name != out.TempName {
|
|
t.Errorf("the produced bundle calls it %q, want %q", control.Name, out.TempName)
|
|
}
|
|
// And the plain name is free, which is the whole point: it belongs to the module now.
|
|
for _, name := range containerNames(out.Declaration) {
|
|
if name == ControlPlaneModule {
|
|
t.Errorf("the produced bundle still declares a container called %q, which the module "+
|
|
"will also declare", ControlPlaneModule)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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-controller@") ||
|
|
strings.Contains(string(out.Bundle), "/"+TempPrefix+"mesh-controller") {
|
|
t.Error("the rename reached inside an image reference")
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
was, now := containerNames(before), containerNames(out.Declaration)
|
|
for id, name := range was {
|
|
if id == ControlPlaneID {
|
|
continue
|
|
}
|
|
if now[id] != name {
|
|
t.Errorf("%s was renamed from %q to %q", id, name, now[id])
|
|
}
|
|
}
|
|
}
|
|
|
|
// A re-run against a bundle this installer produced renames nothing and says so. The installer is
|
|
// run over and over while somebody gets a machine working, and a step that could not tell "already
|
|
// done" from "just done" makes the second run indistinguishable from the first.
|
|
func TestRewritingABundleThisAlreadyProducedRenamesNothing(t *testing.T) {
|
|
first, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := Rewrite(first.Bundle, held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if second.Renamed {
|
|
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",
|
|
second.TempName, first.TempName)
|
|
}
|
|
if string(second.Bundle) != string(first.Bundle) {
|
|
t.Error("rewriting a produced bundle changed it")
|
|
}
|
|
}
|