client.ImagePush(): default to ":latest" instead of "all tags"

The `docker push` command up until docker v0.9.1 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`,
and `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 issue 3411 and PR 4948 (commit e648a186d6).

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".

`docker pull` had a similar behavior, but PR 7759 (9c08364a41)
changed the behavior to default to the `:latest` tag, and added a `--all-tags` flag
to the CLI to optionally pull all images.

This patch implements the API client changes to make `docker push` match the behavior
of `docker pull`, and default to pull a single image, unless the `all` option is passed.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-12-10 12:24:14 +01:00
parent 2d467dc8d0
commit d135dc242e
No known key found for this signature in database
GPG key ID: 76698F39D527CE8C
2 changed files with 58 additions and 39 deletions

View file

@ -25,15 +25,14 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options types.Im
return nil, errors.New("cannot push a digest reference")
}
tag := ""
name := reference.FamiliarName(ref)
if nameTaggedRef, isNamedTagged := ref.(reference.NamedTagged); isNamedTagged {
tag = nameTaggedRef.Tag()
}
query := url.Values{}
query.Set("tag", tag)
if !options.All {
ref = reference.TagNameOnly(ref)
if tagged, ok := ref.(reference.Tagged); ok {
query.Set("tag", tagged.Tag())
}
}
resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth)
if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {

View file

@ -131,50 +131,70 @@ func TestImagePushWithPrivilegedFuncNoError(t *testing.T) {
func TestImagePushWithoutErrors(t *testing.T) {
expectedOutput := "hello world"
expectedURLFormat := "/images/%s/push"
pullCases := []struct {
testCases := []struct {
all bool
reference string
expectedImage string
expectedTag string
}{
{
all: false,
reference: "myimage",
expectedImage: "myimage",
expectedTag: "latest",
},
{
all: false,
reference: "myimage:tag",
expectedImage: "myimage",
expectedTag: "tag",
},
{
all: true,
reference: "myimage",
expectedImage: "myimage",
expectedTag: "",
},
{
reference: "myimage:tag",
all: true,
reference: "myimage:anything",
expectedImage: "myimage",
expectedTag: "tag",
expectedTag: "",
},
}
for _, pullCase := range pullCases {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
expectedURL := fmt.Sprintf(expectedURLFormat, pullCase.expectedImage)
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
tag := query.Get("tag")
if tag != pullCase.expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
}, nil
}),
}
resp, err := client.ImagePush(context.Background(), pullCase.reference, types.ImagePushOptions{})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
}
for _, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("%s,all-tags=%t", tc.reference, tc.all), func(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
expectedURL := fmt.Sprintf(expectedURLFormat, tc.expectedImage)
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
tag := query.Get("tag")
if tag != tc.expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", tc.expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
}, nil
}),
}
resp, err := client.ImagePush(context.Background(), tc.reference, types.ImagePushOptions{
All: tc.all,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
}
})
}
}