...

Source file src/github.com/google/go-containerregistry/pkg/gcrane/copy_test.go

Documentation: github.com/google/go-containerregistry/pkg/gcrane

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gcrane
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"net/url"
    26  	"os"
    27  	"path"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/google/go-cmp/cmp"
    33  	"github.com/google/go-containerregistry/internal/retry"
    34  	"github.com/google/go-containerregistry/pkg/logs"
    35  	"github.com/google/go-containerregistry/pkg/name"
    36  	"github.com/google/go-containerregistry/pkg/registry"
    37  	"github.com/google/go-containerregistry/pkg/v1/google"
    38  	"github.com/google/go-containerregistry/pkg/v1/partial"
    39  	"github.com/google/go-containerregistry/pkg/v1/random"
    40  	"github.com/google/go-containerregistry/pkg/v1/remote"
    41  	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
    42  	"github.com/google/go-containerregistry/pkg/v1/types"
    43  )
    44  
    45  type fakeXCR struct {
    46  	h     http.Handler
    47  	repos map[string]google.Tags
    48  	t     *testing.T
    49  }
    50  
    51  func (xcr *fakeXCR) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    52  	xcr.t.Logf("%s %s", r.Method, r.URL)
    53  	if strings.HasPrefix(r.URL.Path, "/v2/") && strings.HasSuffix(r.URL.Path, "/tags/list") {
    54  		repo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v2/"), "/tags/list")
    55  		if tags, ok := xcr.repos[repo]; !ok {
    56  			w.WriteHeader(http.StatusNotFound)
    57  		} else {
    58  			xcr.t.Logf("%+v", tags)
    59  			if err := json.NewEncoder(w).Encode(tags); err != nil {
    60  				xcr.t.Fatal(err)
    61  			}
    62  		}
    63  	} else {
    64  		xcr.h.ServeHTTP(w, r)
    65  	}
    66  }
    67  
    68  func newFakeXCR(t *testing.T) *fakeXCR {
    69  	h := registry.New()
    70  	return &fakeXCR{h: h, t: t}
    71  }
    72  
    73  func (xcr *fakeXCR) setRefs(stuff map[name.Reference]partial.Describable) error {
    74  	repos := make(map[string]google.Tags)
    75  
    76  	for ref, thing := range stuff {
    77  		repo := ref.Context().RepositoryStr()
    78  		tags, ok := repos[repo]
    79  		if !ok {
    80  			tags = google.Tags{
    81  				Name:     repo,
    82  				Children: []string{},
    83  			}
    84  		}
    85  
    86  		// Populate the "child" field.
    87  		for parentPath := repo; parentPath != "."; parentPath = path.Dir(parentPath) {
    88  			child, parent := path.Base(parentPath), path.Dir(parentPath)
    89  			tags, ok := repos[parent]
    90  			if !ok {
    91  				tags = google.Tags{}
    92  			}
    93  			for _, c := range repos[parent].Children {
    94  				if c == child {
    95  					break
    96  				}
    97  			}
    98  			tags.Children = append(tags.Children, child)
    99  			repos[parent] = tags
   100  		}
   101  
   102  		// Populate the "manifests" and "tags" field.
   103  		d, err := thing.Digest()
   104  		if err != nil {
   105  			return err
   106  		}
   107  		mt, err := thing.MediaType()
   108  		if err != nil {
   109  			return err
   110  		}
   111  		if tags.Manifests == nil {
   112  			tags.Manifests = make(map[string]google.ManifestInfo)
   113  		}
   114  		mi, ok := tags.Manifests[d.String()]
   115  		if !ok {
   116  			mi = google.ManifestInfo{
   117  				MediaType: string(mt),
   118  				Tags:      []string{},
   119  			}
   120  		}
   121  		if tag, ok := ref.(name.Tag); ok {
   122  			tags.Tags = append(tags.Tags, tag.Identifier())
   123  			mi.Tags = append(mi.Tags, tag.Identifier())
   124  		}
   125  		tags.Manifests[d.String()] = mi
   126  		repos[repo] = tags
   127  	}
   128  	xcr.repos = repos
   129  	return nil
   130  }
   131  
   132  func TestCopy(t *testing.T) {
   133  	logs.Warn.SetOutput(os.Stderr)
   134  	xcr := newFakeXCR(t)
   135  	s := httptest.NewServer(xcr)
   136  	u, err := url.Parse(s.URL)
   137  	if err != nil {
   138  		t.Fatal(err)
   139  	}
   140  	defer s.Close()
   141  	src := path.Join(u.Host, "test/gcrane")
   142  	dst := path.Join(u.Host, "test/gcrane/copy")
   143  
   144  	oneTag, err := random.Image(1024, 5)
   145  	if err != nil {
   146  		t.Fatal(err)
   147  	}
   148  	twoTags, err := random.Image(1024, 5)
   149  	if err != nil {
   150  		t.Fatal(err)
   151  	}
   152  	noTags, err := random.Image(1024, 3)
   153  	if err != nil {
   154  		t.Fatal(err)
   155  	}
   156  
   157  	latestRef, err := name.ParseReference(src)
   158  	if err != nil {
   159  		t.Fatal(err)
   160  	}
   161  	oneTagRef := latestRef.Context().Tag("bar")
   162  
   163  	d, err := noTags.Digest()
   164  	if err != nil {
   165  		t.Fatal(err)
   166  	}
   167  	noTagsRef := latestRef.Context().Digest(d.String())
   168  	fooRef := latestRef.Context().Tag("foo")
   169  
   170  	// Populate this after we create it so we know the hostname.
   171  	if err := xcr.setRefs(map[name.Reference]partial.Describable{
   172  		oneTagRef: oneTag,
   173  		latestRef: twoTags,
   174  		fooRef:    twoTags,
   175  		noTagsRef: noTags,
   176  	}); err != nil {
   177  		t.Fatal(err)
   178  	}
   179  
   180  	if err := remote.Write(latestRef, twoTags); err != nil {
   181  		t.Fatal(err)
   182  	}
   183  	if err := remote.Write(fooRef, twoTags); err != nil {
   184  		t.Fatal(err)
   185  	}
   186  	if err := remote.Write(oneTagRef, oneTag); err != nil {
   187  		t.Fatal(err)
   188  	}
   189  	if err := remote.Write(noTagsRef, noTags); err != nil {
   190  		t.Fatal(err)
   191  	}
   192  
   193  	if err := Copy(src, dst); err != nil {
   194  		t.Fatal(err)
   195  	}
   196  
   197  	if err := CopyRepository(context.Background(), src, dst); err != nil {
   198  		t.Fatal(err)
   199  	}
   200  }
   201  
   202  func TestRename(t *testing.T) {
   203  	c := copier{
   204  		srcRepo: name.MustParseReference("registry.example.com/foo").Context(),
   205  		dstRepo: name.MustParseReference("registry.example.com/bar").Context(),
   206  	}
   207  
   208  	got, err := c.rename(name.MustParseReference("registry.example.com/foo/sub/repo").Context())
   209  	if err != nil {
   210  		t.Fatalf("unexpected err: %v", err)
   211  	}
   212  	want := name.MustParseReference("registry.example.com/bar/sub/repo").Context()
   213  
   214  	if want.String() != got.String() {
   215  		t.Errorf("%s != %s", want, got)
   216  	}
   217  }
   218  
   219  func TestSubtractStringLists(t *testing.T) {
   220  	cases := []struct {
   221  		minuend    []string
   222  		subtrahend []string
   223  		result     []string
   224  	}{{
   225  		minuend:    []string{"a", "b", "c"},
   226  		subtrahend: []string{"a"},
   227  		result:     []string{"b", "c"},
   228  	}, {
   229  		minuend:    []string{"a", "a", "a"},
   230  		subtrahend: []string{"a", "b"},
   231  		result:     []string{},
   232  	}, {
   233  		minuend:    []string{},
   234  		subtrahend: []string{"a", "b"},
   235  		result:     []string{},
   236  	}, {
   237  		minuend:    []string{"a", "b"},
   238  		subtrahend: []string{},
   239  		result:     []string{"a", "b"},
   240  	}}
   241  
   242  	for _, tc := range cases {
   243  		want, got := tc.result, subtractStringLists(tc.minuend, tc.subtrahend)
   244  		if diff := cmp.Diff(want, got); diff != "" {
   245  			t.Errorf("subtracting string lists: %v - %v: (-want +got)\n%s", tc.minuend, tc.subtrahend, diff)
   246  		}
   247  	}
   248  }
   249  
   250  func TestDiffImages(t *testing.T) {
   251  	cases := []struct {
   252  		want map[string]google.ManifestInfo
   253  		have map[string]google.ManifestInfo
   254  		need map[string]google.ManifestInfo
   255  	}{{
   256  		// Have everything we need.
   257  		want: map[string]google.ManifestInfo{
   258  			"a": {
   259  				Tags: []string{"b", "c"},
   260  			},
   261  		},
   262  		have: map[string]google.ManifestInfo{
   263  			"a": {
   264  				Tags: []string{"b", "c"},
   265  			},
   266  		},
   267  		need: map[string]google.ManifestInfo{},
   268  	}, {
   269  		// Missing image a.
   270  		want: map[string]google.ManifestInfo{
   271  			"a": {
   272  				Tags: []string{"b", "c", "d"},
   273  			},
   274  		},
   275  		have: map[string]google.ManifestInfo{},
   276  		need: map[string]google.ManifestInfo{
   277  			"a": {
   278  				Tags: []string{"b", "c", "d"},
   279  			},
   280  		},
   281  	}, {
   282  		// Missing tags "b" and "d"
   283  		want: map[string]google.ManifestInfo{
   284  			"a": {
   285  				Tags: []string{"b", "c", "d"},
   286  			},
   287  		},
   288  		have: map[string]google.ManifestInfo{
   289  			"a": {
   290  				Tags: []string{"c"},
   291  			},
   292  		},
   293  		need: map[string]google.ManifestInfo{
   294  			"a": {
   295  				Tags: []string{"b", "d"},
   296  			},
   297  		},
   298  	}, {
   299  		// Make sure all properties get copied over.
   300  		want: map[string]google.ManifestInfo{
   301  			"a": {
   302  				Size:      123,
   303  				MediaType: string(types.DockerManifestSchema2),
   304  				Created:   time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC),
   305  				Uploaded:  time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC),
   306  				Tags:      []string{"b", "c", "d"},
   307  			},
   308  		},
   309  		have: map[string]google.ManifestInfo{},
   310  		need: map[string]google.ManifestInfo{
   311  			"a": {
   312  				Size:      123,
   313  				MediaType: string(types.DockerManifestSchema2),
   314  				Created:   time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC),
   315  				Uploaded:  time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC),
   316  				Tags:      []string{"b", "c", "d"},
   317  			},
   318  		},
   319  	}}
   320  
   321  	for _, tc := range cases {
   322  		want, got := tc.need, diffImages(tc.want, tc.have)
   323  		if diff := cmp.Diff(want, got); diff != "" {
   324  			t.Errorf("diffing images: %v - %v: (-want +got)\n%s", tc.want, tc.have, diff)
   325  		}
   326  	}
   327  }
   328  
   329  // Test that our backoff works the way we expect.
   330  func TestBackoff(t *testing.T) {
   331  	backoff := GCRBackoff()
   332  
   333  	if d := backoff.Step(); d > 10*time.Second {
   334  		t.Errorf("Duration too long: %v", d)
   335  	}
   336  	if d := backoff.Step(); d > 100*time.Second {
   337  		t.Errorf("Duration too long: %v", d)
   338  	}
   339  	if d := backoff.Step(); d > 1000*time.Second {
   340  		t.Errorf("Duration too long: %v", d)
   341  	}
   342  	if s := backoff.Steps; s != 0 {
   343  		t.Errorf("backoff.Steps should be 0, got %d", s)
   344  	}
   345  }
   346  
   347  func TestErrors(t *testing.T) {
   348  	if hasStatusCode(nil, http.StatusOK) {
   349  		t.Fatal("nil error should not have any status code")
   350  	}
   351  	if !hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusOK) {
   352  		t.Fatal("200 should be 200")
   353  	}
   354  	if hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusNotFound) {
   355  		t.Fatal("200 should not be 404")
   356  	}
   357  
   358  	if isServerError(nil) {
   359  		t.Fatal("nil should not be server error")
   360  	}
   361  	if isServerError(fmt.Errorf("i am a string")) {
   362  		t.Fatal("string should not be server error")
   363  	}
   364  	if !isServerError(&transport.Error{StatusCode: http.StatusServiceUnavailable}) {
   365  		t.Fatal("503 should be server error")
   366  	}
   367  	if isServerError(&transport.Error{StatusCode: http.StatusTooManyRequests}) {
   368  		t.Fatal("429 should not be server error")
   369  	}
   370  }
   371  
   372  func TestRetryErrors(t *testing.T) {
   373  	// We log a warning during retries, so we can tell if something retried by checking logs.Warn.
   374  	var b bytes.Buffer
   375  	logs.Warn.SetOutput(&b)
   376  
   377  	err := backoffErrors(retry.Backoff{
   378  		Duration: 1 * time.Millisecond,
   379  		Steps:    3,
   380  	}, func() error {
   381  		return &transport.Error{StatusCode: http.StatusTooManyRequests}
   382  	})
   383  
   384  	if err == nil {
   385  		t.Fatal("backoffErrors should return internal err, got nil")
   386  	}
   387  	var terr *transport.Error
   388  	if !errors.As(err, &terr) {
   389  		t.Fatalf("backoffErrors should return internal err, got different error: %v", err)
   390  	} else if terr.StatusCode != http.StatusTooManyRequests {
   391  		t.Fatalf("backoffErrors should return internal err, got different status code: %v", terr.StatusCode)
   392  	}
   393  
   394  	if b.Len() == 0 {
   395  		t.Fatal("backoffErrors didn't log to logs.Warn")
   396  	}
   397  }
   398  
   399  func TestBadInputs(t *testing.T) {
   400  	t.Parallel()
   401  	invalid := "@@@@@@"
   402  
   403  	// Create a valid image reference that will fail with not found.
   404  	s := httptest.NewServer(http.NotFoundHandler())
   405  	u, err := url.Parse(s.URL)
   406  	if err != nil {
   407  		t.Fatal(err)
   408  	}
   409  	valid404 := fmt.Sprintf("%s/some/image", u.Host)
   410  
   411  	ctx := context.Background()
   412  
   413  	for _, tc := range []struct {
   414  		desc string
   415  		err  error
   416  	}{
   417  		{"Copy(invalid, invalid)", Copy(invalid, invalid)},
   418  		{"Copy(404, invalid)", Copy(valid404, invalid)},
   419  		{"Copy(404, 404)", Copy(valid404, valid404)},
   420  		{"CopyRepository(invalid, invalid)", CopyRepository(ctx, invalid, invalid)},
   421  		{"CopyRepository(404, invalid)", CopyRepository(ctx, valid404, invalid)},
   422  		{"CopyRepository(404, 404)", CopyRepository(ctx, valid404, valid404, WithJobs(1))},
   423  	} {
   424  		if tc.err == nil {
   425  			t.Errorf("%s: expected err, got nil", tc.desc)
   426  		}
   427  	}
   428  }
   429  

View as plain text