Skip to content

Commit

Permalink
crane digest: fallback to GET when HEAD fails (#971)
Browse files Browse the repository at this point in the history
* crane digest: fallback to GET when HEAD fails

* Improve error message for unparseable hash string
  • Loading branch information
imjasonh committed Mar 22, 2021
1 parent 3678a26 commit a11b12f
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 5 deletions.
9 changes: 8 additions & 1 deletion pkg/crane/digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package crane

import "github.com/google/go-containerregistry/pkg/logs"

// Digest returns the sha256 hash of the remote image at ref.
func Digest(ref string, opt ...Option) (string, error) {
o := makeOptions(opt...)
Expand All @@ -39,7 +41,12 @@ func Digest(ref string, opt ...Option) (string, error) {
}
desc, err := head(ref, opt...)
if err != nil {
return "", err
logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err)
rdesc, err := getManifest(ref, opt...)
if err != nil {
return "", err
}
return rdesc.Digest.String(), nil
}
return desc.Digest.String(), nil
}
61 changes: 61 additions & 0 deletions pkg/crane/digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package crane

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/google/go-containerregistry/pkg/v1/types"
)

func TestDigest_MissingDigest(t *testing.T) {
response := []byte("doesn't matter")
digest := "sha256:477c34d98f9e090a4441cf82d2f1f03e64c8eb730e8c1ef39a8595e685d4df65" // Digest of "doesn't matter"
getCalled := false

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/" {
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Content-Type", string(types.DockerManifestSchema2))
if r.Method == http.MethodGet {
getCalled = true
w.Header().Set("Docker-Content-Digest", digest)
}
// This will automatically set the Content-Length header.
w.Write(response)
}))
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("url.Parse(%v) = %v", server.URL, err)
}

got, err := Digest(fmt.Sprintf("%s/repo:latest", u.Host))
if err != nil {
t.Fatalf("Digest: %v", err)
}
if got != digest {
t.Errorf("Digest: got %q, want %q", got, digest)
}
if !getCalled {
t.Errorf("Digest: expected GET to be called")
}
}
2 changes: 1 addition & 1 deletion pkg/v1/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func Hasher(name string) (hash.Hash, error) {
func (h *Hash) parse(unquoted string) error {
parts := strings.Split(unquoted, ":")
if len(parts) != 2 {
return fmt.Errorf("too many parts in hash: %s", unquoted)
return fmt.Errorf("cannot parse hash: %q", unquoted)
}

rest := strings.TrimLeft(parts[1], "0123456789abcdef")
Expand Down
18 changes: 15 additions & 3 deletions pkg/v1/remote/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,14 +324,26 @@ func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType)
return nil, err
}

mediaType := types.MediaType(resp.Header.Get("Content-Type"))
mth := resp.Header.Get("Content-Type")
if mth == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
}
mediaType := types.MediaType(mth)

size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
lh := resp.Header.Get("Content-Length")
if lh == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Content-Length header", u.String())
}
size, err := strconv.ParseInt(lh, 10, 64)
if err != nil {
return nil, err
}

digest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
dh := resp.Header.Get("Docker-Content-Digest")
if dh == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
}
digest, err := v1.NewHash(dh)
if err != nil {
return nil, err
}
Expand Down
40 changes: 40 additions & 0 deletions pkg/v1/remote/descriptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -174,3 +175,42 @@ func TestHeadSchema1(t *testing.T) {
t.Errorf("Descriptor.Size = %q, expected %q", desc.Size, len(response))
}
}

// TestHead_MissingHeaders tests that HEAD responses missing necessary headers
// result in errors.
func TestHead_MissingHeaders(t *testing.T) {
missingType := "missing-type"
missingLength := "missing-length"
missingDigest := "missing-digest"

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/" {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != http.MethodHead {
t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
}
if !strings.Contains(r.URL.Path, missingType) {
w.Header().Set("Content-Type", "My-Media-Type")
}
if !strings.Contains(r.URL.Path, missingLength) {
w.Header().Set("Content-Length", "10")
}
if !strings.Contains(r.URL.Path, missingDigest) {
w.Header().Set("Docker-Content-Digest", "sha256:0000000000000000000000000000000000000000000000000000000000000000")
}
}))
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("url.Parse(%v) = %v", server.URL, err)
}

for _, repo := range []string{missingType, missingLength, missingDigest} {
tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, repo))
if _, err := Head(tag); err == nil {
t.Errorf("Head(%q): expected error, got nil", tag)
}
}
}

0 comments on commit a11b12f

Please sign in to comment.