diff options
Diffstat (limited to 'pkg/gcrane/copy_test.go')
-rw-r--r-- | pkg/gcrane/copy_test.go | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/pkg/gcrane/copy_test.go b/pkg/gcrane/copy_test.go new file mode 100644 index 0000000..e50564a --- /dev/null +++ b/pkg/gcrane/copy_test.go @@ -0,0 +1,428 @@ +// 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 gcrane + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/internal/retry" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type fakeXCR struct { + h http.Handler + repos map[string]google.Tags + t *testing.T +} + +func (xcr *fakeXCR) ServeHTTP(w http.ResponseWriter, r *http.Request) { + xcr.t.Logf("%s %s", r.Method, r.URL) + if strings.HasPrefix(r.URL.Path, "/v2/") && strings.HasSuffix(r.URL.Path, "/tags/list") { + repo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v2/"), "/tags/list") + if tags, ok := xcr.repos[repo]; !ok { + w.WriteHeader(http.StatusNotFound) + } else { + xcr.t.Logf("%+v", tags) + if err := json.NewEncoder(w).Encode(tags); err != nil { + xcr.t.Fatal(err) + } + } + } else { + xcr.h.ServeHTTP(w, r) + } +} + +func newFakeXCR(t *testing.T) *fakeXCR { + h := registry.New() + return &fakeXCR{h: h, t: t} +} + +func (xcr *fakeXCR) setRefs(stuff map[name.Reference]partial.Describable) error { + repos := make(map[string]google.Tags) + + for ref, thing := range stuff { + repo := ref.Context().RepositoryStr() + tags, ok := repos[repo] + if !ok { + tags = google.Tags{ + Name: repo, + Children: []string{}, + } + } + + // Populate the "child" field. + for parentPath := repo; parentPath != "."; parentPath = path.Dir(parentPath) { + child, parent := path.Base(parentPath), path.Dir(parentPath) + tags, ok := repos[parent] + if !ok { + tags = google.Tags{} + } + for _, c := range repos[parent].Children { + if c == child { + break + } + } + tags.Children = append(tags.Children, child) + repos[parent] = tags + } + + // Populate the "manifests" and "tags" field. + d, err := thing.Digest() + if err != nil { + return err + } + mt, err := thing.MediaType() + if err != nil { + return err + } + if tags.Manifests == nil { + tags.Manifests = make(map[string]google.ManifestInfo) + } + mi, ok := tags.Manifests[d.String()] + if !ok { + mi = google.ManifestInfo{ + MediaType: string(mt), + Tags: []string{}, + } + } + if tag, ok := ref.(name.Tag); ok { + tags.Tags = append(tags.Tags, tag.Identifier()) + mi.Tags = append(mi.Tags, tag.Identifier()) + } + tags.Manifests[d.String()] = mi + repos[repo] = tags + } + xcr.repos = repos + return nil +} + +func TestCopy(t *testing.T) { + logs.Warn.SetOutput(os.Stderr) + xcr := newFakeXCR(t) + s := httptest.NewServer(xcr) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + defer s.Close() + src := path.Join(u.Host, "test/gcrane") + dst := path.Join(u.Host, "test/gcrane/copy") + + oneTag, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + twoTags, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + noTags, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + + latestRef, err := name.ParseReference(src) + if err != nil { + t.Fatal(err) + } + oneTagRef := latestRef.Context().Tag("bar") + + d, err := noTags.Digest() + if err != nil { + t.Fatal(err) + } + noTagsRef := latestRef.Context().Digest(d.String()) + fooRef := latestRef.Context().Tag("foo") + + // Populate this after we create it so we know the hostname. + if err := xcr.setRefs(map[name.Reference]partial.Describable{ + oneTagRef: oneTag, + latestRef: twoTags, + fooRef: twoTags, + noTagsRef: noTags, + }); err != nil { + t.Fatal(err) + } + + if err := remote.Write(latestRef, twoTags); err != nil { + t.Fatal(err) + } + if err := remote.Write(fooRef, twoTags); err != nil { + t.Fatal(err) + } + if err := remote.Write(oneTagRef, oneTag); err != nil { + t.Fatal(err) + } + if err := remote.Write(noTagsRef, noTags); err != nil { + t.Fatal(err) + } + + if err := Copy(src, dst); err != nil { + t.Fatal(err) + } + + if err := CopyRepository(context.Background(), src, dst); err != nil { + t.Fatal(err) + } +} + +func TestRename(t *testing.T) { + c := copier{ + srcRepo: name.MustParseReference("registry.example.com/foo").Context(), + dstRepo: name.MustParseReference("registry.example.com/bar").Context(), + } + + got, err := c.rename(name.MustParseReference("registry.example.com/foo/sub/repo").Context()) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + want := name.MustParseReference("registry.example.com/bar/sub/repo").Context() + + if want.String() != got.String() { + t.Errorf("%s != %s", want, got) + } +} + +func TestSubtractStringLists(t *testing.T) { + cases := []struct { + minuend []string + subtrahend []string + result []string + }{{ + minuend: []string{"a", "b", "c"}, + subtrahend: []string{"a"}, + result: []string{"b", "c"}, + }, { + minuend: []string{"a", "a", "a"}, + subtrahend: []string{"a", "b"}, + result: []string{}, + }, { + minuend: []string{}, + subtrahend: []string{"a", "b"}, + result: []string{}, + }, { + minuend: []string{"a", "b"}, + subtrahend: []string{}, + result: []string{"a", "b"}, + }} + + for _, tc := range cases { + want, got := tc.result, subtractStringLists(tc.minuend, tc.subtrahend) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("subtracting string lists: %v - %v: (-want +got)\n%s", tc.minuend, tc.subtrahend, diff) + } + } +} + +func TestDiffImages(t *testing.T) { + cases := []struct { + want map[string]google.ManifestInfo + have map[string]google.ManifestInfo + need map[string]google.ManifestInfo + }{{ + // Have everything we need. + want: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c"}, + }, + }, + have: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c"}, + }, + }, + need: map[string]google.ManifestInfo{}, + }, { + // Missing image a. + want: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c", "d"}, + }, + }, + have: map[string]google.ManifestInfo{}, + need: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c", "d"}, + }, + }, + }, { + // Missing tags "b" and "d" + want: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c", "d"}, + }, + }, + have: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"c"}, + }, + }, + need: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "d"}, + }, + }, + }, { + // Make sure all properties get copied over. + want: map[string]google.ManifestInfo{ + "a": { + Size: 123, + MediaType: string(types.DockerManifestSchema2), + Created: time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC), + Uploaded: time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC), + Tags: []string{"b", "c", "d"}, + }, + }, + have: map[string]google.ManifestInfo{}, + need: map[string]google.ManifestInfo{ + "a": { + Size: 123, + MediaType: string(types.DockerManifestSchema2), + Created: time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC), + Uploaded: time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC), + Tags: []string{"b", "c", "d"}, + }, + }, + }} + + for _, tc := range cases { + want, got := tc.need, diffImages(tc.want, tc.have) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("diffing images: %v - %v: (-want +got)\n%s", tc.want, tc.have, diff) + } + } +} + +// Test that our backoff works the way we expect. +func TestBackoff(t *testing.T) { + backoff := GCRBackoff() + + if d := backoff.Step(); d > 10*time.Second { + t.Errorf("Duration too long: %v", d) + } + if d := backoff.Step(); d > 100*time.Second { + t.Errorf("Duration too long: %v", d) + } + if d := backoff.Step(); d > 1000*time.Second { + t.Errorf("Duration too long: %v", d) + } + if s := backoff.Steps; s != 0 { + t.Errorf("backoff.Steps should be 0, got %d", s) + } +} + +func TestErrors(t *testing.T) { + if hasStatusCode(nil, http.StatusOK) { + t.Fatal("nil error should not have any status code") + } + if !hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusOK) { + t.Fatal("200 should be 200") + } + if hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusNotFound) { + t.Fatal("200 should not be 404") + } + + if isServerError(nil) { + t.Fatal("nil should not be server error") + } + if isServerError(fmt.Errorf("i am a string")) { + t.Fatal("string should not be server error") + } + if !isServerError(&transport.Error{StatusCode: http.StatusServiceUnavailable}) { + t.Fatal("503 should be server error") + } + if isServerError(&transport.Error{StatusCode: http.StatusTooManyRequests}) { + t.Fatal("429 should not be server error") + } +} + +func TestRetryErrors(t *testing.T) { + // We log a warning during retries, so we can tell if something retried by checking logs.Warn. + var b bytes.Buffer + logs.Warn.SetOutput(&b) + + err := backoffErrors(retry.Backoff{ + Duration: 1 * time.Millisecond, + Steps: 3, + }, func() error { + return &transport.Error{StatusCode: http.StatusTooManyRequests} + }) + + if err == nil { + t.Fatal("backoffErrors should return internal err, got nil") + } + var terr *transport.Error + if !errors.As(err, &terr) { + t.Fatalf("backoffErrors should return internal err, got different error: %v", err) + } else if terr.StatusCode != http.StatusTooManyRequests { + t.Fatalf("backoffErrors should return internal err, got different status code: %v", terr.StatusCode) + } + + if b.Len() == 0 { + t.Fatal("backoffErrors didn't log to logs.Warn") + } +} + +func TestBadInputs(t *testing.T) { + t.Parallel() + invalid := "@@@@@@" + + // Create a valid image reference that will fail with not found. + s := httptest.NewServer(http.NotFoundHandler()) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + valid404 := fmt.Sprintf("%s/some/image", u.Host) + + ctx := context.Background() + + for _, tc := range []struct { + desc string + err error + }{ + {"Copy(invalid, invalid)", Copy(invalid, invalid)}, + {"Copy(404, invalid)", Copy(valid404, invalid)}, + {"Copy(404, 404)", Copy(valid404, valid404)}, + {"CopyRepository(invalid, invalid)", CopyRepository(ctx, invalid, invalid)}, + {"CopyRepository(404, invalid)", CopyRepository(ctx, valid404, invalid)}, + {"CopyRepository(404, 404)", CopyRepository(ctx, valid404, valid404, WithJobs(1))}, + } { + if tc.err == nil { + t.Errorf("%s: expected err, got nil", tc.desc) + } + } +} |