summaryrefslogtreecommitdiffstats
path: root/pkg/v1/remote/write_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/v1/remote/write_test.go')
-rw-r--r--pkg/v1/remote/write_test.go1643
1 files changed, 1643 insertions, 0 deletions
diff --git a/pkg/v1/remote/write_test.go b/pkg/v1/remote/write_test.go
new file mode 100644
index 0000000..7235c96
--- /dev/null
+++ b/pkg/v1/remote/write_test.go
@@ -0,0 +1,1643 @@
+// Copyright 2018 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 remote
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "strings"
+ "sync/atomic"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func mustNewTag(t *testing.T, s string) name.Tag {
+ tag, err := name.NewTag(s, name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag(%v) = %v", s, err)
+ }
+ return tag
+}
+
+func TestUrl(t *testing.T) {
+ tests := []struct {
+ tag string
+ path string
+ url string
+ }{{
+ tag: "gcr.io/foo/bar:latest",
+ path: "/v2/foo/bar/manifests/latest",
+ url: "https://gcr.io/v2/foo/bar/manifests/latest",
+ }, {
+ tag: "localhost:8080/foo/bar:baz",
+ path: "/v2/foo/bar/blobs/upload",
+ url: "http://localhost:8080/v2/foo/bar/blobs/upload",
+ }}
+
+ for _, test := range tests {
+ w := &writer{
+ repo: mustNewTag(t, test.tag).Context(),
+ }
+ if got, want := w.url(test.path), test.url; got.String() != want {
+ t.Errorf("url(%v) = %v, want %v", test.path, got.String(), want)
+ }
+ }
+}
+
+func TestNextLocation(t *testing.T) {
+ tests := []struct {
+ location string
+ url string
+ }{{
+ location: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ url: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ }, {
+ location: "/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ url: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ }}
+
+ ref := mustNewTag(t, "gcr.io/foo/bar:latest")
+ w := &writer{
+ repo: ref.Context(),
+ }
+
+ for _, test := range tests {
+ resp := &http.Response{
+ Header: map[string][]string{
+ "Location": {test.location},
+ },
+ Request: &http.Request{
+ URL: &url.URL{
+ Scheme: ref.Registry.Scheme(),
+ Host: ref.RegistryStr(),
+ },
+ },
+ }
+
+ got, err := w.nextLocation(resp)
+ if err != nil {
+ t.Errorf("nextLocation(%v) = %v", resp, err)
+ }
+ want := test.url
+ if got != want {
+ t.Errorf("nextLocation(%v) = %v, want %v", resp, got, want)
+ }
+ }
+}
+
+type closer interface {
+ Close()
+}
+
+func setupImage(t *testing.T) v1.Image {
+ rnd, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ return rnd
+}
+
+func setupIndex(t *testing.T, children int64) v1.ImageIndex {
+ rnd, err := random.Index(1024, 1, children)
+ if err != nil {
+ t.Fatalf("random.Index() = %v", err)
+ }
+ return rnd
+}
+
+func mustConfigName(t *testing.T, img v1.Image) v1.Hash {
+ h, err := img.ConfigName()
+ if err != nil {
+ t.Fatalf("ConfigName() = %v", err)
+ }
+ return h
+}
+
+func setupWriter(repo string, handler http.HandlerFunc) (*writer, closer, error) {
+ server := httptest.NewServer(handler)
+ return setupWriterWithServer(server, repo)
+}
+
+func setupWriterWithServer(server *httptest.Server, repo string) (*writer, closer, error) {
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ server.Close()
+ return nil, nil, err
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, repo), name.WeakValidation)
+ if err != nil {
+ server.Close()
+ return nil, nil, err
+ }
+
+ return &writer{
+ repo: tag.Context(),
+ client: http.DefaultClient,
+ predicate: defaultRetryPredicate,
+ backoff: defaultRetryBackoff,
+ }, server, nil
+}
+
+func TestCheckExistingBlob(t *testing.T) {
+ tests := []struct {
+ name string
+ status int
+ existing bool
+ wantErr bool
+ }{{
+ name: "success",
+ status: http.StatusOK,
+ existing: true,
+ }, {
+ name: "not found",
+ status: http.StatusNotFound,
+ existing: false,
+ }, {
+ name: "error",
+ status: http.StatusInternalServerError,
+ existing: false,
+ wantErr: true,
+ }}
+
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, h.String())
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ http.Error(w, http.StatusText(test.status), test.status)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ existing, err := w.checkExistingBlob(context.Background(), h)
+ if test.existing != existing {
+ t.Errorf("checkExistingBlob() = %v, want %v", existing, test.existing)
+ }
+ if err != nil && !test.wantErr {
+ t.Errorf("checkExistingBlob() = %v", err)
+ } else if err == nil && test.wantErr {
+ t.Error("checkExistingBlob() wanted err, got nil")
+ }
+ })
+ }
+}
+
+func TestInitiateUploadNoMountsExists(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadNoMountsInitiated(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "baz/blah"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+ expectedLocation := "https://somewhere.io/upload?foo=bar"
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ w.Header().Set("Location", expectedLocation)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ location, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if mounted {
+ t.Error("initiateUpload() = mounted, want !mounted")
+ }
+ if location != expectedLocation {
+ t.Errorf("initiateUpload(); got %v, want %v", location, expectedLocation)
+ }
+}
+
+func TestInitiateUploadNoMountsBadStatus(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "ugh/another"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Unknown", http.StatusNoContent)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ location, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err == nil {
+ t.Errorf("intiateUpload() = %v, %v; wanted error", location, mounted)
+ }
+}
+
+func TestInitiateUploadMountsWithMountFromDifferentRegistry(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadMountsWithMountFromTheSameRegistry(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedMountRepo := "a/different/repo"
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{expectedMountRepo},
+ }.Encode()
+
+ serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ })
+ server := httptest.NewServer(serverHandler)
+
+ w, closer, err := setupWriterWithServer(server, expectedRepo)
+ if err != nil {
+ t.Fatalf("setupWriterWithServer() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadMountsWithOrigin(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedMountRepo := "a/different/repo"
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedOrigin := "fakeOrigin"
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{expectedMountRepo},
+ "origin": []string{expectedOrigin},
+ }.Encode()
+
+ serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ })
+ server := httptest.NewServer(serverHandler)
+
+ w, closer, err := setupWriterWithServer(server, expectedRepo)
+ if err != nil {
+ t.Fatalf("setupWriterWithServer() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "fakeOrigin")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadMountsWithOriginFallback(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedMountRepo := "a/different/repo"
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedOrigin := "fakeOrigin"
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{expectedMountRepo},
+ "origin": []string{expectedOrigin},
+ }.Encode()
+
+ queries := []string{expectedQuery, ""}
+ queryCount := 0
+
+ serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != queries[queryCount] {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ if queryCount == 0 {
+ http.Error(w, "nope", http.StatusUnauthorized)
+ } else {
+ http.Error(w, "Mounted", http.StatusCreated)
+ }
+ queryCount++
+ })
+ server := httptest.NewServer(serverHandler)
+
+ w, closer, err := setupWriterWithServer(server, expectedRepo)
+ if err != nil {
+ t.Fatalf("setupWriterWithServer() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "fakeOrigin")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestDedupeLayers(t *testing.T) {
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, 10000))) }
+
+ img, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+
+ // Append three identical tarball.Layers, which should be deduped
+ // because contents can be hashed before uploading.
+ for i := 0; i < 3; i++ {
+ tl, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return newBlob(), nil })
+ if err != nil {
+ t.Fatalf("LayerFromOpener(#%d): %v", i, err)
+ }
+ img, err = mutate.AppendLayers(img, tl)
+ if err != nil {
+ t.Fatalf("mutate.AppendLayer(#%d): %v", i, err)
+ }
+ }
+
+ // Append three identical stream.Layers, whose uploads will *not* be
+ // deduped since Write can't tell they're identical ahead of time.
+ for i := 0; i < 3; i++ {
+ sl := stream.NewLayer(newBlob())
+ img, err = mutate.AppendLayers(img, sl)
+ if err != nil {
+ t.Fatalf("mutate.AppendLayer(#%d): %v", i, err)
+ }
+ }
+
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ uploadPath := "/upload"
+ commitPath := "/commit"
+ var numUploads int32
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", uploadPath)
+ http.Error(w, "Accepted", http.StatusAccepted)
+ case uploadPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ atomic.AddInt32(&numUploads, 1)
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Created", http.StatusCreated)
+ case commitPath:
+ http.Error(w, "Created", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Write(tag, img); err != nil {
+ t.Errorf("Write: %v", err)
+ }
+
+ // 3 random layers, 1 tarball layer (deduped), 3 stream layers (not deduped), 1 image config blob
+ wantUploads := int32(3 + 1 + 3 + 1)
+ if numUploads != wantUploads {
+ t.Fatalf("Write uploaded %d blobs, want %d", numUploads, wantUploads)
+ }
+}
+
+func TestStreamBlob(t *testing.T) {
+ img := setupImage(t)
+ expectedPath := "/vWhatever/I/decide"
+ expectedCommitLocation := "https://commit.io/v12/blob"
+
+ w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ got, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("ReadAll(Body) = %v", err)
+ }
+ want, err := img.RawConfigFile()
+ if err != nil {
+ t.Errorf("RawConfigFile() = %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("bytes.Equal(); got %v, want %v", got, want)
+ }
+ w.Header().Set("Location", expectedCommitLocation)
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ streamLocation := w.url(expectedPath)
+
+ l, err := partial.ConfigLayer(img)
+ if err != nil {
+ t.Fatalf("ConfigLayer: %v", err)
+ }
+
+ commitLocation, err := w.streamBlob(context.Background(), l, streamLocation.String())
+ if err != nil {
+ t.Errorf("streamBlob() = %v", err)
+ }
+ if commitLocation != expectedCommitLocation {
+ t.Errorf("streamBlob(); got %v, want %v", commitLocation, expectedCommitLocation)
+ }
+}
+
+func TestStreamLayer(t *testing.T) {
+ var n, wantSize int64 = 10000, 49
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) }
+ wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e"
+
+ expectedPath := "/vWhatever/I/decide"
+ expectedCommitLocation := "https://commit.io/v12/blob"
+ w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+
+ h := crypto.SHA256.New()
+ s, err := io.Copy(h, r.Body)
+ if err != nil {
+ t.Errorf("Reading body: %v", err)
+ }
+ if s != wantSize {
+ t.Errorf("Received %d bytes, want %d", s, wantSize)
+ }
+ gotDigest := "sha256:" + hex.EncodeToString(h.Sum(nil))
+ if gotDigest != wantDigest {
+ t.Errorf("Received bytes with digest %q, want %q", gotDigest, wantDigest)
+ }
+
+ w.Header().Set("Location", expectedCommitLocation)
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ streamLocation := w.url(expectedPath)
+ sl := stream.NewLayer(newBlob())
+
+ commitLocation, err := w.streamBlob(context.Background(), sl, streamLocation.String())
+ if err != nil {
+ t.Errorf("streamBlob: %v", err)
+ }
+ if commitLocation != expectedCommitLocation {
+ t.Errorf("streamBlob(); got %v, want %v", commitLocation, expectedCommitLocation)
+ }
+}
+
+func TestCommitBlob(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedPath := "/no/commitment/issues"
+ expectedQuery := url.Values{
+ "digest": []string{h.String()},
+ }.Encode()
+
+ w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ commitLocation := w.url(expectedPath)
+
+ if err := w.commitBlob(context.Background(), commitLocation.String(), h.String()); err != nil {
+ t.Errorf("commitBlob() = %v", err)
+ }
+}
+
+func TestUploadOne(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "baz/blah"
+ headPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, h.String())
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ streamPath := "/path/to/upload"
+ commitPath := "/path/to/commit"
+ ctx := context.Background()
+
+ uploaded := false
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case headPath:
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if uploaded {
+ return
+ }
+ http.Error(w, "NotFound", http.StatusNotFound)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", streamPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case streamPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ got, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("ReadAll(Body) = %v", err)
+ }
+ want, err := img.RawConfigFile()
+ if err != nil {
+ t.Errorf("RawConfigFile() = %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("bytes.Equal(); got %v, want %v", got, want)
+ }
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case commitPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ uploaded = true
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ l, err := partial.ConfigLayer(img)
+ if err != nil {
+ t.Fatalf("ConfigLayer: %v", err)
+ }
+ ml := &MountableLayer{
+ Layer: l,
+ Reference: w.repo.Digest(h.String()),
+ }
+ if err := w.uploadOne(ctx, ml); err != nil {
+ t.Errorf("uploadOne() = %v", err)
+ }
+ // Hit the existing blob path.
+ if err := w.uploadOne(ctx, l); err != nil {
+ t.Errorf("uploadOne() = %v", err)
+ }
+}
+
+func TestUploadOneStreamedLayer(t *testing.T) {
+ expectedRepo := "baz/blah"
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ streamPath := "/path/to/upload"
+ commitPath := "/path/to/commit"
+ ctx := context.Background()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", streamPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case streamPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ // TODO(jasonhall): What should we check here?
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case commitPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ var n, wantSize int64 = 10000, 49
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) }
+ wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e"
+ wantDiffID := "sha256:27dd1f61b867b6a0f6e9d8a41c43231de52107e53ae424de8f847b821db4b711"
+ l := stream.NewLayer(newBlob())
+ if err := w.uploadOne(ctx, l); err != nil {
+ t.Fatalf("uploadOne: %v", err)
+ }
+
+ if dig, err := l.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if dig.String() != wantDigest {
+ t.Errorf("Digest got %q, want %q", dig, wantDigest)
+ }
+ if diffID, err := l.DiffID(); err != nil {
+ t.Errorf("DiffID: %v", err)
+ } else if diffID.String() != wantDiffID {
+ t.Errorf("DiffID got %q, want %q", diffID, wantDiffID)
+ }
+ if size, err := l.Size(); err != nil {
+ t.Errorf("Size: %v", err)
+ } else if size != wantSize {
+ t.Errorf("Size got %d, want %d", size, wantSize)
+ }
+}
+
+func TestCommitImage(t *testing.T) {
+ img := setupImage(t)
+ ctx := context.Background()
+
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ got, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("ReadAll(Body) = %v", err)
+ }
+ want, err := img.RawManifest()
+ if err != nil {
+ t.Errorf("RawManifest() = %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("bytes.Equal(); got %v, want %v", got, want)
+ }
+ mt, err := img.MediaType()
+ if err != nil {
+ t.Errorf("MediaType() = %v", err)
+ }
+ if got, want := r.Header.Get("Content-Type"), string(mt); got != want {
+ t.Errorf("Header; got %v, want %v", got, want)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ if err := w.commitManifest(ctx, img, w.repo.Tag("latest")); err != nil {
+ t.Error("commitManifest() = ", err)
+ }
+}
+
+func TestWrite(t *testing.T) {
+ img := setupImage(t)
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Write(tag, img); err != nil {
+ t.Errorf("Write() = %v", err)
+ }
+}
+
+func TestWriteWithErrors(t *testing.T) {
+ img := setupImage(t)
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+
+ errorBody := `{"errors":[{"code":"NAME_INVALID","message":"some explanation of how things were messed up."}],"StatusCode":400}`
+ expectedErrMsg, err := regexp.Compile(`POST .+ NAME_INVALID: some explanation of how things were messed up.`)
+ if err != nil {
+ t.Error(err)
+ }
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(errorBody))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ c := make(chan v1.Update, 100)
+
+ var terr *transport.Error
+ if err := Write(tag, img, WithProgress(c)); err == nil {
+ t.Error("Write() = nil; wanted error")
+ } else if !errors.As(err, &terr) {
+ t.Errorf("Write() = %T; wanted *transport.Error", err)
+ } else if !expectedErrMsg.Match([]byte(terr.Error())) {
+ diff := cmp.Diff(expectedErrMsg, terr.Error())
+ t.Errorf("Write(); (-want +got) = %s", diff)
+ }
+
+ var last v1.Update
+ for update := range c {
+ last = update
+ }
+ if last.Error == nil {
+ t.Error("Progress chan didn't report error")
+ }
+}
+
+func TestDockerhubScopes(t *testing.T) {
+ src, err := name.ParseReference("busybox")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rl, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ml := &MountableLayer{
+ Layer: rl,
+ Reference: src,
+ }
+ want := src.Scope(transport.PullScope)
+
+ for _, s := range []string{
+ "jonjohnson/busybox",
+ "docker.io/jonjohnson/busybox",
+ "index.docker.io/jonjohnson/busybox",
+ } {
+ dst, err := name.ParseReference(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ scopes := scopesForUploadingImage(dst.Context(), []v1.Layer{ml})
+
+ if len(scopes) != 2 {
+ t.Errorf("Should have two scopes (src and dst), got %d", len(scopes))
+ } else if diff := cmp.Diff(want, scopes[1]); diff != "" {
+ t.Errorf("TestDockerhubScopes %q: (-want +got) = %v", s, diff)
+ }
+ }
+}
+
+func TestScopesForUploadingImage(t *testing.T) {
+ referenceToUpload, err := name.NewTag("example.com/sample/sample:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ sameReference, err := name.NewTag("example.com/sample/sample:previous", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ anotherRepo1, err := name.NewTag("example.com/sample/another_repo1:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ anotherRepo2, err := name.NewTag("example.com/sample/another_repo2:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ repoOnOtherRegistry, err := name.NewTag("other-domain.com/sample/any_repo:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ img := setupImage(t)
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("img.Layers() = %v", err)
+ }
+ dummyLayer := layers[0]
+
+ testCases := []struct {
+ name string
+ reference name.Reference
+ layers []v1.Layer
+ expected []string
+ }{
+ {
+ name: "empty layers",
+ reference: referenceToUpload,
+ layers: []v1.Layer{},
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ },
+ },
+ {
+ name: "mountable layers with same reference",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: sameReference,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ },
+ },
+ {
+ name: "mountable layers with single reference with no-duplicate",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "mountable layers with single reference with duplicate",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "mountable layers with multiple references with no-duplicates",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo2,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ anotherRepo2.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "mountable layers with multiple references with duplicates",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo2,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo2,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ anotherRepo2.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "cross repository mountable layer",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: repoOnOtherRegistry,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ actual := scopesForUploadingImage(tc.reference.Context(), tc.layers)
+
+ if want, got := tc.expected[0], actual[0]; want != got {
+ t.Errorf("TestScopesForUploadingImage() %s: Wrong first scope; want %v, got %v", tc.name, want, got)
+ }
+
+ less := func(a, b string) bool {
+ return strings.Compare(a, b) <= -1
+ }
+ if diff := cmp.Diff(tc.expected[1:], actual[1:], cmpopts.SortSlices(less)); diff != "" {
+ t.Errorf("TestScopesForUploadingImage() %s: Wrong scopes (-want +got) = %v", tc.name, diff)
+ }
+ }
+}
+
+func TestCheckExistingManifest(t *testing.T) {
+ tests := []struct {
+ name string
+ status int
+ existing bool
+ wantErr bool
+ }{{
+ name: "success",
+ status: http.StatusOK,
+ existing: true,
+ }, {
+ name: "not found",
+ status: http.StatusNotFound,
+ existing: false,
+ }, {
+ name: "error",
+ status: http.StatusInternalServerError,
+ existing: false,
+ wantErr: true,
+ }}
+
+ img := setupImage(t)
+ h := mustDigest(t, img)
+ mt := mustMediaType(t, img)
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, h.String())
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if got, want := r.Header.Get("Accept"), string(mt); got != want {
+ t.Errorf("r.Header['Accept']; got %v, want %v", got, want)
+ }
+ http.Error(w, http.StatusText(test.status), test.status)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ existing, err := w.checkExistingManifest(context.Background(), h, mt)
+ if test.existing != existing {
+ t.Errorf("checkExistingManifest() = %v, want %v", existing, test.existing)
+ }
+ if err != nil && !test.wantErr {
+ t.Errorf("checkExistingManifest() = %v", err)
+ } else if err == nil && test.wantErr {
+ t.Error("checkExistingManifest() wanted err, got nil")
+ }
+ })
+ }
+}
+
+func TestWriteIndex(t *testing.T) {
+ idx := setupIndex(t, 2)
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ childDigest := mustIndexManifest(t, idx).Manifests[0].Digest
+ childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest)
+ existinChildDigest := mustIndexManifest(t, idx).Manifests[1].Digest
+ existingChildPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, existinChildDigest)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ case existingChildPath:
+ if r.Method == http.MethodHead {
+ http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
+ return
+ }
+ t.Errorf("Unexpected method; got %v, want %v", r.Method, http.MethodHead)
+ case childPath:
+ if r.Method == http.MethodHead {
+ http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+ return
+ }
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := WriteIndex(tag, idx); err != nil {
+ t.Errorf("WriteIndex() = %v", err)
+ }
+}
+
+// If we actually attempt to read the contents, this will fail the test.
+type fakeForeignLayer struct {
+ t *testing.T
+}
+
+func (l *fakeForeignLayer) MediaType() (types.MediaType, error) {
+ return types.DockerForeignLayer, nil
+}
+
+func (l *fakeForeignLayer) Size() (int64, error) {
+ return 0, nil
+}
+
+func (l *fakeForeignLayer) Digest() (v1.Hash, error) {
+ return v1.Hash{Algorithm: "sha256", Hex: strings.Repeat("a", 64)}, nil
+}
+
+func (l *fakeForeignLayer) DiffID() (v1.Hash, error) {
+ return v1.Hash{Algorithm: "sha256", Hex: strings.Repeat("a", 64)}, nil
+}
+
+func (l *fakeForeignLayer) Compressed() (io.ReadCloser, error) {
+ l.t.Helper()
+ l.t.Errorf("foreign layer not skipped: Compressed")
+ return nil, nil
+}
+
+func (l *fakeForeignLayer) Uncompressed() (io.ReadCloser, error) {
+ l.t.Helper()
+ l.t.Errorf("foreign layer not skipped: Uncompressed")
+ return nil, nil
+}
+
+func TestSkipForeignLayersByDefault(t *testing.T) {
+ // Set up an image with a foreign layer.
+ base := setupImage(t)
+ img, err := mutate.AppendLayers(base, &fakeForeignLayer{t: t})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/foreign/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, img); err != nil {
+ t.Errorf("failed to Write: %v", err)
+ }
+}
+
+func TestWriteForeignLayerIfOptionSet(t *testing.T) {
+ // Set up an image with a foreign layer.
+ base := setupImage(t)
+ foreignLayer, err := random.Layer(1024, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatal("random.Layer:", err)
+ }
+ img, err := mutate.AppendLayers(base, foreignLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ uploadPath := "/upload"
+ commitPath := "/commit"
+ var numUploads int32
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", uploadPath)
+ http.Error(w, "Accepted", http.StatusAccepted)
+ case uploadPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ atomic.AddInt32(&numUploads, 1)
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Created", http.StatusCreated)
+ case commitPath:
+ http.Error(w, "Created", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Write(tag, img, WithNondistributable); err != nil {
+ t.Errorf("Write: %v", err)
+ }
+
+ // 1 random layer, 1 foreign layer, 1 image config blob
+ wantUploads := int32(1 + 1 + 1)
+ if numUploads != wantUploads {
+ t.Fatalf("Write uploaded %d blobs, want %d", numUploads, wantUploads)
+ }
+}
+
+func TestTag(t *testing.T) {
+ idx := setupIndex(t, 3)
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("%s/test/tag:src", u.Host)
+ srcRef, err := name.NewTag(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteIndex(srcRef, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ dst := fmt.Sprintf("%s/test/tag:dst", u.Host)
+ dstRef, err := name.NewTag(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Tag(dstRef, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := Index(dstRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Index(got); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+}
+
+func TestTagDescriptor(t *testing.T) {
+ idx := setupIndex(t, 3)
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("%s/test/tag:src", u.Host)
+ srcRef, err := name.NewTag(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteIndex(srcRef, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ desc, err := Get(srcRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dst := fmt.Sprintf("%s/test/tag:dst", u.Host)
+ dstRef, err := name.NewTag(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Tag(dstRef, desc); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestNestedIndex(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("%s/test/tag:src", u.Host)
+ srcRef, err := name.NewTag(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ child, err := random.Index(1024, 1, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ parent := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
+ Add: child,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"example.com/url"},
+ },
+ })
+
+ l, err := random.Layer(100, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ parent = mutate.AppendManifests(parent, mutate.IndexAddendum{
+ Add: l,
+ })
+
+ if err := WriteIndex(srcRef, parent); err != nil {
+ t.Fatal(err)
+ }
+ pulled, err := Index(srcRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Index(pulled); err != nil {
+ t.Fatalf("validate.Index: %v", err)
+ }
+
+ digest, err := child.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pulledChild, err := pulled.ImageIndex(digest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ desc, err := partial.Descriptor(pulledChild)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(desc.URLs) != 1 {
+ t.Fatalf("expected url for pulledChild")
+ }
+
+ if want, got := "example.com/url", desc.URLs[0]; want != got {
+ t.Errorf("pulledChild.urls[0] = %s != %s", got, want)
+ }
+}
+
+func BenchmarkWrite(b *testing.B) {
+ // unfortunately the registry _and_ the img have caching behaviour, so we need a new registry
+ // and image every iteration of benchmarking.
+ for i := 0; i < b.N; i++ {
+ // set up the registry
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+
+ // load the image
+ img, err := random.Image(50*1024*1024, 10)
+ if err != nil {
+ b.Fatalf("random.Image(...): %v", err)
+ }
+
+ b.ResetTimer()
+
+ tagStr := strings.TrimPrefix(s.URL+"/test/image:tag", "http://")
+ tag, err := name.NewTag(tagStr)
+ if err != nil {
+ b.Fatalf("parsing tag (%s): %v", tagStr, err)
+ }
+
+ err = Write(tag, img)
+ if err != nil {
+ b.Fatalf("pushing tag one: %v", err)
+ }
+ }
+}