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") return nil, errors.New("cannot push a digest reference")
} }
tag := ""
name := reference.FamiliarName(ref) name := reference.FamiliarName(ref)
if nameTaggedRef, isNamedTagged := ref.(reference.NamedTagged); isNamedTagged {
tag = nameTaggedRef.Tag()
}
query := url.Values{} 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) resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth)
if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {

View File

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