Skip to content

Commit 5fb5e80

Browse files
committed
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 moby/moby#3411 and the pull request that implemented this: moby/moby#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 <[email protected]>
1 parent ddde460 commit 5fb5e80

7 files changed

Lines changed: 171 additions & 38 deletions

File tree

cli/command/image/push.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import (
77
"github.com/docker/cli/cli"
88
"github.com/docker/cli/cli/command"
99
"github.com/docker/distribution/reference"
10+
"github.com/docker/docker/api/types"
1011
"github.com/docker/docker/pkg/jsonmessage"
1112
"github.com/docker/docker/registry"
13+
"github.com/pkg/errors"
1214
"github.com/spf13/cobra"
1315
)
1416

1517
type pushOptions struct {
18+
all bool
1619
remote string
1720
untrusted bool
1821
quiet bool
@@ -33,6 +36,7 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
3336
}
3437

3538
flags := cmd.Flags()
39+
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tagged images in the repository")
3640
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
3741
command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
3842

@@ -42,8 +46,16 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
4246
// RunPush performs a push against the engine based on the specified options
4347
func RunPush(dockerCli command.Cli, opts pushOptions) error {
4448
ref, err := reference.ParseNormalizedNamed(opts.remote)
45-
if err != nil {
49+
switch {
50+
case err != nil:
4651
return err
52+
case opts.all && !reference.IsNameOnly(ref):
53+
return errors.New("tag can't be used with --all-tags/-a")
54+
case !opts.all && reference.IsNameOnly(ref):
55+
ref = reference.TagNameOnly(ref)
56+
if tagged, ok := ref.(reference.Tagged); ok && !opts.quiet {
57+
_, _ = fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag())
58+
}
4759
}
4860

4961
// Resolve the Repository name from fqn to RepositoryInfo
@@ -56,18 +68,31 @@ func RunPush(dockerCli command.Cli, opts pushOptions) error {
5668

5769
// Resolve the Auth config relevant for this server
5870
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
71+
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
72+
if err != nil {
73+
return err
74+
}
5975
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
76+
options := types.ImagePushOptions{
77+
All: opts.all,
78+
RegistryAuth: encodedAuth,
79+
PrivilegeFunc: requestPrivilege,
80+
}
6081

6182
if !opts.untrusted {
62-
return TrustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
83+
return TrustedPush(ctx, dockerCli, repoInfo, ref, authConfig, options)
6384
}
6485

65-
responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege)
86+
responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
6687
if err != nil {
6788
return err
6889
}
6990

7091
defer responseBody.Close()
92+
if !opts.untrusted {
93+
return PushTrustedReference(dockerCli, repoInfo, ref, authConfig, responseBody)
94+
}
95+
7196
if !opts.quiet {
7297
return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
7398
}

cli/command/image/trust.go

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ type target struct {
3131
}
3232

3333
// TrustedPush handles content trust pushing of an image
34-
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
35-
responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege)
34+
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, options types.ImagePushOptions) error {
35+
responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
3636
if err != nil {
3737
return err
3838
}
@@ -167,20 +167,6 @@ func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target)
167167
return repo.AddTarget(target, signableRoles...)
168168
}
169169

170-
// imagePushPrivileged push the image
171-
func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Reference, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) {
172-
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
173-
if err != nil {
174-
return nil, err
175-
}
176-
options := types.ImagePushOptions{
177-
RegistryAuth: encodedAuth,
178-
PrivilegeFunc: requestPrivilege,
179-
}
180-
181-
return cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
182-
}
183-
184170
// trustedPull handles content trust pulling of an image
185171
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error {
186172
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)

cli/command/trust/sign.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/docker/cli/cli/command"
1313
"github.com/docker/cli/cli/command/image"
1414
"github.com/docker/cli/cli/trust"
15+
"github.com/docker/docker/api/types"
1516
"github.com/pkg/errors"
1617
"github.com/spf13/cobra"
1718
"github.com/theupdateframework/notary/client"
@@ -90,7 +91,17 @@ func runSignImage(cli command.Cli, options signOptions) error {
9091
return err
9192
}
9293
fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName)
93-
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), requestPrivilege)
94+
95+
authConfig := command.ResolveAuthConfig(ctx, cli, imgRefAndAuth.RepoInfo().Index)
96+
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
97+
if err != nil {
98+
return err
99+
}
100+
options := types.ImagePushOptions{
101+
RegistryAuth: encodedAuth,
102+
PrivilegeFunc: requestPrivilege,
103+
}
104+
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), options)
94105
default:
95106
return err
96107
}

contrib/completion/bash/docker

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3066,7 +3066,7 @@ _docker_image_pull() {
30663066
_docker_image_push() {
30673067
case "$cur" in
30683068
-*)
3069-
COMPREPLY=( $( compgen -W "--disable-content-trust=false --help --quiet -q" -- "$cur" ) )
3069+
COMPREPLY=( $( compgen -W "--all-tags -a --disable-content-trust=false --help --quiet -q" -- "$cur" ) )
30703070
;;
30713071
*)
30723072
local counter=$(__docker_pos_first_nonflag)

docs/reference/commandline/push.md

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@ Usage: docker push [OPTIONS] NAME[:TAG]
2121
Push an image or a repository to a registry
2222

2323
Options:
24+
-a, --all-tags Push all tagged images in the repository
2425
--disable-content-trust Skip image signing (default true)
2526
--help Print usage
2627
-q, --quiet Suppress verbose output
2728
```
2829

2930
## Description
3031

31-
Use `docker push` to share your images to the [Docker Hub](https://hub.docker.com)
32+
Use `docker image push` to share your images to the [Docker Hub](https://hub.docker.com)
3233
registry or to a self-hosted one.
3334

34-
Refer to the [`docker tag`](tag.md) reference for more information about valid
35+
Refer to the [`docker image tag`](tag.md) reference for more information about valid
3536
image and tag names.
3637

37-
Killing the `docker push` process, for example by pressing `CTRL-c` while it is
38+
Killing the `docker image push` process, for example by pressing `CTRL-c` while it is
3839
running in a terminal, terminates the push operation.
3940

4041
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
5455

5556
### Push a new image to a registry
5657

57-
First save the new image by finding the container ID (using [`docker ps`](ps.md))
58+
First save the new image by finding the container ID (using [`docker container ls`](ps.md))
5859
and then committing it to a new image name. Note that only `a-z0-9-_.` are
5960
allowed when naming images:
6061

6162
```bash
62-
$ docker commit c16378f943fe rhel-httpd
63+
$ docker container commit c16378f943fe rhel-httpd:latest
6364
```
6465

6566
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
6869
registry:
6970

7071
```bash
71-
$ docker tag rhel-httpd registry-host:5000/myadmin/rhel-httpd
72+
$ docker image tag rhel-httpd:latest registry-host:5000/myadmin/rhel-httpd:latest
7273

73-
$ docker push registry-host:5000/myadmin/rhel-httpd
74+
$ docker image push registry-host:5000/myadmin/rhel-httpd:latest
7475
```
7576

7677
Check that this worked by running:
7778

7879
```bash
79-
$ docker images
80+
$ docker image ls
8081
```
8182

8283
You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd`
8384
listed.
85+
86+
### Push all tags of an image
87+
88+
Use the `-a` (or `--all-tags`) option to push To push all tags of a local image.
89+
90+
The following example creates multiple tags for an image, and pushes all those
91+
tags to Docker Hub.
92+
93+
94+
```bash
95+
$ docker image tag myimage registry-host:5000/myname/myimage:latest
96+
$ docker image tag myimage registry-host:5000/myname/myimage:v1.0.1
97+
$ docker image tag myimage registry-host:5000/myname/myimage:v1.0
98+
$ docker image tag myimage registry-host:5000/myname/myimage:v1
99+
```
100+
101+
The image is now tagged under multiple names:
102+
103+
```bash
104+
$ docker image ls
105+
106+
REPOSITORY TAG IMAGE ID CREATED SIZE
107+
myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB
108+
localhost:5000/myname/myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB
109+
localhost:5000/myname/myimage v1 6d5fcfe5ff17 2 hours ago 1.22MB
110+
localhost:5000/myname/myimage v1.0 6d5fcfe5ff17 2 hours ago 1.22MB
111+
localhost:5000/myname/myimage v1.0.1 6d5fcfe5ff17 2 hours ago 1.22MB
112+
```
113+
114+
When pushing with the `--all-tags` option, all tags of the `registry-host:5000/myname/myimage`
115+
image are pushed:
116+
117+
118+
```bash
119+
$ docker image push --all-tags registry-host:5000/myname/myimage
120+
121+
The push refers to repository [registry-host:5000/myname/myimage]
122+
195be5f8be1d: Pushed
123+
latest: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
124+
195be5f8be1d: Layer already exists
125+
v1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
126+
195be5f8be1d: Layer already exists
127+
v1.0: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
128+
195be5f8be1d: Layer already exists
129+
v1.0.1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
130+
```
131+

e2e/image/push_test.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,33 @@ const (
3030
privkey4 = "./testdata/notary/delgkey4.key"
3131
)
3232

33+
func TestPushAllTags(t *testing.T) {
34+
skip.If(t, environment.RemoteDaemon())
35+
36+
dir := fixtures.SetupConfigFile(t)
37+
defer dir.Remove()
38+
_ = createImage(t, "push-all-tags", "latest", "v1", "v1.0", "v1.0.1")
39+
40+
result := icmd.RunCmd(icmd.Command("docker", "push", "--all-tags", registryPrefix+"/push-all-tags"))
41+
42+
result.Assert(t, icmd.Success)
43+
golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden")
44+
output.Assert(t, result.Stdout(), map[int]func(string) error{
45+
0: output.Equals("The push refers to repository [registry:5000/push-all-tags]"),
46+
1: output.Equals("5bef08742407: Preparing"),
47+
3: output.Equals("latest: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"),
48+
6: output.Equals("v1: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"),
49+
9: output.Equals("v1.0: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"),
50+
12: output.Equals("v1.0.1: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"),
51+
})
52+
}
53+
3354
func TestPushWithContentTrust(t *testing.T) {
3455
skip.If(t, environment.RemoteDaemon())
3556

3657
dir := fixtures.SetupConfigFile(t)
3758
defer dir.Remove()
38-
image := createImage(t, registryPrefix, "trust-push", "latest")
59+
image := createImage(t, "trust-push", "latest")
3960

4061
result := icmd.RunCmd(icmd.Command("docker", "push", image),
4162
fixtures.WithConfig(dir.Path()),
@@ -60,7 +81,7 @@ func TestPushWithContentTrustUnreachableServer(t *testing.T) {
6081

6182
dir := fixtures.SetupConfigFile(t)
6283
defer dir.Remove()
63-
image := createImage(t, registryPrefix, "trust-push-unreachable", "latest")
84+
image := createImage(t, "trust-push-unreachable", "latest")
6485

6586
result := icmd.RunCmd(icmd.Command("docker", "push", image),
6687
fixtures.WithConfig(dir.Path()),
@@ -78,7 +99,7 @@ func TestPushWithContentTrustExistingTag(t *testing.T) {
7899

79100
dir := fixtures.SetupConfigFile(t)
80101
defer dir.Remove()
81-
image := createImage(t, registryPrefix, "trust-push-existing", "latest")
102+
image := createImage(t, "trust-push-existing", "latest")
82103

83104
result := icmd.RunCmd(icmd.Command("docker", "push", image))
84105
result.Assert(t, icmd.Success)
@@ -289,11 +310,14 @@ func TestPushWithContentTrustSignsForRolesWithKeysAndValidPaths(t *testing.T) {
289310
})
290311
}
291312

292-
func createImage(t *testing.T, registryPrefix, repo, tag string) string {
293-
image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
313+
func createImage(t *testing.T, repo string, tags ...string) string {
294314
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
295-
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
296-
return image
315+
316+
for _, tag := range tags {
317+
image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
318+
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
319+
}
320+
return fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tags[0])
297321
}
298322

299323
//nolint: unparam

man/src/image/push.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,51 @@ registry is on host named `registry-host` and listening on port `5000`. To do
2323
this, tag the image with the host name or IP address, and the port of the
2424
registry:
2525

26-
# docker image tag rhel-httpd registry-host:5000/myadmin/rhel-httpd
27-
# docker image push registry-host:5000/myadmin/rhel-httpd
26+
# docker image tag rhel-httpd registry-host:5000/myadmin/rhel-httpd:latest
27+
# docker image push registry-host:5000/myadmin/rhel-httpd:latest
2828

2929
Check that this worked by running:
3030

3131
# docker image ls
3232

3333
You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd`
3434
listed.
35+
36+
### Push all tags of an image
37+
38+
Use the `-a` (or `--all-tags`) option to push To push all tags of a local image.
39+
40+
The following example creates multiple tags for an image, and pushes all those
41+
tags to Docker Hub.
42+
43+
$ docker image tag myimage registry-host:5000/myname/myimage:latest
44+
$ docker image tag myimage registry-host:5000/myname/myimage:v1.0.1
45+
$ docker image tag myimage registry-host:5000/myname/myimage:v1.0
46+
$ docker image tag myimage registry-host:5000/myname/myimage:v1
47+
48+
The image is now tagged under multiple names:
49+
50+
$ docker image ls
51+
52+
REPOSITORY TAG IMAGE ID CREATED SIZE
53+
myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB
54+
localhost:5000/myname/myimage latest 6d5fcfe5ff17 2 hours ago 1.22MB
55+
localhost:5000/myname/myimage v1 6d5fcfe5ff17 2 hours ago 1.22MB
56+
localhost:5000/myname/myimage v1.0 6d5fcfe5ff17 2 hours ago 1.22MB
57+
localhost:5000/myname/myimage v1.0.1 6d5fcfe5ff17 2 hours ago 1.22MB
58+
59+
When pushing with the `--all-tags` option, all tags of the `registry-host:5000/myname/myimage`
60+
image are pushed:
61+
62+
63+
$ docker image push --all-tags registry-host:5000/myname/myimage
64+
65+
The push refers to repository [registry-host:5000/myname/myimage]
66+
195be5f8be1d: Pushed
67+
latest: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
68+
195be5f8be1d: Layer already exists
69+
v1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
70+
195be5f8be1d: Layer already exists
71+
v1.0: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527
72+
195be5f8be1d: Layer already exists
73+
v1.0.1: digest: sha256:edafc0a0fb057813850d1ba44014914ca02d671ae247107ca70c94db686e7de6 size: 4527

0 commit comments

Comments
 (0)