From a2015272c1442aec6e8e3049dd4694a5331e37eb Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Fri, 25 Aug 2017 17:15:10 -0400 Subject: [PATCH] Support HEAD requests without Docker-Content-Digest header A statically hosted registry that responds correctly to GET with a manifest will load the right digest (by looking at the manifest body and calculating the digest). If the registry returns a HEAD without `Docker-Content-Digest`, then the client Tags().Get() call will return an empty digest. This commit changes the client to fallback to loading the tag via GET if the `Docker-Content-Digest` header is not set. Signed-off-by: Clayton Coleman --- registry/client/repository.go | 3 +- registry/client/repository_test.go | 61 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/registry/client/repository.go b/registry/client/repository.go index 8bd2c3fb..9846c42c 100644 --- a/registry/client/repository.go +++ b/registry/client/repository.go @@ -321,7 +321,8 @@ func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, er defer resp.Body.Close() switch { - case resp.StatusCode >= 200 && resp.StatusCode < 400: + case resp.StatusCode >= 200 && resp.StatusCode < 400 && len(resp.Header.Get("Docker-Content-Digest")) > 0: + // if the response is a success AND a Docker-Content-Digest can be retrieved from the headers return descriptorFromResponse(resp) default: // if the response is an error - there will be no body to decode. diff --git a/registry/client/repository_test.go b/registry/client/repository_test.go index f22fa33d..45ef24cd 100644 --- a/registry/client/repository_test.go +++ b/registry/client/repository_test.go @@ -647,7 +647,38 @@ func addTestManifest(repo reference.Named, reference string, mediatype string, c }), }, }) +} +func addTestManifestWithoutDigestHeader(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) { + *m = append(*m, testutil.RequestResponseMapping{ + Request: testutil.Request{ + Method: "GET", + Route: "/v2/" + repo.Name() + "/manifests/" + reference, + }, + Response: testutil.Response{ + StatusCode: http.StatusOK, + Body: content, + Headers: http.Header(map[string][]string{ + "Content-Length": {fmt.Sprint(len(content))}, + "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, + "Content-Type": {mediatype}, + }), + }, + }) + *m = append(*m, testutil.RequestResponseMapping{ + Request: testutil.Request{ + Method: "HEAD", + Route: "/v2/" + repo.Name() + "/manifests/" + reference, + }, + Response: testutil.Response{ + StatusCode: http.StatusOK, + Headers: http.Header(map[string][]string{ + "Content-Length": {fmt.Sprint(len(content))}, + "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, + "Content-Type": {mediatype}, + }), + }, + }) } func checkEqualManifest(m1, m2 *schema1.SignedManifest) error { @@ -994,6 +1025,36 @@ func TestObtainsErrorForMissingTag(t *testing.T) { } } +func TestObtainsManifestForTagWithoutHeaders(t *testing.T) { + repo, _ := reference.WithName("test.example.com/repo") + + var m testutil.RequestResponseMap + m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) + _, pl, err := m1.Payload() + if err != nil { + t.Fatal(err) + } + addTestManifestWithoutDigestHeader(repo, "1.0.0", schema1.MediaTypeSignedManifest, pl, &m) + + e, c := testServer(m) + defer c() + + ctx := context.Background() + r, err := NewRepository(ctx, repo, e, nil) + if err != nil { + t.Fatal(err) + } + + tagService := r.Tags(ctx) + + desc, err := tagService.Get(ctx, "1.0.0") + if err != nil { + t.Fatalf("Expected no error") + } + if desc.Digest != dgst { + t.Fatalf("Unexpected digest") + } +} func TestManifestTagsPaginated(t *testing.T) { s := httptest.NewServer(http.NotFoundHandler()) defer s.Close()