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), "", } { 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) } } }