fix(image): pin the image a container runs to its digest - #5125
Conversation
3a458da to
a31ddac
Compare
| // ImageDigest is the digest of the image target the container was created from. The image name | ||
| // stored by containerd can be retagged to point at something else, so it is not enough to tell | ||
| // which image a container actually uses. | ||
| ImageDigest = Prefix + "image-digest" |
There was a problem hiding this comment.
Docker stores the image digest in:
There was a problem hiding this comment.
Good point, and it turns out the two are the same value.
I have pushed that: ContainerFromNative now reports the pinned digest in Image, and keeps the reference the user asked for in Config.Image, as Docker does. Containers created before the label existed, or created outside nerdctl, still fall back to the name.
Happy to split the inspect part into its own PR if you would rather keep this one to the in-use lookup.
There was a problem hiding this comment.
The label should no longer be required?
There was a problem hiding this comment.
It is still required: the label is where the digest is stored, and Container.Image is the view built from it.
The container record containerd keeps has no field for an image digest — WithImage writes the name:
// containerd client/container_opts.go
func WithImage(i Image) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
c.Image = i.Name()
return nil
}
}So without the label ContainerFromNative has nothing to read but that name, which is the mutable reference this PR is about.
The one way to drop the label would be to write a digested reference into c.Image instead (alpine:3.13@sha256:...). That field is passed through verbatim to the IMAGE column of nerdctl ps (pkg/cmd/container/list.go), and is keyed on by name elsewhere (usedImages[container.Image] in pkg/imgutil/imgutil.go), so it would change what every container shows, not only the ones a retag affects. A label leaves that alone.
Happy to move it somewhere better if there is one.
`nerdctl images` marks an image as in use by resolving the image name stored on the container, which follows the tag wherever it points now. After `nerdctl tag` moves a tag onto another image, the container gets attributed to an image it never ran: the U indicator lands on the wrong row. Record the image target digest on the container at creation time, in a new nerdctl/image-digest label, and use it for the in-use lookup. Containers created before this label existed, or created outside nerdctl, are still resolved by name; an unparsable value falls back the same way rather than dropping the container from the set. That digest is also what `nerdctl inspect` now reports as Image, where Docker reports the image ID: with the containerd image store that ID is the digest of the image target (moby daemon/containerd/image.go, image.ID(img.Target.Digest)), pinned on the container when it is created. nerdctl used to report the image name there, which a retag moves just the same. The reference the user asked for stays in Config.Image, as it does in Docker. This also matters for the ACTIVE and RECLAIMABLE columns of `nerdctl system df`, which build on the same lookup. Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
a31ddac to
9d1ff92
Compare
| c.Config = &Config{ | ||
| Labels: n.Labels, | ||
| Image: c.Image, | ||
| // Docker keeps the reference the user asked for here, and the digest in Image above. |
There was a problem hiding this comment.
Docker seems just storing the digest here
$ docker run --name foo hello-world
$ docker container inspect foo | jq .[0].Image
"sha256:5dd0d3e6e255913fc30f90b9f2b1d359cc2cbdb48090cc4b65f1676e203243cc"There was a problem hiding this comment.
Both fields, on the same container:
$ docker run --name foo hello-world
$ docker container inspect foo | jq '{Image: .[0].Image, ConfigImage: .[0].Config.Image}'
{
"Image": "sha256:5dd0d3e6e255913fc30f90b9f2b1d359cc2cbdb48090cc4b65f1676e203243cc",
"ConfigImage": "hello-world"
}The digest goes in Image, which is what the line above this one now does; Config.Image keeps the reference, and that is the line this comment landed on. Let me know if you meant something else.
Also worth recording: that digest is the target descriptor, not an ID derived separately.
$ docker image inspect hello-world | jq -r '.[0].Id, .[0].Descriptor.digest'
sha256:5dd0d3e6e255913fc30f90b9f2b1d359cc2cbdb48090cc4b65f1676e203243cc
sha256:5dd0d3e6e255913fc30f90b9f2b1d359cc2cbdb48090cc4b65f1676e203243cc(docker 29.5.2, containerd snapshotter.) So the Image.Target().Digest this PR records at creation is the same value Docker reports.
nerdctl imagesmarks an image as in use by resolving the image name recorded on the container. A name is a mutable reference:container.Image(ctx)looks it up in the image store as it is now, not as it was when the container was created. Once a tag is moved, theUindicator lands on the wrong row.Both answers are wrong. The container still runs alpine, containerd still holds its snapshot and its layers, so alpine cannot be removed - yet it is shown as free, while nginx is shown as busy although nothing ever ran it. A container whose tag was removed altogether fails to resolve and drops out of the in-use set entirely.
The fix
Record the image target digest on the container at creation time, in a new nerdctl/image-digest label, and use it for the in-use lookup. The digest comes from the image nerdctl has already resolved, so nothing extra is fetched, and the label is read from the metadata the listing has already loaded (WithoutRefreshedMetadata), so the lookup costs no additional round trip.
Compatibility
Containers created before this label existed, or created outside nerdctl (ctr, kubelet, another client), have no such label. For those the previous behavior is kept as a fallback: they are still resolved by name. No state migration is needed and no existing container changes behavior for the worse.
An unparsable label value falls back the same way rather than dropping the container from the in-use set. Dropping it would be the more harmful failure: the image would look free, and anything built on this lookup would offer to reclaim space that is actually held.
Why it matters beyond the indicator
The same lookup backs the ACTIVE and RECLAIMABLE columns of
nerdctl system df(follow-up, #3942). There a misattributed container is not a single letter in a column: the unique layers of an image that is actually in use get counted as reclaimable space, which is exactly the number a user acts on when deciding what to delete.The same digest is what
nerdctl inspectshould reportAdded after review feedback. Docker names the image a container was created from by ID, and with the containerd image store that ID is the digest of the image target, pinned on the container at creation:
That is the value this label records. nerdctl was filling
Container.Imagewith the containerd image name, which a retag moves just the same as the in-use lookup above, soContainerFromNativenow reports the pinned digest there. The reference the user asked for stays inConfig.Image, as it does in Docker; the containers with no label keep the name.Tests
TestPinnedImageDigestcovers the four label states: pinned, absent, empty, unparsable.TestImages/In use survives a retagreproduces the scenario end to end and asserts both sides: the image the container runs keepsU, the image the tag now points at does not.TestContainerFromNativeImagecovers both inspect fields, pinned and not.TestContainerInspectConfigImageasserts end to end thatImageis a digest andConfig.Imageis not.The in-use lookup was introduced in #5093.