diff options
Diffstat (limited to 'pkg/v1/remote/write_test.go')
-rw-r--r-- | pkg/v1/remote/write_test.go | 1643 |
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) + } + } +} |