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 <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-12-09 14:48:42 +01:00
parent 3c8c0ff380
commit 9e620e990f
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
8 changed files with 168 additions and 40 deletions

View File

@ -9,12 +9,15 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type pushOptions struct { type pushOptions struct {
all bool
remote string remote string
untrusted bool untrusted bool
quiet bool quiet bool
@ -35,6 +38,7 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
} }
flags := cmd.Flags() 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") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) 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 // RunPush performs a push against the engine based on the specified options
func RunPush(dockerCli command.Cli, opts pushOptions) error { func RunPush(dockerCli command.Cli, opts pushOptions) error {
ref, err := reference.ParseNormalizedNamed(opts.remote) ref, err := reference.ParseNormalizedNamed(opts.remote)
if err != nil { switch {
case err != nil:
return err 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 // 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 // Resolve the Auth config relevant for this server
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
options := types.ImagePushOptions{
if !opts.untrusted { All: opts.all,
return TrustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) 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 { if err != nil {
return err return err
} }
defer responseBody.Close() defer responseBody.Close()
if !opts.untrusted {
// TODO PushTrustedReference currently doesn't respect `--quiet`
return PushTrustedReference(dockerCli, repoInfo, ref, authConfig, responseBody)
}
if opts.quiet { if opts.quiet {
err = jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(ioutil.Discard), nil) err = jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(ioutil.Discard), nil)
if err == nil { if err == nil {

View File

@ -31,8 +31,8 @@ type target struct {
} }
// TrustedPush handles content trust pushing of an image // 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 { func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, options types.ImagePushOptions) error {
responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege) responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
if err != nil { if err != nil {
return err return err
} }
@ -167,20 +167,6 @@ func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target)
return repo.AddTarget(target, signableRoles...) 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 // trustedPull handles content trust pulling of an image
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error { func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error {
refs, err := getTrustedPullTargets(cli, imgRefAndAuth) refs, err := getTrustedPullTargets(cli, imgRefAndAuth)

View File

@ -12,6 +12,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/client"
@ -90,7 +91,17 @@ func runSignImage(cli command.Cli, options signOptions) error {
return err return err
} }
fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName) 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: default:
return err return err
} }

View File

@ -3067,7 +3067,7 @@ _docker_image_pull() {
_docker_image_push() { _docker_image_push() {
case "$cur" in 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) local counter=$(__docker_pos_first_nonflag)

View File

@ -1078,6 +1078,7 @@ __docker_image_subcommand() {
(push) (push)
_arguments $(__docker_arguments) \ _arguments $(__docker_arguments) \
$opts_help \ $opts_help \
"($help -a --all-tags)"{-a,--all-tags}"[Push all tagged images in the repository]" \
"($help)--disable-content-trust[Skip image signing]" \ "($help)--disable-content-trust[Skip image signing]" \
"($help -): :__docker_complete_images" && ret=0 "($help -): :__docker_complete_images" && ret=0
;; ;;

View File

@ -21,6 +21,7 @@ Usage: docker push [OPTIONS] NAME[:TAG]
Push an image or a repository to a registry Push an image or a repository to a registry
Options: Options:
-a, --all-tags Push all tagged images in the repository
--disable-content-trust Skip image signing (default true) --disable-content-trust Skip image signing (default true)
--help Print usage --help Print usage
-q, --quiet Suppress verbose output -q, --quiet Suppress verbose output
@ -28,13 +29,13 @@ Options:
## Description ## 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. 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. 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. running in a terminal, terminates the push operation.
Progress bars are shown during docker push, which show the uncompressed size. The 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 ### 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 and then committing it to a new image name. Note that only `a-z0-9-_.` are
allowed when naming images: allowed when naming images:
```bash ```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 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: registry:
```bash ```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: Check that this worked by running:
```bash ```bash
$ docker images $ docker image ls
``` ```
You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd` You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd`
listed. 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
```

View File

@ -30,12 +30,30 @@ const (
privkey4 = "./testdata/notary/delgkey4.key" 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) { func TestPushWithContentTrust(t *testing.T) {
skip.If(t, environment.RemoteDaemon()) skip.If(t, environment.RemoteDaemon())
dir := fixtures.SetupConfigFile(t) dir := fixtures.SetupConfigFile(t)
defer dir.Remove() defer dir.Remove()
image := createImage(t, registryPrefix, "trust-push", "latest") image := createImage(t, "trust-push", "latest")
result := icmd.RunCmd(icmd.Command("docker", "push", image), result := icmd.RunCmd(icmd.Command("docker", "push", image),
fixtures.WithConfig(dir.Path()), fixtures.WithConfig(dir.Path()),
@ -68,7 +86,7 @@ func TestPushWithContentTrustUnreachableServer(t *testing.T) {
dir := fixtures.SetupConfigFile(t) dir := fixtures.SetupConfigFile(t)
defer dir.Remove() 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), result := icmd.RunCmd(icmd.Command("docker", "push", image),
fixtures.WithConfig(dir.Path()), fixtures.WithConfig(dir.Path()),
@ -86,7 +104,7 @@ func TestPushWithContentTrustExistingTag(t *testing.T) {
dir := fixtures.SetupConfigFile(t) dir := fixtures.SetupConfigFile(t)
defer dir.Remove() 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 := icmd.RunCmd(icmd.Command("docker", "push", image))
result.Assert(t, icmd.Success) result.Assert(t, icmd.Success)
@ -297,11 +315,14 @@ func TestPushWithContentTrustSignsForRolesWithKeysAndValidPaths(t *testing.T) {
}) })
} }
func createImage(t *testing.T, registryPrefix, repo, tag string) string { func createImage(t *testing.T, repo string, tags ...string) string {
image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
for _, tag := range tags {
image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success) icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
return image }
return fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tags[0])
} }
//nolint: unparam //nolint: unparam

View File

@ -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 this, tag the image with the host name or IP address, and the port of the
registry: registry:
# docker image tag rhel-httpd 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 # docker image push registry-host:5000/myadmin/rhel-httpd:latest
Check that this worked by running: 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` You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd`
listed. 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