Files
mesh-host/internal/bootstrap/load_test.go
T
jschoubben 121367319d 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
2026-09-16 18:40:40 +02:00

314 lines
13 KiB
Go

package bootstrap
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"github.com/novox/mesh-host/internal/image"
)
// Docker is never required here. What is being tested is which commands the installer issues and
// what it concludes from the answers, so the runtime is injected the way `internal/apply` injects
// its Runner (novox/hq ADR 0017). Behaviour against a real runtime is proved in the lab.
// asked records every command, so a test can assert that something was NOT run — which is the
// whole of what idempotence means here.
type asked struct {
commands []string
answer func(name string, args []string) (string, error)
}
func (a *asked) run(_ context.Context, name string, args ...string) (string, error) {
a.commands = append(a.commands, strings.TrimSpace(name+" "+strings.Join(args, " ")))
if a.answer == nil {
return "", errors.New("this test did not expect any command to be run")
}
return a.answer(name, args)
}
func (a *asked) ran(fragment string) bool {
for _, command := range a.commands {
if strings.Contains(command, fragment) {
return true
}
}
return false
}
// 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-controller:test"}
}
entries, err := json.Marshal([]struct {
Config string
RepoTags []string
}{{Config: digest + ".json", RepoTags: tags}})
if err != nil {
t.Fatal(err)
}
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
if err := writer.WriteHeader(&tar.Header{
Name: "manifest.json", Mode: 0o644, Size: int64(len(entries)),
}); err != nil {
t.Fatal(err)
}
if _, err := writer.Write(entries); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return buffer.Bytes()
}
// 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-controller:development` was `sha256:b86bb81c…` on the
// workstation that saved it and `sha256:2dc21904…` on the machine that loaded it.
const (
fixtureDigest = "3333333333333333333333333333333333333333333333333333333333333333"
runtimeDigest = "4444444444444444444444444444444444444444444444444444444444444444"
)
// **The id the bundle is named by comes from the RUNTIME, not from the archive.**
//
// This is the test for the fault that took the lab down. The installer used to read the id out of
// the carried tar and use it for the bundle, which is correct on the machine the image was built
// on and wrong on every machine it is carried to — and a bundle naming an id the machine does not
// hold names an image nothing can serve, because an image named by the digest of its own
// configuration is by definition served by nobody. The apply then stops inside a pull that cannot
// succeed, three steps from the cause.
//
// So the image is identified by its TAG, which survives save and load unchanged, and the runtime
// is asked what that tag resolves to.
func TestTheIdComesFromTheRuntimeAndNotFromTheArchive(t *testing.T) {
runtime := &asked{}
inspected := 0
runtime.answer = func(_ string, args []string) (string, error) {
switch {
case len(args) > 1 && args[0] == "image" && args[1] == "inspect":
inspected++
if inspected == 1 {
// Nothing held yet.
return "", errors.New("Error: No such image")
}
// 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-controller:test\n", nil
}
return "", fmt.Errorf("unexpected command: %v", args)
}
var said []string
loaded, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest), false, func(line string) { said = append(said, line) })
if err != nil {
t.Fatal(err)
}
if loaded.ID != "sha256:"+runtimeDigest {
t.Errorf("the bundle would name %q; this machine holds sha256:%s", loaded.ID, runtimeDigest)
}
if loaded.Archive != "sha256:"+fixtureDigest {
t.Errorf("the archive's own id is reported as %q", loaded.Archive)
}
if loaded.Predicted {
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-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
// the build machine concludes the wrong image was carried.
if !strings.Contains(strings.Join(said, "\n"), "the archive says") {
t.Errorf("nothing was said about the two ids differing: %v", said)
}
}
// A machine that already holds the image is not loaded again, and says so.
//
// This is the idempotence the installer's usefulness rests on: it is run over and over while
// somebody gets a machine working, and a step that did its work again every time would be
// indistinguishable from one that had never run. **Decided from what the runtime holds under the
// tag, not from what the archive predicts** — a predicted id cannot answer this question at all on
// a machine whose runtime rewrites configurations.
func TestAnImageThisMachineAlreadyHoldsIsNotLoadedAgain(t *testing.T) {
runtime := &asked{answer: func(_ string, args []string) (string, error) {
if len(args) > 1 && args[0] == "image" && args[1] == "inspect" {
return "sha256:" + runtimeDigest + "\n", nil
}
return "", fmt.Errorf("unexpected command: %v", args)
}}
var said []string
loaded, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest), false, func(line string) { said = append(said, line) })
if err != nil {
t.Fatal(err)
}
if !loaded.Held {
t.Error("the machine already held the image and the load did not say so")
}
if loaded.ID != "sha256:"+runtimeDigest {
t.Errorf("the id reported for an already-held image is %q, and the machine holds sha256:%s",
loaded.ID, runtimeDigest)
}
if runtime.ran("docker load") {
t.Errorf("the image was loaded again although the machine held it: %v", runtime.commands)
}
if !strings.Contains(strings.Join(said, "\n"), "already held") {
t.Errorf("nothing was said about finding the image already there: %v", said)
}
}
// A load that reported success and left nothing there is a failure, not a convergence
// (novox/hq ADR 0018). Without the read-back it would surface later as the host refusing a bundle
// naming an image nothing serves — a true message about the wrong thing.
func TestALoadThatLeftNothingBehindIsAFailure(t *testing.T) {
runtime := &asked{answer: func(_ string, args []string) (string, error) {
if len(args) > 1 && args[0] == "image" && args[1] == "inspect" {
return "", errors.New("Error: No such image")
}
return "Loaded image: mesh-controller:test\n", nil
}}
_, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest), false, func(string) {})
if err == nil {
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-controller:test") {
t.Errorf("the failure does not say what this machine holds nothing of: %v", err)
}
}
// **A dry run cannot know the id, and says so rather than pretending.** It loads nothing, so no
// runtime has decided anything, and the id in the archive is a fact about a file rather than a
// prediction about this machine. `--dry-run` still produces and checks the bundle's shape; what it
// cannot promise is the one value that only a load can settle.
func TestADryRunLoadsNothingAndSaysTheIdIsUnconfirmed(t *testing.T) {
runtime := &asked{answer: func(_ string, args []string) (string, error) {
if len(args) > 1 && args[0] == "image" && args[1] == "inspect" {
return "", errors.New("Error: No such image")
}
return "", fmt.Errorf("a dry run ran %v", args)
}}
var said []string
loaded, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest), true, func(line string) { said = append(said, line) })
if err != nil {
t.Fatal(err)
}
if !loaded.Predicted {
t.Error("a dry run reported an id no runtime had confirmed as though it had been")
}
if loaded.ID != "sha256:"+fixtureDigest {
t.Errorf("a dry run reported %q, and the archive says sha256:%s", loaded.ID, fixtureDigest)
}
if runtime.ran("docker load") {
t.Errorf("a dry run loaded an image: %v", runtime.commands)
}
if !strings.Contains(strings.Join(said, "\n"), "NOT THE FINAL ID") {
t.Errorf("a dry run did not say its id is unconfirmed: %v", said)
}
}
// A dry run on a machine that already holds the image DOES know the id, because the runtime was
// asked and answered. Reading is not changing, so a dry run is entitled to that.
func TestADryRunOnAMachineThatHoldsItKnowsTheRealId(t *testing.T) {
runtime := &asked{answer: func(_ string, args []string) (string, error) {
if len(args) > 1 && args[0] == "image" && args[1] == "inspect" {
return "sha256:" + runtimeDigest + "\n", nil
}
return "", fmt.Errorf("a dry run ran %v", args)
}}
loaded, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest), true, func(string) {})
if err != nil {
t.Fatal(err)
}
if loaded.Predicted {
t.Error("an id this machine's runtime supplied was reported as a prediction")
}
if loaded.ID != "sha256:"+runtimeDigest {
t.Errorf("the id is %q, and the runtime said sha256:%s", loaded.ID, runtimeDigest)
}
}
// **An untagged archive is a build-time fault, refused rather than worked around.** Without a tag
// there is no portable name to ask the runtime about, and the only thing left is scraping the
// sentence `docker load` prints for a person — which differs between runtime versions and is
// exactly the kind of guess this whole step exists to stop making.
func TestAnUntaggedArchiveIsRefused(t *testing.T) {
runtime := &asked{answer: func(_ string, args []string) (string, error) {
return "", fmt.Errorf("nothing should have been run: %v", args)
}}
_, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest, []string{}...), false, func(string) {})
if err == nil {
t.Fatal("an archive with no tag was accepted, and there is no way to ask about it")
}
if !strings.Contains(err.Error(), "make bootstrap") {
t.Errorf("the refusal does not say how to build one that is tagged: %v", err)
}
if len(runtime.commands) != 0 {
t.Errorf("the machine was touched first: %v", runtime.commands)
}
}
// An installer built from a plain checkout carries no image, and says which build step is missing.
// Discovered here, before a machine is touched, rather than after a bundle has been written.
func TestAnInstallerCarryingNoImageSaysSoRatherThanRaisingHalfAMesh(t *testing.T) {
if !image.IsEmpty() {
t.Skip("this checkout has a saved image embedded")
}
_, err := Load(context.Background(), (&asked{}).run, false, func(string) {})
if !errors.Is(err, image.ErrEmpty) {
t.Fatalf("an installer with no control-plane image gave %v, want ErrEmpty", err)
}
if !strings.Contains(err.Error(), "make bootstrap") {
t.Errorf("the refusal does not say how to build one that carries an image: %v", err)
}
}
// An answer that is not an image id is refused rather than written into a bundle.
//
// **This replaces a test that refused an answer differing from the archive's id.** That test
// encoded the mistake: a differing id is now the expected case, not a fault, because a runtime
// rewrites an image's configuration as it loads. What is still worth refusing is an answer that is
// not an id at all — that value becomes the name a bundle applies on a machine with no mesh to
// 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-controller:test",
"sha256:" + strings.Repeat("9", 63),
"<no value>",
} {
runtime := &asked{answer: func(_ string, _ []string) (string, error) {
return nonsense + "\n", nil
}}
if _, err := loadImage(context.Background(), runtime.run,
savedImageFixture(t, fixtureDigest), false, func(string) {}); err == nil {
t.Errorf("the runtime answered %q and it was accepted as an image id", nonsense)
}
}
}