package image import ( "archive/tar" "bytes" "encoding/json" "strings" "testing" ) // Each test names the decision it defends (novox/hq ADR 0017). // savedImage builds what `docker save` produces, as far as this package reads it. func savedImage(t *testing.T, files map[string]string) []byte { t.Helper() var buffer bytes.Buffer writer := tar.NewWriter(&buffer) for name, content := range files { header := &tar.Header{Name: name, Mode: 0o644, Size: int64(len(content))} if err := writer.WriteHeader(header); err != nil { t.Fatalf("building the fixture: %v", err) } if _, err := writer.Write([]byte(content)); err != nil { t.Fatalf("building the fixture: %v", err) } } if err := writer.Close(); err != nil { t.Fatalf("building the fixture: %v", err) } return buffer.Bytes() } func manifest(t *testing.T, config string, tags ...string) string { t.Helper() raw, err := json.Marshal([]manifestEntry{{Config: config, RepoTags: tags}}) if err != nil { t.Fatalf("building the fixture: %v", err) } return string(raw) } // The image id is read from the FILE, before any runtime is asked anything. // // That is what makes the load idempotent: knowing the id in advance lets the installer ask "do you // already hold exactly this" instead of loading and then finding out. Scraping it from what // `docker load` prints would only be possible after loading, so the second run of an installer // would load again every time and be unable to say it had not. func TestTheImageIdIsReadFromTheSavedFile(t *testing.T) { digest := strings.Repeat("a", 64) // Both layouts `docker save` has used. The older one names the config `.json`; the OCI // one names it `blobs/sha256/`. They carry the same sixty-four characters, and a // reader that understood only one would work until somebody upgraded their runtime. for _, config := range []string{digest + ".json", "blobs/sha256/" + digest} { saved := savedImage(t, map[string]string{"manifest.json": manifest(t, config)}) id, err := ID(saved) if err != nil { t.Fatalf("config %q: %v", config, err) } if id != "sha256:"+digest { t.Errorf("config %q gave id %q, want sha256:%s", config, id, digest) } } } func TestTheSavedTagsAreReadForAPersonToRecognise(t *testing.T) { saved := savedImage(t, map[string]string{ "manifest.json": manifest(t, strings.Repeat("b", 64)+".json", "mesh-control:v1"), }) got := Tags(saved) if len(got) != 1 || got[0] != "mesh-control:v1" { t.Errorf("tags = %v, want [mesh-control:v1]", got) } } // A tar that is not a saved image is refused with what is wrong, not with a nil id. // // The installer names the control plane by this id in the bundle it writes. An id it could not // read, treated as empty, would produce a bundle naming nothing — refused by the host two steps // later, with a message about a declaration rather than about what somebody embedded. func TestSomethingThatIsNotASavedImageIsRefused(t *testing.T) { notAnImage := savedImage(t, map[string]string{"hello": "world"}) if _, err := ID(notAnImage); err == nil { t.Error("a tar with no manifest.json was accepted as a saved image") } else if !strings.Contains(err.Error(), "docker save") { t.Errorf("the refusal does not say what to embed instead: %v", err) } if _, err := ID([]byte("this is not a tar at all")); err == nil { t.Error("bytes that are not a tar were accepted") } } // Exactly one image. A bootstrap that chose between several would be the thing that guesses which // one is the control plane, and it would guess right until the day somebody saved two. func TestATarHoldingSeveralImagesIsRefused(t *testing.T) { entries, err := json.Marshal([]manifestEntry{ {Config: strings.Repeat("a", 64) + ".json"}, {Config: strings.Repeat("b", 64) + ".json"}, }) if err != nil { t.Fatal(err) } saved := savedImage(t, map[string]string{"manifest.json": string(entries)}) if _, err := ID(saved); err == nil { t.Error("a tar holding two images was accepted") } } // An id is a digest or it is nothing. A truncated one names several images, and which one ran // would be whichever the runtime matched first — the same reasoning `internal/declaration` gives // for refusing a short image reference. func TestAConfigThatIsNotADigestIsRefused(t *testing.T) { for _, config := range []string{"config.json", "abc.json", "blobs/sha256/" + strings.Repeat("a", 63)} { saved := savedImage(t, map[string]string{"manifest.json": manifest(t, config)}) if _, err := ID(saved); err == nil { t.Errorf("config %q was accepted and is not a digest", config) } } } // The committed placeholder must read as "carries nothing", so an installer built from a plain // checkout says so in preflight rather than getting a machine as far as a running store and // stopping. This is the same guarantee `internal/bundle` makes about a lock file of only comments. func TestAnInstallerBuiltFromAPlainCheckoutCarriesNothing(t *testing.T) { if !IsEmpty() { // Not a failure of this checkout: `make bootstrap` embeds a real image and puts the // placeholder back, so a real image here means a build was interrupted. t.Skip("this checkout has a saved image embedded, so there is no placeholder to check") } if _, err := Saved(); err == nil { t.Fatal("an installer carrying only the placeholder reported it carries an image") } } // And "empty" is decided by whether the bytes could be loaded, not by matching the placeholder's // text. A truncated or corrupted embed is equally unloadable and equally worth refusing early. func TestEmptyMeansUnloadableRatherThanEqualToThePlaceholder(t *testing.T) { restore := saved defer func() { saved = restore }() saved = []byte("half a tar, cut off") if !IsEmpty() { t.Error("bytes that are not a tar were reported as a carried image") } saved = savedImage(t, map[string]string{ "manifest.json": manifest(t, strings.Repeat("c", 64)+".json"), }) if IsEmpty() { t.Error("a real saved image was reported as no image at all") } }