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 } func savedImageFixture(t *testing.T, digest string) []byte { t.Helper() entries, err := json.Marshal([]struct { Config string RepoTags []string }{{Config: digest + ".json", RepoTags: []string{"mesh-control:test"}}}) 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() } const fixtureDigest = "3333333333333333333333333333333333333333333333333333333333333333" // 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. 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:" + fixtureDigest + "\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 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) } } // The id comes out of the file, and the bundle is named by it. // // Not scraped from what `docker load` prints — that is a sentence for a person, which reads // `Loaded image: name:tag` or `Loaded image ID: sha256:…` depending on how the image was saved. // A program depending on which one a runtime chose would be depending on a runtime version. func TestTheImageIdComesFromTheCarriedFileNotFromWhatTheRuntimeSays(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 { return "", errors.New("Error: No such image") } return "sha256:" + fixtureDigest + "\n", nil case len(args) > 0 && args[0] == "load": // Deliberately says something else entirely. The id must not come from here. return "Loaded image: some-other-name:whatever\n", nil } return "", fmt.Errorf("unexpected command: %v", args) } loaded, err := loadImage(context.Background(), runtime.run, savedImageFixture(t, fixtureDigest), false, func(string) {}) if err != nil { t.Fatal(err) } if loaded.ID != "sha256:"+fixtureDigest { t.Errorf("the image id is %q, want sha256:%s", loaded.ID, fixtureDigest) } if loaded.Held { t.Error("an image that had to be loaded was reported as already held") } if !runtime.ran("docker load") { t.Errorf("the image was never loaded: %v", runtime.commands) } } // 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-control: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") } if !strings.Contains(err.Error(), fixtureDigest) { t.Errorf("the failure does not say which image is missing: %v", err) } } // A dry run changes nothing, and still knows the id — because the id is a property of the carried // file. That is what lets `--dry-run` produce and check the real bundle rather than a guess. func TestADryRunLearnsTheIdAndLoadsNothing(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) }} loaded, err := loadImage(context.Background(), runtime.run, savedImageFixture(t, fixtureDigest), true, func(string) {}) if err != nil { t.Fatal(err) } if loaded.ID != "sha256:"+fixtureDigest { t.Errorf("a dry run did not work out the image id: %q", loaded.ID) } if runtime.ran("docker load") { t.Errorf("a dry run loaded an image: %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) } } // Two different images cannot share an id, so an answer that is not the id asked about means the // runtime is talking about something else. Reported rather than believed. func TestARuntimeAnsweringAboutADifferentImageIsRefused(t *testing.T) { runtime := &asked{answer: func(_ string, _ []string) (string, error) { return "sha256:" + strings.Repeat("9", 64) + "\n", nil }} if _, err := loadImage(context.Background(), runtime.run, savedImageFixture(t, fixtureDigest), false, func(string) {}); err == nil { t.Fatal("the runtime answered about a different image and it was accepted") } }