Skip to content

Commit 2ff6f48

Browse files
authored
add remote.Head (#770)
* add remote.Head this is useful for fetching a reference's digest/size/type via a HEAD request, which does not count towards Docker Hub image pull rate limits Signed-off-by: Alex Suraci <[email protected]> * fix up modules Signed-off-by: Alex Suraci <[email protected]>
1 parent d7f8d06 commit 2ff6f48

File tree

7 files changed

+252
-19
lines changed

7 files changed

+252
-19
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.sum

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/v1/remote/descriptor.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"io/ioutil"
2323
"net/http"
2424
"net/url"
25+
"strconv"
2526
"strings"
2627

2728
"github.com/google/go-containerregistry/pkg/logs"
@@ -76,6 +77,8 @@ func (d *Descriptor) RawManifest() ([]byte, error) {
7677
// Get returns a remote.Descriptor for the given reference. The response from
7778
// the registry is left un-interpreted, for the most part. This is useful for
7879
// querying what kind of artifact a reference represents.
80+
//
81+
// See Head if you don't need the response body.
7982
func Get(ref name.Reference, options ...Option) (*Descriptor, error) {
8083
acceptable := []types.MediaType{
8184
// Just to look at them.
@@ -87,6 +90,30 @@ func Get(ref name.Reference, options ...Option) (*Descriptor, error) {
8790
return get(ref, acceptable, options...)
8891
}
8992

93+
// Head returns a v1.Descriptor for the given reference by issuing a HEAD
94+
// request.
95+
func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) {
96+
acceptable := []types.MediaType{
97+
// Just to look at them.
98+
types.DockerManifestSchema1,
99+
types.DockerManifestSchema1Signed,
100+
}
101+
acceptable = append(acceptable, acceptableImageMediaTypes...)
102+
acceptable = append(acceptable, acceptableIndexMediaTypes...)
103+
104+
o, err := makeOptions(ref.Context(), options...)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
f, err := makeFetcher(ref, o)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
return f.headManifest(ref, acceptable)
115+
}
116+
90117
// Handle options and fetch the manifest with the acceptable MediaTypes in the
91118
// Accept header.
92119
func get(ref name.Reference, acceptable []types.MediaType, options ...Option) (*Descriptor, error) {
@@ -277,6 +304,55 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
277304
return manifest, &desc, nil
278305
}
279306

307+
func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType) (*v1.Descriptor, error) {
308+
u := f.url("manifests", ref.Identifier())
309+
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
310+
if err != nil {
311+
return nil, err
312+
}
313+
accept := []string{}
314+
for _, mt := range acceptable {
315+
accept = append(accept, string(mt))
316+
}
317+
req.Header.Set("Accept", strings.Join(accept, ","))
318+
319+
resp, err := f.Client.Do(req.WithContext(f.context))
320+
if err != nil {
321+
return nil, err
322+
}
323+
defer resp.Body.Close()
324+
325+
if err := transport.CheckError(resp, http.StatusOK); err != nil {
326+
return nil, err
327+
}
328+
329+
mediaType := types.MediaType(resp.Header.Get("Content-Type"))
330+
331+
size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
332+
if err != nil {
333+
return nil, err
334+
}
335+
336+
digest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
337+
if err != nil {
338+
return nil, err
339+
}
340+
341+
// Validate the digest matches what we asked for, if pulling by digest.
342+
if dgst, ok := ref.(name.Digest); ok {
343+
if digest.String() != dgst.DigestStr() {
344+
return nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref)
345+
}
346+
}
347+
348+
// Return all this info since we have to calculate it anyway.
349+
return &v1.Descriptor{
350+
Digest: digest,
351+
Size: size,
352+
MediaType: mediaType,
353+
}, nil
354+
}
355+
280356
func (f *fetcher) fetchBlob(h v1.Hash) (io.ReadCloser, error) {
281357
u := f.url("blobs", h.String())
282358
req, err := http.NewRequest(http.MethodGet, u.String(), nil)

pkg/v1/remote/descriptor_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"net/http"
2020
"net/http/httptest"
2121
"net/url"
22+
"strconv"
2223
"testing"
2324

2425
"github.com/google/go-cmp/cmp"
@@ -123,3 +124,53 @@ func TestGetImageAsIndex(t *testing.T) {
123124
t.Errorf("ImageIndex() = %v, expected err", err)
124125
}
125126
}
127+
128+
func TestHeadSchema1(t *testing.T) {
129+
expectedRepo := "foo/bar"
130+
mediaType := types.DockerManifestSchema1Signed
131+
fakeDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
132+
response := []byte("doesn't matter")
133+
manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
134+
135+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136+
switch r.URL.Path {
137+
case "/v2/":
138+
w.WriteHeader(http.StatusOK)
139+
case manifestPath:
140+
if r.Method != http.MethodHead {
141+
t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
142+
}
143+
w.Header().Set("Content-Type", string(mediaType))
144+
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
145+
w.Header().Set("Docker-Content-Digest", fakeDigest)
146+
w.Write(response)
147+
default:
148+
t.Fatalf("Unexpected path: %v", r.URL.Path)
149+
}
150+
}))
151+
defer server.Close()
152+
u, err := url.Parse(server.URL)
153+
if err != nil {
154+
t.Fatalf("url.Parse(%v) = %v", server.URL, err)
155+
}
156+
157+
tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
158+
159+
// Head should succeed even for invalid json. We don't parse the response.
160+
desc, err := Head(tag)
161+
if err != nil {
162+
t.Fatalf("Head(%s) = %v", tag, err)
163+
}
164+
165+
if desc.MediaType != mediaType {
166+
t.Errorf("Descriptor.MediaType = %q, expected %q", desc.MediaType, mediaType)
167+
}
168+
169+
if desc.Digest.String() != fakeDigest {
170+
t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest)
171+
}
172+
173+
if desc.Size != int64(len(response)) {
174+
t.Errorf("Descriptor.Size = %q, expected %q", desc.Size, len(response))
175+
}
176+
}

vendor/golang.org/x/tools/go/packages/golist.go

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/golang.org/x/tools/go/packages/golist_overlay.go

Lines changed: 101 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/modules.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ golang.org/x/text/unicode/norm
250250
# golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
251251
## explicit
252252
golang.org/x/time/rate
253-
# golang.org/x/tools v0.0.0-20200911153331-7ad463ce66dd
253+
# golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3
254254
## explicit
255255
golang.org/x/tools/go/ast/astutil
256256
golang.org/x/tools/go/gcexportdata

0 commit comments

Comments
 (0)