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:
@@ -20,6 +20,13 @@
|
||||
// configuration, which is exact, unforgeable, and needs nothing to have served it (novox/hq
|
||||
// ADR 0006, and `internal/declaration`'s checkImage). Third-party images keep their upstream
|
||||
// `name@sha256:` references and are pulled from the internet like anything else.
|
||||
//
|
||||
// **And that id is read back from the machine, never predicted from the archive.** The digest of a
|
||||
// configuration is not portable: a runtime rewrites the configuration as it loads, so the same
|
||||
// bytes are held under a different name on the machine that receives them than on the one that
|
||||
// saved them. The archive is identified by its TAG, which does survive the transfer, and the id
|
||||
// the bundle names is whatever the runtime answers for that tag afterwards. See Load, which
|
||||
// carries the measurement.
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
@@ -151,12 +158,22 @@ type Result struct {
|
||||
System string `json:"system"`
|
||||
DryRun bool `json:"dry-run,omitempty"`
|
||||
|
||||
// Image is the control plane's image id — what the produced bundle names it by.
|
||||
// Image is what THIS MACHINE'S RUNTIME holds the control plane as, read back from it — and
|
||||
// what the produced bundle names it by.
|
||||
Image string `json:"image,omitempty"`
|
||||
// ImageTags is what that image was called when it was saved. Decoration, for a person.
|
||||
// ImageArchive is what the carried archive calls the same image. Reported because it is
|
||||
// routinely a DIFFERENT id: a runtime rewrites an image's configuration as it loads, and an id
|
||||
// is that configuration's digest. Never what the bundle names.
|
||||
ImageArchive string `json:"image-in-archive,omitempty"`
|
||||
// ImageTag is the name the runtime was asked by, which is how the id above was obtained.
|
||||
ImageTag string `json:"image-tag,omitempty"`
|
||||
// ImageTags is everything the image was called when it was saved.
|
||||
ImageTags []string `json:"image-tags,omitempty"`
|
||||
// ImageHeld is true when the machine already held it and nothing was loaded.
|
||||
ImageHeld bool `json:"image-already-held,omitempty"`
|
||||
// ImagePredicted is true when Image is the archive's id because nothing was loaded — a dry run
|
||||
// only, and the reason a dry run does not claim to know what would be applied.
|
||||
ImagePredicted bool `json:"image-id-is-a-prediction,omitempty"`
|
||||
|
||||
// Bundle is where the produced bundle was written, and what was done to produce it.
|
||||
Bundle string `json:"bundle,omitempty"`
|
||||
@@ -276,6 +293,8 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
return result, failed(StepLoad, err)
|
||||
}
|
||||
result.Image, result.ImageTags, result.ImageHeld = loaded.ID, loaded.Tags, loaded.Held
|
||||
result.ImageArchive, result.ImageTag = loaded.Archive, loaded.Tag
|
||||
result.ImagePredicted = loaded.Predicted
|
||||
|
||||
// ---- 3. bundle ----------------------------------------------------------------------
|
||||
say("bundle — what this machine will be asked to be")
|
||||
@@ -301,6 +320,10 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
say(fmt.Sprintf(" its image %s — the template already named it, nothing rewritten",
|
||||
rewritten.Now))
|
||||
}
|
||||
if loaded.Predicted {
|
||||
say(" UNCONFIRMED that id is the archive's and no runtime has been asked. A real " +
|
||||
"run reads it back.")
|
||||
}
|
||||
for _, kept := range rewritten.Kept {
|
||||
say(" left alone " + kept)
|
||||
}
|
||||
@@ -330,10 +353,27 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
}
|
||||
result.Stopped = "dry run: the bundle was produced and checked, and nothing was written, " +
|
||||
"loaded or applied"
|
||||
if loaded.Predicted {
|
||||
result.Stopped += ". The image id in it is the archive's own and is not necessarily " +
|
||||
"the one this machine would hold — a runtime rewrites an image's configuration as " +
|
||||
"it loads, and the id is that configuration's digest"
|
||||
}
|
||||
say("\n" + result.Stopped)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// **Nothing predicted is ever written down.** The line above is the only path on which
|
||||
// `loaded.ID` can be the archive's id, and it returns. Asserted here rather than left to the
|
||||
// reader, because what would follow is a bundle naming an image this machine does not hold —
|
||||
// and nothing serves an image named by the digest of its own configuration, so it would fail
|
||||
// inside a pull that cannot succeed, three steps from the cause.
|
||||
if loaded.Predicted {
|
||||
return result, failed(StepBundle, fmt.Errorf(
|
||||
"the control plane's image id was never confirmed against this machine's runtime, and "+
|
||||
"the bundle was about to be written with it. This is a fault in the installer, not "+
|
||||
"in the machine"))
|
||||
}
|
||||
|
||||
if err := writeBundleFile(o.Out, rewritten.Bundle); err != nil {
|
||||
return result, failed(StepBundle, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user