From 365d41f52040c69c48b7aaeb644e7d84fe889084 Mon Sep 17 00:00:00 2001 From: byarbrough Date: Thu, 21 Jul 2022 13:51:02 -0600 Subject: [PATCH] Add option to push all tags This seeks to mirror `docker push --all-tags IMAGE`. Because tags are appended to the destination, this will only work with the docker transport. Otherwise you'd get directories overwriting or with weird names. Also note that if AllTags is true then the user must provide the name of the image only; providing a tag will crash. Docker has this behavior: ``` tag can't be used with --all-tags/-a ``` Requirement for https://github.com/containers/podman/pull/14949 ``` Signed-off-by: Brian Yarbrough bcynmelk+git@gmail.com ``` --- libimage/push.go | 55 ++++++++++++++++++++++++++++++++++++------ libimage/push_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/libimage/push.go b/libimage/push.go index 7203838aa..94d6ca005 100644 --- a/libimage/push.go +++ b/libimage/push.go @@ -2,8 +2,11 @@ package libimage import ( "context" + "fmt" + "strings" "time" + dockerTransport "github.com/containers/image/v5/docker" dockerArchiveTransport "github.com/containers/image/v5/docker/archive" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/transports/alltransports" @@ -13,6 +16,7 @@ import ( // PushOptions allows for custommizing image pushes. type PushOptions struct { CopyOptions + AllTags bool } // Push pushes the specified source which must refer to an image in the local @@ -36,11 +40,6 @@ func (r *Runtime) Push(ctx context.Context, source, destination string, options return nil, err } - srcRef, err := image.StorageReference() - if err != nil { - return nil, err - } - // Make sure we have a proper destination, and parse it into an image // reference for copying. if destination == "" { @@ -50,7 +49,44 @@ func (r *Runtime) Push(ctx context.Context, source, destination string, options destination = resolvedSource } - logrus.Debugf("Pushing image %s to %s", source, destination) + // If specified to push --all-tags, look them up and iterate. + if options.AllTags { + + // Do not allow : for tags, other than specifying transport + d := strings.TrimPrefix(destination, "docker://") + if strings.ContainsAny(d, ":") { + return nil, fmt.Errorf("tag can't be used with --all-tags/-a") + } + + namedRepoTags, err := image.NamedTaggedRepoTags() + if err != nil { + return nil, err + } + + logrus.Debugf("Flag --all-tags true, found: %s", namedRepoTags) + + for _, tag := range namedRepoTags { + fullNamedTag := fmt.Sprintf("%s:%s", destination, tag.Tag()) + _, err = pushImage(ctx, fullNamedTag, options, image, r) + if err != nil { + return nil, err + } + } + } else { + // No --all-tags, so just push just the single image. + return pushImage(ctx, destination, options, image, r) + } + + return nil, nil +} + +func pushImage(ctx context.Context, destination string, options *PushOptions, image *Image, r *Runtime) ([]byte, error) { + srcRef, err := image.StorageReference() + if err != nil { + return nil, err + } + + logrus.Debugf("Pushing image %s to %s", srcRef, destination) destRef, err := alltransports.ParseImageName(destination) if err != nil { @@ -63,6 +99,11 @@ func (r *Runtime) Push(ctx context.Context, source, destination string, options destRef = dockerRef } + // If using --all-tags, must push to registry + if destRef.Transport().Name() != dockerTransport.Transport.Name() && options.AllTags { + return nil, fmt.Errorf("--all-tags can only be used with docker transport") + } + if r.eventChannel != nil { defer r.writeEvent(&Event{ID: image.ID(), Name: destination, Time: time.Now(), Type: EventTypeImagePush}) } @@ -70,7 +111,7 @@ func (r *Runtime) Push(ctx context.Context, source, destination string, options // Buildah compat: Make sure to tag the destination image if it's a // Docker archive. This way, we preserve the image name. if destRef.Transport().Name() == dockerArchiveTransport.Transport.Name() { - if named, err := reference.ParseNamed(resolvedSource); err == nil { + if named, err := reference.ParseNamed(destination); err == nil { tagged, isTagged := named.(reference.NamedTagged) if isTagged { options.dockerArchiveAdditionalTags = []reference.NamedTagged{tagged} diff --git a/libimage/push_test.go b/libimage/push_test.go index 444f54d00..d174abc11 100644 --- a/libimage/push_test.go +++ b/libimage/push_test.go @@ -67,6 +67,62 @@ func TestPush(t *testing.T) { } } +func TestPushAllTags(t *testing.T) { + runtime, cleanup := testNewRuntime(t) + defer cleanup() + ctx := context.Background() + + // Prefetch alpine. + pullOptions := &PullOptions{} + pullOptions.Writer = os.Stdout + _, err := runtime.Pull(ctx, "docker.io/library/alpine:latest", config.PullPolicyAlways, pullOptions) + require.NoError(t, err) + + pushOptions := &PushOptions{} + pushOptions.AllTags = true + pushOptions.Writer = os.Stdout + + workdir, err := ioutil.TempDir("", "libimagepush") + require.NoError(t, err) + defer os.RemoveAll(workdir) + + // tag image with alternates + lookupOptions := &LookupImageOptions{} + img, _, err := runtime.LookupImage("alpine", lookupOptions) + require.NoError(t, err) + img.Tag("01") + img.Tag("02") + + for _, test := range []struct { + source string + destination string + expectError bool + }{ + {"alpine", "dir:" + workdir + "/dir", true}, + {"alpine", "containers-storage:localhost/another:alpine", true}, + {"alpine", "docker://docker.io/library/alpine:latest", true}, + {"alpine", "docker://docker.io/library/alpine", false}, + {"alpine", "docker.io/library/alpine", false}, + } { + _, err := runtime.Push(ctx, test.source, test.destination, pushOptions) + if test.expectError { + require.Error(t, err, "%v", test) + continue + } + require.NoError(t, err, "%v", test) + pullOptions.AllTags = true + pulledImages, err := runtime.Pull(ctx, test.destination, config.PullPolicyAlways, pullOptions) + require.NoError(t, err, "%v", test) + require.Len(t, pulledImages, 2, "%v", test) + } + + // And now remove all of them. + rmReports, rmErrors := runtime.RemoveImages(ctx, nil, nil) + require.Len(t, rmErrors, 0) + require.Len(t, rmReports, 3) + +} + func TestPushOtherPlatform(t *testing.T) { runtime, cleanup := testNewRuntime(t) defer cleanup()