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()