feat(push): support --all-tags to push all tags - #5133
Conversation
dd35a5b to
f153bf6
Compare
`nerdctl push` accepts a bare repository name, but referenceutil.Parse
normalizes it to ":latest", so only that single tag is pushed. Add the
Docker-compatible `-a, --all-tags` flag, which pushes every local tag of
the repository instead.
Push is split into a dispatcher and pushSingle(): without --all-tags the
dispatcher just delegates, with it the local tags are resolved through
the `name~=^<repo>:` image filter (the same idiom nameFilterFor() uses
for `nerdctl image ls`) and pushed one by one. The temporary images push
creates for itself are skipped, so an interrupted push cannot leak a
"-tmp-reduced-platform" tag into the registry, and the list is sorted
because ImageService().List() guarantees no order.
A tag or a digest in the reference is rejected, as docker does. The check
looks at ExplicitTag rather than Tag: Parse() runs TagNameOnly(), so Tag
is "latest" even for a bare repository name.
A SOCI index is attached to the image manifest rather than to the tag, so
it is now built once per distinct target digest. Pushing several tags of
one image no longer makes each tag overwrite the index pushed by the
previous one.
Pushing more than once per process also uncovered a bug in the plain HTTP
fallback. pushImageWithLocal builds a fresh in-memory tracker per push,
but the fallback rebuilt the resolver through dockerconfigresolver.New,
which silently substitutes the process-wide PushTracker. containerd's
dockerPusher keys that tracker by content ref ("index-<digest>"), not by
reference, and returns ErrAlreadyExists before issuing any request when
the digest is already committed; remotes.push() treats that as success,
so the manifest PUT that creates the tag never happens and the command
still exits 0. Rebuild the resolver from the host options instead,
reusing the resolver options assembled above so the fallback keeps the
per-push tracker.
The tests assert that the pushed tags are present in the registry rather
than that they are the only ones: the listing is a superset, since a SOCI
v1 index is attached through the referrers fallback tag ("sha256-<digest>")
on registries without the referrers API.
Closes containerd#3751
Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
34b1ba5 to
b8ceb3d
Compare
| indexed := make(map[digest.Digest]struct{}, len(imgs)) | ||
| for _, img := range imgs { | ||
| _, done := indexed[img.Target.Digest] | ||
| if err = pushSingle(ctx, client, img.Name, options, done); err != nil { | ||
| return err | ||
| } | ||
| indexed[img.Target.Digest] = struct{}{} | ||
| } |
There was a problem hiding this comment.
SOCI Index is deduplicated based on the image digest ID, but is it intentional that the same processing for estargz and cosign isn't performed?
There was a problem hiding this comment.
eStargz is a different kind of thing and cannot be skipped the same way: the converted image is the payload pushed under each tag, so skipping it for the second tag would push the wrong content. What could be deduplicated is the conversion work (convert once per digest, push the result under each tag), but that needs pushSingle restructured, since it currently owns the temp image lifecycle for a single ref. I also have not verified that eStargz conversion is byte-reproducible; if it is not, two tags of one source image end up on different manifests, which is pre-existing but more visible under --all-tags.
Signing was not a deliberate exclusion. signutil.Sign gets <pushRef>@<digest> and passes it to cosign sign / notation sign, both of which key the signature by digest in the same repository, so deduplicating it by digest would be correct too. Happy to add it. It would mean renaming indexed/alreadyIndexed to something covering both.
The all-tags SOCI sub-test only checked that both tags were pushed, which
the non-SOCI sub-test already covers, so nothing in it depended on SOCI.
Assert the SOCI index is in the registry too. The test registry is
distribution 2.x, which predates the referrers API, so SOCI attaches its
index through the referrers fallback tag ("sha256-<hex>"); a push without
SOCI never creates one, so its presence is what tells the index apart
from the tags of the image itself.
Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
`done` did not say what was done. The value is whether the target digest already had its SOCI index built by an earlier tag in the loop. Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
Name the parameter after the fact it carries rather than after the step it suppresses, so the call site and the signature read the same way. Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
The doc comment already states what localTags returns, and the filter expression says the rest. What the comment added was a cross-reference to the sibling implementation in cmd/, which the reader of this function does not need and which rots on rename. Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
Closes #3751.
nerdctl pushaccepts a bare repository name, butreferenceutil.Parsenormalizes it to:latest, so only that single tag is pushed. This adds the Docker-compatible-a, --all-tagsflag, which pushes every local tag of the repository instead.Push()is split into a dispatcher andpushSingle(). With--all-tagsthe local tags are resolved through thename~=^<repo>:image filter - the same idiomnameFilterFor()uses fornerdctl image ls- then sorted by name and pushed one by one. The-tmp-reduced-platform/-tmp-esgzimagespushcreates for itself are skipped, so an interrupted push cannot leak one into the registry as a real tag.A tag or a digest in the reference is rejected, as
docker push --all-tagsdoes. The check looks atExplicitTag, notTag:Parse()runsTagNameOnly(), soTagis"latest"even for a bare repository name.The issue also reports that pushing several tags of one image overwrites the SOCI index each time. A SOCI index is attached to the image manifest rather than to the tag, so it is now built once per distinct target digest - deduplicating by digest rather than by position keeps each image indexed when the tags differ.
Tests: four sub-tests in
TestPushcovering all tags pushed (verified against/v2/<repo>/tags/list), explicit tag rejected, no local tags rejected, and SOCI with--all-tags.Note: #4627 is an earlier attempt at this issue, inactive since December 2025. This is an independent implementation. For whoever picks that one up: its guard is
parsedReference.Tag != "", which is always true afterTagNameOnly()normalization, so--all-tagsthere always fails withtag can't be used with --all-tags/-a.