1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
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
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
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
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
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
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
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
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
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