From 9e620e990fe8a763ce147c23f4d8ab1e12f7156b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 9 Dec 2019 14:48:42 +0100 Subject: [PATCH] implement docker push -a/--all-tags The `docker push` command up until [v0.9.1](https://github.com/moby/moby/blob/v0.9.1/api/client.go#L998) always pushed all tags of a given image, so `docker push foo/bar` would push (e.g.) all of `foo/bar:latest`, `foo:/bar:v1`, `foo/bar:v1.0.0`. Pushing all tags of an image was not desirable in many case, so docker v0.10.0 enhanced `docker push` to optionally specify a tag to push (`docker push foo/bar:v1`) (see https://github.com/moby/moby/issues/3411 and the pull request that implemented this: https://github.com/moby/moby/pull/4948). This behavior exists up until today, and is confusing, because unlike other commands, `docker push` does not default to use the `:latest` tag when omitted, but instead makes it push "all tags of the image" For example, in the following situation; ``` docker images REPOSITORY TAG IMAGE ID CREATED SIZE thajeztah/myimage latest b534869c81f0 41 hours ago 1.22MB ``` Running `docker push thajeztah/myimage` seemingly does the expected behavior (it pushes `thajeztah/myimage:latest` to Docker Hub), however, it does not so for the reason expected (`:latest` being the default tag), but because `:latest` happens to be the only tag present for the `thajeztah/myimage` image. If another tag exists for the image: ``` docker images REPOSITORY TAG IMAGE ID CREATED SIZE thajeztah/myimage latest b534869c81f0 41 hours ago 1.22MB thajeztah/myimage v1.0.0 b534869c81f0 41 hours ago 1.22MB ``` Running the same command (`docker push thajeztah/myimage`) will push _both_ images to Docker Hub. > Note that the behavior described above is currently not (clearly) documented; > the `docker push` reference documentation (https://docs.docker.com/engine/reference/commandline/push/) does not mention that omitting the tag will push all tags This patch changes the default behavior, and if no tag is specified, `:latest` is assumed. To push _all_ tags, a new flag (`-a` / `--all-tags`) is added, similar to the flag that's present on `docker pull`. With this change: - `docker push myname/myimage` will be the equivalent of `docker push myname/myimage:latest` - to push all images, the user needs to set a flag (`--all-tags`), so `docker push --all-tags myname/myimage:latest` Signed-off-by: Sebastiaan van Stijn --- cli/command/image/push.go | 32 ++++++++++++--- cli/command/image/trust.go | 18 +-------- cli/command/trust/sign.go | 13 +++++- contrib/completion/bash/docker | 2 +- contrib/completion/zsh/_docker | 1 + docs/reference/commandline/push.md | 64 ++++++++++++++++++++++++++---- e2e/image/push_test.go | 35 ++++++++++++---- man/src/image/push.md | 43 +++++++++++++++++++- 8 files changed, 168 insertions(+), 40 deletions(-) diff --git a/cli/command/image/push.go b/cli/command/image/push.go index b79bb7319a..62cad07f11 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -9,12 +9,15 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" + "github.com/pkg/errors" "github.com/spf13/cobra" ) type pushOptions struct { + all bool remote string untrusted bool quiet bool @@ -35,6 +38,7 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() + flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tagged images in the repository") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output") command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) @@ -44,8 +48,16 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command { // RunPush performs a push against the engine based on the specified options func RunPush(dockerCli command.Cli, opts pushOptions) error { ref, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { + switch { + case err != nil: return err + case opts.all && !reference.IsNameOnly(ref): + return errors.New("tag can't be used with --all-tags/-a") + case !opts.all && reference.IsNameOnly(ref): + ref = reference.TagNameOnly(ref) + if tagged, ok := ref.(reference.Tagged); ok && !opts.quiet { + _, _ = fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag()) + } } // Resolve the Repository name from fqn to RepositoryInfo @@ -58,18 +70,28 @@ func RunPush(dockerCli command.Cli, opts pushOptions) error { // Resolve the Auth config relevant for this server authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) + encodedAuth, err := command.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") - - if !opts.untrusted { - return TrustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) + options := types.ImagePushOptions{ + All: opts.all, + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, } - responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege) + responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) if err != nil { return err } defer responseBody.Close() + if !opts.untrusted { + // TODO PushTrustedReference currently doesn't respect `--quiet` + return PushTrustedReference(dockerCli, repoInfo, ref, authConfig, responseBody) + } + if opts.quiet { err = jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(ioutil.Discard), nil) if err == nil { diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 75f3ab1ccc..f54318cceb 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -31,8 +31,8 @@ type target struct { } // TrustedPush handles content trust pushing of an image -func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege) +func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, options types.ImagePushOptions) error { + responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) if err != nil { return err } @@ -167,20 +167,6 @@ func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target) return repo.AddTarget(target, signableRoles...) } -// imagePushPrivileged push the image -func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Reference, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return nil, err - } - options := types.ImagePushOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - } - - return cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) -} - // trustedPull handles content trust pulling of an image func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error { refs, err := getTrustedPullTargets(cli, imgRefAndAuth) diff --git a/cli/command/trust/sign.go b/cli/command/trust/sign.go index 234a057c32..0841e7b9c7 100644 --- a/cli/command/trust/sign.go +++ b/cli/command/trust/sign.go @@ -12,6 +12,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/trust" + "github.com/docker/docker/api/types" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/theupdateframework/notary/client" @@ -90,7 +91,17 @@ func runSignImage(cli command.Cli, options signOptions) error { return err } fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName) - return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), requestPrivilege) + + authConfig := command.ResolveAuthConfig(ctx, cli, imgRefAndAuth.RepoInfo().Index) + encodedAuth, err := command.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + options := types.ImagePushOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + } + return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), options) default: return err } diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 39f3397d0c..76e2ee685c 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -3067,7 +3067,7 @@ _docker_image_pull() { _docker_image_push() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--disable-content-trust=false --help --quiet -q" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--all-tags -a --disable-content-trust=false --help --quiet -q" -- "$cur" ) ) ;; *) local counter=$(__docker_pos_first_nonflag) diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 32a9a9db20..cfda36755c 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -1078,6 +1078,7 @@ __docker_image_subcommand() { (push) _arguments $(__docker_arguments) \ $opts_help \ + "($help -a --all-tags)"{-a,--all-tags}"[Push all tagged images in the repository]" \ "($help)--disable-content-trust[Skip image signing]" \ "($help -): :__docker_complete_images" && ret=0 ;; diff --git a/docs/reference/commandline/push.md b/docs/reference/commandline/push.md index 336ff5f6d5..c5126ec0df 100644 --- a/docs/reference/commandline/push.md +++ b/docs/reference/commandline/push.md @@ -21,6 +21,7 @@ Usage: docker push [OPTIONS] NAME[:TAG] Push an image or a repository to a registry Options: + -a, --all-tags Push all tagged images in the repository --disable-content-trust Skip image signing (default true) --help Print usage -q, --quiet Suppress verbose output @@ -28,13 +29,13 @@ Options: ## Description -Use `docker push` to share your images to the [Docker Hub](https://hub.docker.com) +Use `docker image push` to share your images to the [Docker Hub](https://hub.docker.com) registry or to a self-hosted one. -Refer to the [`docker tag`](tag.md) reference for more information about valid +Refer to the [`docker image tag`](tag.md) reference for more information about valid image and tag names. -Killing the `docker push` process, for example by pressing `CTRL-c` while it is +Killing the `docker image push` process, for example by pressing `CTRL-c` while it is running in a terminal, terminates the push operation. Progress bars are shown during docker push, which show the uncompressed size. The @@ -54,12 +55,12 @@ this via the `--max-concurrent-uploads` daemon option. See the ### Push a new image to a registry -First save the new image by finding the container ID (using [`docker ps`](ps.md)) +First save the new image by finding the container ID (using [`docker container ls`](ps.md)) and then committing it to a new image name. Note that only `a-z0-9-_.` are allowed when naming images: ```bash -$ docker commit c16378f943fe rhel-httpd +$ docker container commit c16378f943fe rhel-httpd:latest ``` Now, push the image to the registry using the image ID. In this example the @@ -68,16 +69,63 @@ this, tag the image with the host name or IP address, and the port of the registry: ```bash -$ docker tag rhel-httpd registry-host:5000/myadmin/rhel-httpd +$ docker image tag rhel-httpd:latest registry-host:5000/myadmin/rhel-httpd:latest -$ docker push registry-host:5000/myadmin/rhel-httpd +$ docker image push registry-host:5000/myadmin/rhel-httpd:latest ``` Check that this worked by running: ```bash -$ docker images +$ docker image ls ``` You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd` listed. + +### Push all tags of an image + +Use the `-a` (or `--all-tags`) option to push To push all tags of a local image. + +The following example creates multiple tags for an image, and pushes all those +tags to Docker Hub. + + +```bash +$ docker image tag myimage registry-host:5000/myname/myimage:latest +$ docker image tag myimage registry-host:5000/myname/myimage:v1.0.1 +$ docker image tag myimage registry-host:5000/myname/myimage:v1.0 +$ docker image tag myimage registry-host:5000/myname/myimage:v1 +``` + +The image is now tagged under multiple names: + +```bash +$ docker image ls + +REPOSITORY TAG IMAGE ID CREATED SIZE +myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB +registry-host:5000/myname/myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB +registry-host:5000/myname/myimage v1 6d5fcfe5ff17 2 hours ago 1.22MB +registry-host:5000/myname/myimage v1.0 6d5fcfe5ff17 2 hours ago 1.22MB +registry-host:5000/myname/myimage v1.0.1 6d5fcfe5ff17 2 hours ago 1.22MB +``` + +When pushing with the `--all-tags` option, all tags of the `registry-host:5000/myname/myimage` +image are pushed: + + +```bash +$ docker image push --all-tags registry-host:5000/myname/myimage + +The push refers to repository [registry-host:5000/myname/myimage] +195be5f8be1d: Pushed +latest: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 +195be5f8be1d: Layer already exists +v1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 +195be5f8be1d: Layer already exists +v1.0: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 +195be5f8be1d: Layer already exists +v1.0.1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 +``` + diff --git a/e2e/image/push_test.go b/e2e/image/push_test.go index d5607cd9a4..ab68f87363 100644 --- a/e2e/image/push_test.go +++ b/e2e/image/push_test.go @@ -30,12 +30,30 @@ const ( privkey4 = "./testdata/notary/delgkey4.key" ) +func TestPushAllTags(t *testing.T) { + skip.If(t, environment.RemoteDaemon()) + + _ = createImage(t, "push-all-tags", "latest", "v1", "v1.0", "v1.0.1") + result := icmd.RunCmd(icmd.Command("docker", "push", "--all-tags", registryPrefix+"/push-all-tags")) + + result.Assert(t, icmd.Success) + golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden") + output.Assert(t, result.Stdout(), map[int]func(string) error{ + 0: output.Equals("The push refers to repository [registry:5000/push-all-tags]"), + 1: output.Equals("5bef08742407: Preparing"), + 3: output.Equals("latest: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"), + 6: output.Equals("v1: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"), + 9: output.Equals("v1.0: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"), + 12: output.Equals("v1.0.1: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"), + }) +} + func TestPushWithContentTrust(t *testing.T) { skip.If(t, environment.RemoteDaemon()) dir := fixtures.SetupConfigFile(t) defer dir.Remove() - image := createImage(t, registryPrefix, "trust-push", "latest") + image := createImage(t, "trust-push", "latest") result := icmd.RunCmd(icmd.Command("docker", "push", image), fixtures.WithConfig(dir.Path()), @@ -68,7 +86,7 @@ func TestPushWithContentTrustUnreachableServer(t *testing.T) { dir := fixtures.SetupConfigFile(t) defer dir.Remove() - image := createImage(t, registryPrefix, "trust-push-unreachable", "latest") + image := createImage(t, "trust-push-unreachable", "latest") result := icmd.RunCmd(icmd.Command("docker", "push", image), fixtures.WithConfig(dir.Path()), @@ -86,7 +104,7 @@ func TestPushWithContentTrustExistingTag(t *testing.T) { dir := fixtures.SetupConfigFile(t) defer dir.Remove() - image := createImage(t, registryPrefix, "trust-push-existing", "latest") + image := createImage(t, "trust-push-existing", "latest") result := icmd.RunCmd(icmd.Command("docker", "push", image)) result.Assert(t, icmd.Success) @@ -297,11 +315,14 @@ func TestPushWithContentTrustSignsForRolesWithKeysAndValidPaths(t *testing.T) { }) } -func createImage(t *testing.T, registryPrefix, repo, tag string) string { - image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag) +func createImage(t *testing.T, repo string, tags ...string) string { icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success) - return image + + for _, tag := range tags { + image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag) + icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success) + } + return fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tags[0]) } //nolint: unparam diff --git a/man/src/image/push.md b/man/src/image/push.md index 8b4334d273..61e96d4c27 100644 --- a/man/src/image/push.md +++ b/man/src/image/push.md @@ -23,8 +23,8 @@ registry is on host named `registry-host` and listening on port `5000`. To do this, tag the image with the host name or IP address, and the port of the registry: - # docker image tag rhel-httpd registry-host:5000/myadmin/rhel-httpd - # docker image push registry-host:5000/myadmin/rhel-httpd + # docker image tag rhel-httpd registry-host:5000/myadmin/rhel-httpd:latest + # docker image push registry-host:5000/myadmin/rhel-httpd:latest Check that this worked by running: @@ -32,3 +32,42 @@ Check that this worked by running: You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd` listed. + +### Push all tags of an image + +Use the `-a` (or `--all-tags`) option to push To push all tags of a local image. + +The following example creates multiple tags for an image, and pushes all those +tags to Docker Hub. + + $ docker image tag myimage registry-host:5000/myname/myimage:latest + $ docker image tag myimage registry-host:5000/myname/myimage:v1.0.1 + $ docker image tag myimage registry-host:5000/myname/myimage:v1.0 + $ docker image tag myimage registry-host:5000/myname/myimage:v1 + +The image is now tagged under multiple names: + + $ docker image ls + + REPOSITORY TAG IMAGE ID CREATED SIZE + myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB + registry-host:5000/myname/myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB + registry-host:5000/myname/myimage v1 6d5fcfe5ff17 2 hours ago 1.22MB + registry-host:5000/myname/myimage v1.0 6d5fcfe5ff17 2 hours ago 1.22MB + registry-host:5000/myname/myimage v1.0.1 6d5fcfe5ff17 2 hours ago 1.22MB + +When pushing with the `--all-tags` option, all tags of the `registry-host:5000/myname/myimage` +image are pushed: + + + $ docker image push --all-tags registry-host:5000/myname/myimage + + The push refers to repository [registry-host:5000/myname/myimage] + 195be5f8be1d: Pushed + latest: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 + 195be5f8be1d: Layer already exists + v1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 + 195be5f8be1d: Layer already exists + v1.0: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527 + 195be5f8be1d: Layer already exists + v1.0.1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527