bootstrap: read the image id back from the runtime, never predict it
An image id does not survive `docker save` -> transfer -> `docker load`. The id is
the digest of the image's *configuration*, and a runtime rewrites that
configuration as it loads: a newer Docker saves in one format, an older one stores
it in another. Same layers, same program, different name. Measured on a live raise:
saved on the workstation sha256:b86bb81ca2f9691f24f4725f50962d1e49c98c5ffe211113241243d42d18ceea
loaded on the machine sha256:2dc219046c73702fc640317f0342a28ec962ef1e9ef547b2f02861c508ca78fb
`internal/image`.ID read the id out of the carried tar and its comment said that
was the id the runtime would assign. That is true on the machine the image was
built on and false on every machine it is carried to — which is every machine this
program exists for. The installer then either stopped at step 2 refusing the
runtime's answer, or would have written a bundle naming an image the machine does
not hold; and nothing serves an image named by the digest of its own configuration,
which is the whole point of naming one that way, so the apply would have died
inside a pull that cannot succeed. The lab hit this.
So the image is identified by its TAG, which is ordinary metadata the tar carries
through unchanged. The runtime is asked what that tag resolves to before the load
(already held, nothing to do) and again after (this is what the bundle names). The
tag never reaches the bundle — a pinned bundle may not rely on one, ADR 0006 — it
is how the id is obtained, not what is written down.
- image.ID becomes image.ArchiveID, and says plainly that it is a fact about the
file and not a prediction about any machine. It is kept for reports, and printed
beside the runtime's answer whenever the two differ.
- Idempotence is decided from what the runtime holds under the tag, not from a
predicted id, which cannot answer the question at all here.
- An untagged archive is refused, in preflight and again at the load: there would
be no portable name to ask about, and the only thing left is scraping a sentence
`docker load` writes for a person. `make bootstrap` refuses an id or an untagged
image, so it is caught in front of whoever can fix it.
- A dry run cannot know the id and says so rather than pretending. Run refuses to
write a bundle carrying an unconfirmed id at all.
Tests: the injected Runner now answers with an id DIFFERING from the tar's, and the
runtime's answer is what must be used. The test that refused a differing id encoded
the mistake and is replaced by one refusing an answer that is not an id at all.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
+164
-62
@@ -41,12 +41,17 @@ func (a *asked) ran(fragment string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func savedImageFixture(t *testing.T, digest string) []byte {
|
||||
// savedImageFixture builds what `docker save` produces, tagged `mesh-control: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-control:test"}
|
||||
}
|
||||
entries, err := json.Marshal([]struct {
|
||||
Config string
|
||||
RepoTags []string
|
||||
}{{Config: digest + ".json", RepoTags: []string{"mesh-control:test"}}})
|
||||
}{{Config: digest + ".json", RepoTags: tags}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -67,17 +72,83 @@ func savedImageFixture(t *testing.T, digest string) []byte {
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
const fixtureDigest = "3333333333333333333333333333333333333333333333333333333333333333"
|
||||
// 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-control: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-control: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-control: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.
|
||||
// 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:" + fixtureDigest + "\n", nil
|
||||
return "sha256:" + runtimeDigest + "\n", nil
|
||||
}
|
||||
return "", fmt.Errorf("unexpected command: %v", args)
|
||||
}}
|
||||
@@ -92,6 +163,10 @@ func TestAnImageThisMachineAlreadyHoldsIsNotLoadedAgain(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -100,45 +175,6 @@ func TestAnImageThisMachineAlreadyHoldsIsNotLoadedAgain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -155,14 +191,17 @@ func TestALoadThatLeftNothingBehindIsAFailure(t *testing.T) {
|
||||
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)
|
||||
// Named by the tag, because that is what was asked about and what is missing.
|
||||
if !strings.Contains(err.Error(), "mesh-control:test") {
|
||||
t.Errorf("the failure does not say what this machine holds nothing of: %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) {
|
||||
// **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")
|
||||
@@ -170,16 +209,68 @@ func TestADryRunLearnsTheIdAndLoadsNothing(t *testing.T) {
|
||||
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.ID != "sha256:"+fixtureDigest {
|
||||
t.Errorf("a dry run did not work out the image id: %q", loaded.ID)
|
||||
if loaded.Predicted {
|
||||
t.Error("an id this machine's runtime supplied was reported as a prediction")
|
||||
}
|
||||
if runtime.ran("docker load") {
|
||||
t.Errorf("a dry run loaded an image: %v", runtime.commands)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +289,25 @@ func TestAnInstallerCarryingNoImageSaysSoRatherThanRaisingHalfAMesh(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
// 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-control: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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user