1 package client
2
3 import (
4 "bytes"
5 "crypto/rand"
6 "encoding/json"
7 "fmt"
8 "io"
9 "log"
10 "net/http"
11 "net/http/httptest"
12 "reflect"
13 "sort"
14 "strconv"
15 "strings"
16 "testing"
17 "time"
18
19 "github.com/distribution/reference"
20 "github.com/docker/distribution"
21 "github.com/docker/distribution/context"
22 "github.com/docker/distribution/manifest"
23 "github.com/docker/distribution/manifest/schema1"
24 "github.com/docker/distribution/registry/api/errcode"
25 v2 "github.com/docker/distribution/registry/api/v2"
26 "github.com/docker/distribution/testutil"
27 "github.com/docker/distribution/uuid"
28 "github.com/docker/libtrust"
29 "github.com/opencontainers/go-digest"
30 )
31
32 func testServer(rrm testutil.RequestResponseMap) (string, func()) {
33 h := testutil.NewHandler(rrm)
34 s := httptest.NewServer(h)
35 return s.URL, s.Close
36 }
37
38 func newRandomBlob(size int) (digest.Digest, []byte) {
39 b := make([]byte, size)
40 if n, err := rand.Read(b); err != nil {
41 panic(err)
42 } else if n != size {
43 panic("unable to read enough bytes")
44 }
45
46 return digest.FromBytes(b), b
47 }
48
49 func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) {
50 *m = append(*m, testutil.RequestResponseMapping{
51 Request: testutil.Request{
52 Method: "GET",
53 Route: "/v2/" + repo + "/blobs/" + dgst.String(),
54 },
55 Response: testutil.Response{
56 StatusCode: http.StatusOK,
57 Body: content,
58 Headers: http.Header(map[string][]string{
59 "Content-Length": {fmt.Sprint(len(content))},
60 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
61 }),
62 },
63 })
64
65 *m = append(*m, testutil.RequestResponseMapping{
66 Request: testutil.Request{
67 Method: "HEAD",
68 Route: "/v2/" + repo + "/blobs/" + dgst.String(),
69 },
70 Response: testutil.Response{
71 StatusCode: http.StatusOK,
72 Headers: http.Header(map[string][]string{
73 "Content-Length": {fmt.Sprint(len(content))},
74 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
75 }),
76 },
77 })
78 }
79
80 func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
81 headers := map[string][]string{
82 "Content-Length": {strconv.Itoa(len(content))},
83 "Content-Type": {"application/json; charset=utf-8"},
84 }
85 if link != "" {
86 headers["Link"] = append(headers["Link"], link)
87 }
88
89 *m = append(*m, testutil.RequestResponseMapping{
90 Request: testutil.Request{
91 Method: "GET",
92 Route: route,
93 },
94 Response: testutil.Response{
95 StatusCode: http.StatusOK,
96 Body: content,
97 Headers: http.Header(headers),
98 },
99 })
100 }
101
102 func TestBlobDelete(t *testing.T) {
103 dgst, _ := newRandomBlob(1024)
104 var m testutil.RequestResponseMap
105 repo, _ := reference.WithName("test.example.com/repo1")
106 m = append(m, testutil.RequestResponseMapping{
107 Request: testutil.Request{
108 Method: "DELETE",
109 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
110 },
111 Response: testutil.Response{
112 StatusCode: http.StatusAccepted,
113 Headers: http.Header(map[string][]string{
114 "Content-Length": {"0"},
115 }),
116 },
117 })
118
119 e, c := testServer(m)
120 defer c()
121
122 ctx := context.Background()
123 r, err := NewRepository(repo, e, nil)
124 if err != nil {
125 t.Fatal(err)
126 }
127 l := r.Blobs(ctx)
128 err = l.Delete(ctx, dgst)
129 if err != nil {
130 t.Errorf("Error deleting blob: %s", err.Error())
131 }
132
133 }
134
135 func TestBlobFetch(t *testing.T) {
136 d1, b1 := newRandomBlob(1024)
137 var m testutil.RequestResponseMap
138 addTestFetch("test.example.com/repo1", d1, b1, &m)
139
140 e, c := testServer(m)
141 defer c()
142
143 ctx := context.Background()
144 repo, _ := reference.WithName("test.example.com/repo1")
145 r, err := NewRepository(repo, e, nil)
146 if err != nil {
147 t.Fatal(err)
148 }
149 l := r.Blobs(ctx)
150
151 b, err := l.Get(ctx, d1)
152 if err != nil {
153 t.Fatal(err)
154 }
155 if !bytes.Equal(b, b1) {
156 t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1))
157 }
158
159
160 }
161
162 func TestBlobExistsNoContentLength(t *testing.T) {
163 var m testutil.RequestResponseMap
164
165 repo, _ := reference.WithName("biff")
166 dgst, content := newRandomBlob(1024)
167 m = append(m, testutil.RequestResponseMapping{
168 Request: testutil.Request{
169 Method: "GET",
170 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
171 },
172 Response: testutil.Response{
173 StatusCode: http.StatusOK,
174 Body: content,
175 Headers: http.Header(map[string][]string{
176
177 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
178 }),
179 },
180 })
181
182 m = append(m, testutil.RequestResponseMapping{
183 Request: testutil.Request{
184 Method: "HEAD",
185 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
186 },
187 Response: testutil.Response{
188 StatusCode: http.StatusOK,
189 Headers: http.Header(map[string][]string{
190
191 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
192 }),
193 },
194 })
195 e, c := testServer(m)
196 defer c()
197
198 ctx := context.Background()
199 r, err := NewRepository(repo, e, nil)
200 if err != nil {
201 t.Fatal(err)
202 }
203 l := r.Blobs(ctx)
204
205 _, err = l.Stat(ctx, dgst)
206 if err == nil {
207 t.Fatal(err)
208 }
209 if !strings.Contains(err.Error(), "missing content-length heade") {
210 t.Fatalf("Expected missing content-length error message")
211 }
212
213 }
214
215 func TestBlobExists(t *testing.T) {
216 d1, b1 := newRandomBlob(1024)
217 var m testutil.RequestResponseMap
218 addTestFetch("test.example.com/repo1", d1, b1, &m)
219
220 e, c := testServer(m)
221 defer c()
222
223 ctx := context.Background()
224 repo, _ := reference.WithName("test.example.com/repo1")
225 r, err := NewRepository(repo, e, nil)
226 if err != nil {
227 t.Fatal(err)
228 }
229 l := r.Blobs(ctx)
230
231 stat, err := l.Stat(ctx, d1)
232 if err != nil {
233 t.Fatal(err)
234 }
235
236 if stat.Digest != d1 {
237 t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1)
238 }
239
240 if stat.Size != int64(len(b1)) {
241 t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1))
242 }
243
244
245 }
246
247 func TestBlobUploadChunked(t *testing.T) {
248 dgst, b1 := newRandomBlob(1024)
249 var m testutil.RequestResponseMap
250 chunks := [][]byte{
251 b1[0:256],
252 b1[256:512],
253 b1[512:513],
254 b1[513:1024],
255 }
256 repo, _ := reference.WithName("test.example.com/uploadrepo")
257 uuids := []string{uuid.Generate().String()}
258 m = append(m, testutil.RequestResponseMapping{
259 Request: testutil.Request{
260 Method: "POST",
261 Route: "/v2/" + repo.Name() + "/blobs/uploads/",
262 },
263 Response: testutil.Response{
264 StatusCode: http.StatusAccepted,
265 Headers: http.Header(map[string][]string{
266 "Content-Length": {"0"},
267 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[0]},
268 "Docker-Upload-UUID": {uuids[0]},
269 "Range": {"0-0"},
270 }),
271 },
272 })
273 offset := 0
274 for i, chunk := range chunks {
275 uuids = append(uuids, uuid.Generate().String())
276 newOffset := offset + len(chunk)
277 m = append(m, testutil.RequestResponseMapping{
278 Request: testutil.Request{
279 Method: "PATCH",
280 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i],
281 Body: chunk,
282 },
283 Response: testutil.Response{
284 StatusCode: http.StatusAccepted,
285 Headers: http.Header(map[string][]string{
286 "Content-Length": {"0"},
287 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i+1]},
288 "Docker-Upload-UUID": {uuids[i+1]},
289 "Range": {fmt.Sprintf("%d-%d", offset, newOffset-1)},
290 }),
291 },
292 })
293 offset = newOffset
294 }
295 m = append(m, testutil.RequestResponseMapping{
296 Request: testutil.Request{
297 Method: "PUT",
298 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[len(uuids)-1],
299 QueryParams: map[string][]string{
300 "digest": {dgst.String()},
301 },
302 },
303 Response: testutil.Response{
304 StatusCode: http.StatusCreated,
305 Headers: http.Header(map[string][]string{
306 "Content-Length": {"0"},
307 "Docker-Content-Digest": {dgst.String()},
308 "Content-Range": {fmt.Sprintf("0-%d", offset-1)},
309 }),
310 },
311 })
312 m = append(m, testutil.RequestResponseMapping{
313 Request: testutil.Request{
314 Method: "HEAD",
315 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
316 },
317 Response: testutil.Response{
318 StatusCode: http.StatusOK,
319 Headers: http.Header(map[string][]string{
320 "Content-Length": {fmt.Sprint(offset)},
321 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
322 }),
323 },
324 })
325
326 e, c := testServer(m)
327 defer c()
328
329 ctx := context.Background()
330 r, err := NewRepository(repo, e, nil)
331 if err != nil {
332 t.Fatal(err)
333 }
334 l := r.Blobs(ctx)
335
336 upload, err := l.Create(ctx)
337 if err != nil {
338 t.Fatal(err)
339 }
340
341 if upload.ID() != uuids[0] {
342 log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0])
343 }
344
345 for _, chunk := range chunks {
346 n, err := upload.Write(chunk)
347 if err != nil {
348 t.Fatal(err)
349 }
350 if n != len(chunk) {
351 t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk))
352 }
353 }
354
355 blob, err := upload.Commit(ctx, distribution.Descriptor{
356 Digest: dgst,
357 Size: int64(len(b1)),
358 })
359 if err != nil {
360 t.Fatal(err)
361 }
362
363 if blob.Size != int64(len(b1)) {
364 t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
365 }
366 }
367
368 func TestBlobUploadMonolithic(t *testing.T) {
369 dgst, b1 := newRandomBlob(1024)
370 var m testutil.RequestResponseMap
371 repo, _ := reference.WithName("test.example.com/uploadrepo")
372 uploadID := uuid.Generate().String()
373 m = append(m, testutil.RequestResponseMapping{
374 Request: testutil.Request{
375 Method: "POST",
376 Route: "/v2/" + repo.Name() + "/blobs/uploads/",
377 },
378 Response: testutil.Response{
379 StatusCode: http.StatusAccepted,
380 Headers: http.Header(map[string][]string{
381 "Content-Length": {"0"},
382 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
383 "Docker-Upload-UUID": {uploadID},
384 "Range": {"0-0"},
385 }),
386 },
387 })
388 m = append(m, testutil.RequestResponseMapping{
389 Request: testutil.Request{
390 Method: "PATCH",
391 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
392 Body: b1,
393 },
394 Response: testutil.Response{
395 StatusCode: http.StatusAccepted,
396 Headers: http.Header(map[string][]string{
397 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
398 "Docker-Upload-UUID": {uploadID},
399 "Content-Length": {"0"},
400 "Docker-Content-Digest": {dgst.String()},
401 "Range": {fmt.Sprintf("0-%d", len(b1)-1)},
402 }),
403 },
404 })
405 m = append(m, testutil.RequestResponseMapping{
406 Request: testutil.Request{
407 Method: "PUT",
408 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
409 QueryParams: map[string][]string{
410 "digest": {dgst.String()},
411 },
412 },
413 Response: testutil.Response{
414 StatusCode: http.StatusCreated,
415 Headers: http.Header(map[string][]string{
416 "Content-Length": {"0"},
417 "Docker-Content-Digest": {dgst.String()},
418 "Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)},
419 }),
420 },
421 })
422 m = append(m, testutil.RequestResponseMapping{
423 Request: testutil.Request{
424 Method: "HEAD",
425 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
426 },
427 Response: testutil.Response{
428 StatusCode: http.StatusOK,
429 Headers: http.Header(map[string][]string{
430 "Content-Length": {fmt.Sprint(len(b1))},
431 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
432 }),
433 },
434 })
435
436 e, c := testServer(m)
437 defer c()
438
439 ctx := context.Background()
440 r, err := NewRepository(repo, e, nil)
441 if err != nil {
442 t.Fatal(err)
443 }
444 l := r.Blobs(ctx)
445
446 upload, err := l.Create(ctx)
447 if err != nil {
448 t.Fatal(err)
449 }
450
451 if upload.ID() != uploadID {
452 log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID)
453 }
454
455 n, err := upload.ReadFrom(bytes.NewReader(b1))
456 if err != nil {
457 t.Fatal(err)
458 }
459 if n != int64(len(b1)) {
460 t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
461 }
462
463 blob, err := upload.Commit(ctx, distribution.Descriptor{
464 Digest: dgst,
465 Size: int64(len(b1)),
466 })
467 if err != nil {
468 t.Fatal(err)
469 }
470
471 if blob.Size != int64(len(b1)) {
472 t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
473 }
474 }
475
476 func TestBlobMount(t *testing.T) {
477 dgst, content := newRandomBlob(1024)
478 var m testutil.RequestResponseMap
479 repo, _ := reference.WithName("test.example.com/uploadrepo")
480
481 sourceRepo, _ := reference.WithName("test.example.com/sourcerepo")
482 canonicalRef, _ := reference.WithDigest(sourceRepo, dgst)
483
484 m = append(m, testutil.RequestResponseMapping{
485 Request: testutil.Request{
486 Method: "POST",
487 Route: "/v2/" + repo.Name() + "/blobs/uploads/",
488 QueryParams: map[string][]string{"from": {sourceRepo.Name()}, "mount": {dgst.String()}},
489 },
490 Response: testutil.Response{
491 StatusCode: http.StatusCreated,
492 Headers: http.Header(map[string][]string{
493 "Content-Length": {"0"},
494 "Location": {"/v2/" + repo.Name() + "/blobs/" + dgst.String()},
495 "Docker-Content-Digest": {dgst.String()},
496 }),
497 },
498 })
499 m = append(m, testutil.RequestResponseMapping{
500 Request: testutil.Request{
501 Method: "HEAD",
502 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
503 },
504 Response: testutil.Response{
505 StatusCode: http.StatusOK,
506 Headers: http.Header(map[string][]string{
507 "Content-Length": {fmt.Sprint(len(content))},
508 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
509 }),
510 },
511 })
512
513 e, c := testServer(m)
514 defer c()
515
516 ctx := context.Background()
517 r, err := NewRepository(repo, e, nil)
518 if err != nil {
519 t.Fatal(err)
520 }
521
522 l := r.Blobs(ctx)
523
524 bw, err := l.Create(ctx, WithMountFrom(canonicalRef))
525 if bw != nil {
526 t.Fatalf("Expected blob writer to be nil, was %v", bw)
527 }
528
529 if ebm, ok := err.(distribution.ErrBlobMounted); ok {
530 if ebm.From.Digest() != dgst {
531 t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst)
532 }
533 if ebm.From.Name() != sourceRepo.Name() {
534 t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo)
535 }
536 } else {
537 t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err)
538 }
539 }
540
541 func newRandomSchemaV1Manifest(name reference.Named, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) {
542 blobs := make([]schema1.FSLayer, blobCount)
543 history := make([]schema1.History, blobCount)
544
545 for i := 0; i < blobCount; i++ {
546 dgst, blob := newRandomBlob((i % 5) * 16)
547
548 blobs[i] = schema1.FSLayer{BlobSum: dgst}
549 history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)}
550 }
551
552 m := schema1.Manifest{
553 Name: name.String(),
554 Tag: tag,
555 Architecture: "x86",
556 FSLayers: blobs,
557 History: history,
558 Versioned: manifest.Versioned{
559 SchemaVersion: 1,
560 },
561 }
562
563 pk, err := libtrust.GenerateECP256PrivateKey()
564 if err != nil {
565 panic(err)
566 }
567
568 sm, err := schema1.Sign(&m, pk)
569 if err != nil {
570 panic(err)
571 }
572
573 return sm, digest.FromBytes(sm.Canonical), sm.Canonical
574 }
575
576 func addTestManifestWithEtag(repo reference.Named, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) {
577 actualDigest := digest.FromBytes(content)
578 getReqWithEtag := testutil.Request{
579 Method: "GET",
580 Route: "/v2/" + repo.Name() + "/manifests/" + reference,
581 Headers: http.Header(map[string][]string{
582 "If-None-Match": {fmt.Sprintf(`"%s"`, dgst)},
583 }),
584 }
585
586 var getRespWithEtag testutil.Response
587 if actualDigest.String() == dgst {
588 getRespWithEtag = testutil.Response{
589 StatusCode: http.StatusNotModified,
590 Body: []byte{},
591 Headers: http.Header(map[string][]string{
592 "Content-Length": {"0"},
593 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
594 "Content-Type": {schema1.MediaTypeSignedManifest},
595 }),
596 }
597 } else {
598 getRespWithEtag = testutil.Response{
599 StatusCode: http.StatusOK,
600 Body: content,
601 Headers: http.Header(map[string][]string{
602 "Content-Length": {fmt.Sprint(len(content))},
603 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
604 "Content-Type": {schema1.MediaTypeSignedManifest},
605 }),
606 }
607
608 }
609 *m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag})
610 }
611
612 func contentDigestString(mediatype string, content []byte) string {
613 if mediatype == schema1.MediaTypeSignedManifest {
614 m, _, _ := distribution.UnmarshalManifest(mediatype, content)
615 content = m.(*schema1.SignedManifest).Canonical
616 }
617 return digest.Canonical.FromBytes(content).String()
618 }
619
620 func addTestManifest(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) {
621 *m = append(*m, testutil.RequestResponseMapping{
622 Request: testutil.Request{
623 Method: "GET",
624 Route: "/v2/" + repo.Name() + "/manifests/" + reference,
625 },
626 Response: testutil.Response{
627 StatusCode: http.StatusOK,
628 Body: content,
629 Headers: http.Header(map[string][]string{
630 "Content-Length": {fmt.Sprint(len(content))},
631 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
632 "Content-Type": {mediatype},
633 "Docker-Content-Digest": {contentDigestString(mediatype, content)},
634 }),
635 },
636 })
637 *m = append(*m, testutil.RequestResponseMapping{
638 Request: testutil.Request{
639 Method: "HEAD",
640 Route: "/v2/" + repo.Name() + "/manifests/" + reference,
641 },
642 Response: testutil.Response{
643 StatusCode: http.StatusOK,
644 Headers: http.Header(map[string][]string{
645 "Content-Length": {fmt.Sprint(len(content))},
646 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
647 "Content-Type": {mediatype},
648 "Docker-Content-Digest": {digest.Canonical.FromBytes(content).String()},
649 }),
650 },
651 })
652 }
653
654 func addTestManifestWithoutDigestHeader(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) {
655 *m = append(*m, testutil.RequestResponseMapping{
656 Request: testutil.Request{
657 Method: "GET",
658 Route: "/v2/" + repo.Name() + "/manifests/" + reference,
659 },
660 Response: testutil.Response{
661 StatusCode: http.StatusOK,
662 Body: content,
663 Headers: http.Header(map[string][]string{
664 "Content-Length": {fmt.Sprint(len(content))},
665 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
666 "Content-Type": {mediatype},
667 }),
668 },
669 })
670 *m = append(*m, testutil.RequestResponseMapping{
671 Request: testutil.Request{
672 Method: "HEAD",
673 Route: "/v2/" + repo.Name() + "/manifests/" + reference,
674 },
675 Response: testutil.Response{
676 StatusCode: http.StatusOK,
677 Headers: http.Header(map[string][]string{
678 "Content-Length": {fmt.Sprint(len(content))},
679 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
680 "Content-Type": {mediatype},
681 }),
682 },
683 })
684 }
685
686 func checkEqualManifest(m1, m2 *schema1.SignedManifest) error {
687 if m1.Name != m2.Name {
688 return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name)
689 }
690 if m1.Tag != m2.Tag {
691 return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag)
692 }
693 if len(m1.FSLayers) != len(m2.FSLayers) {
694 return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers))
695 }
696 for i := range m1.FSLayers {
697 if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum {
698 return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum)
699 }
700 }
701 if len(m1.History) != len(m2.History) {
702 return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History))
703 }
704 for i := range m1.History {
705 if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility {
706 return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility)
707 }
708 }
709 return nil
710 }
711
712 func TestV1ManifestFetch(t *testing.T) {
713 ctx := context.Background()
714 repo, _ := reference.WithName("test.example.com/repo")
715 m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
716 var m testutil.RequestResponseMap
717 _, pl, err := m1.Payload()
718 if err != nil {
719 t.Fatal(err)
720 }
721 addTestManifest(repo, dgst.String(), schema1.MediaTypeSignedManifest, pl, &m)
722 addTestManifest(repo, "latest", schema1.MediaTypeSignedManifest, pl, &m)
723 addTestManifest(repo, "badcontenttype", "text/html", pl, &m)
724
725 e, c := testServer(m)
726 defer c()
727
728 r, err := NewRepository(repo, e, nil)
729 if err != nil {
730 t.Fatal(err)
731 }
732 ms, err := r.Manifests(ctx)
733 if err != nil {
734 t.Fatal(err)
735 }
736
737 ok, err := ms.Exists(ctx, dgst)
738 if err != nil {
739 t.Fatal(err)
740 }
741 if !ok {
742 t.Fatal("Manifest does not exist")
743 }
744
745 manifest, err := ms.Get(ctx, dgst)
746 if err != nil {
747 t.Fatal(err)
748 }
749 v1manifest, ok := manifest.(*schema1.SignedManifest)
750 if !ok {
751 t.Fatalf("Unexpected manifest type from Get: %T", manifest)
752 }
753
754 if err := checkEqualManifest(v1manifest, m1); err != nil {
755 t.Fatal(err)
756 }
757
758 var contentDigest digest.Digest
759 manifest, err = ms.Get(ctx, dgst, distribution.WithTag("latest"), ReturnContentDigest(&contentDigest))
760 if err != nil {
761 t.Fatal(err)
762 }
763 v1manifest, ok = manifest.(*schema1.SignedManifest)
764 if !ok {
765 t.Fatalf("Unexpected manifest type from Get: %T", manifest)
766 }
767
768 if err = checkEqualManifest(v1manifest, m1); err != nil {
769 t.Fatal(err)
770 }
771
772 if contentDigest != dgst {
773 t.Fatalf("Unexpected returned content digest %v, expected %v", contentDigest, dgst)
774 }
775
776 manifest, err = ms.Get(ctx, dgst, distribution.WithTag("badcontenttype"))
777 if err != nil {
778 t.Fatal(err)
779 }
780 v1manifest, ok = manifest.(*schema1.SignedManifest)
781 if !ok {
782 t.Fatalf("Unexpected manifest type from Get: %T", manifest)
783 }
784
785 if err = checkEqualManifest(v1manifest, m1); err != nil {
786 t.Fatal(err)
787 }
788 }
789
790 func TestManifestFetchWithEtag(t *testing.T) {
791 repo, _ := reference.WithName("test.example.com/repo/by/tag")
792 _, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6)
793 var m testutil.RequestResponseMap
794 addTestManifestWithEtag(repo, "latest", p1, &m, d1.String())
795
796 e, c := testServer(m)
797 defer c()
798
799 ctx := context.Background()
800 r, err := NewRepository(repo, e, nil)
801 if err != nil {
802 t.Fatal(err)
803 }
804
805 ms, err := r.Manifests(ctx)
806 if err != nil {
807 t.Fatal(err)
808 }
809
810 clientManifestService, ok := ms.(*manifests)
811 if !ok {
812 panic("wrong type for client manifest service")
813 }
814 _, err = clientManifestService.Get(ctx, d1, distribution.WithTag("latest"), AddEtagToTag("latest", d1.String()))
815 if err != distribution.ErrManifestNotModified {
816 t.Fatal(err)
817 }
818 }
819
820 func TestManifestFetchWithAccept(t *testing.T) {
821 ctx := context.Background()
822 repo, _ := reference.WithName("test.example.com/repo")
823 _, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
824 headers := make(chan []string, 1)
825 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
826 headers <- req.Header["Accept"]
827 }))
828 defer close(headers)
829 defer s.Close()
830
831 r, err := NewRepository(repo, s.URL, nil)
832 if err != nil {
833 t.Fatal(err)
834 }
835 ms, err := r.Manifests(ctx)
836 if err != nil {
837 t.Fatal(err)
838 }
839
840 testCases := []struct {
841
842 mediaTypes []string
843
844 expect []string
845
846 sort bool
847 }{
848 {
849 mediaTypes: []string{},
850 expect: distribution.ManifestMediaTypes(),
851 sort: true,
852 },
853 {
854 mediaTypes: []string{"test1", "test2"},
855 expect: []string{"test1", "test2"},
856 },
857 {
858 mediaTypes: []string{"test1"},
859 expect: []string{"test1"},
860 },
861 {
862 mediaTypes: []string{""},
863 expect: []string{""},
864 },
865 }
866 for _, testCase := range testCases {
867 ms.Get(ctx, dgst, distribution.WithManifestMediaTypes(testCase.mediaTypes))
868 actual := <-headers
869 if testCase.sort {
870 sort.Strings(actual)
871 sort.Strings(testCase.expect)
872 }
873 if !reflect.DeepEqual(actual, testCase.expect) {
874 t.Fatalf("unexpected Accept header values: %v", actual)
875 }
876 }
877 }
878
879 func TestManifestDelete(t *testing.T) {
880 repo, _ := reference.WithName("test.example.com/repo/delete")
881 _, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
882 _, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
883 var m testutil.RequestResponseMap
884 m = append(m, testutil.RequestResponseMapping{
885 Request: testutil.Request{
886 Method: "DELETE",
887 Route: "/v2/" + repo.Name() + "/manifests/" + dgst1.String(),
888 },
889 Response: testutil.Response{
890 StatusCode: http.StatusAccepted,
891 Headers: http.Header(map[string][]string{
892 "Content-Length": {"0"},
893 }),
894 },
895 })
896
897 e, c := testServer(m)
898 defer c()
899
900 r, err := NewRepository(repo, e, nil)
901 if err != nil {
902 t.Fatal(err)
903 }
904 ctx := context.Background()
905 ms, err := r.Manifests(ctx)
906 if err != nil {
907 t.Fatal(err)
908 }
909
910 if err := ms.Delete(ctx, dgst1); err != nil {
911 t.Fatal(err)
912 }
913 if err := ms.Delete(ctx, dgst2); err == nil {
914 t.Fatal("Expected error deleting unknown manifest")
915 }
916
917 }
918
919 func TestManifestPut(t *testing.T) {
920 repo, _ := reference.WithName("test.example.com/repo/delete")
921 m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6)
922
923 _, payload, err := m1.Payload()
924 if err != nil {
925 t.Fatal(err)
926 }
927
928 var m testutil.RequestResponseMap
929 m = append(m, testutil.RequestResponseMapping{
930 Request: testutil.Request{
931 Method: "PUT",
932 Route: "/v2/" + repo.Name() + "/manifests/other",
933 Body: payload,
934 },
935 Response: testutil.Response{
936 StatusCode: http.StatusAccepted,
937 Headers: http.Header(map[string][]string{
938 "Content-Length": {"0"},
939 "Docker-Content-Digest": {dgst.String()},
940 }),
941 },
942 })
943
944 putDgst := digest.FromBytes(m1.Canonical)
945 m = append(m, testutil.RequestResponseMapping{
946 Request: testutil.Request{
947 Method: "PUT",
948 Route: "/v2/" + repo.Name() + "/manifests/" + putDgst.String(),
949 Body: payload,
950 },
951 Response: testutil.Response{
952 StatusCode: http.StatusAccepted,
953 Headers: http.Header(map[string][]string{
954 "Content-Length": {"0"},
955 "Docker-Content-Digest": {putDgst.String()},
956 }),
957 },
958 })
959
960 e, c := testServer(m)
961 defer c()
962
963 r, err := NewRepository(repo, e, nil)
964 if err != nil {
965 t.Fatal(err)
966 }
967 ctx := context.Background()
968 ms, err := r.Manifests(ctx)
969 if err != nil {
970 t.Fatal(err)
971 }
972
973 if _, err := ms.Put(ctx, m1, distribution.WithTag(m1.Tag)); err != nil {
974 t.Fatal(err)
975 }
976
977 if _, err := ms.Put(ctx, m1); err != nil {
978 t.Fatal(err)
979 }
980
981
982 }
983
984 func TestManifestTags(t *testing.T) {
985 repo, _ := reference.WithName("test.example.com/repo/tags/list")
986 tagsList := []byte(strings.TrimSpace(`
987 {
988 "name": "test.example.com/repo/tags/list",
989 "tags": [
990 "tag1",
991 "tag2",
992 "funtag"
993 ]
994 }
995 `))
996 var m testutil.RequestResponseMap
997 for i := 0; i < 3; i++ {
998 m = append(m, testutil.RequestResponseMapping{
999 Request: testutil.Request{
1000 Method: "GET",
1001 Route: "/v2/" + repo.Name() + "/tags/list",
1002 },
1003 Response: testutil.Response{
1004 StatusCode: http.StatusOK,
1005 Body: tagsList,
1006 Headers: http.Header(map[string][]string{
1007 "Content-Length": {fmt.Sprint(len(tagsList))},
1008 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
1009 }),
1010 },
1011 })
1012 }
1013 e, c := testServer(m)
1014 defer c()
1015
1016 r, err := NewRepository(repo, e, nil)
1017 if err != nil {
1018 t.Fatal(err)
1019 }
1020
1021 ctx := context.Background()
1022 tagService := r.Tags(ctx)
1023
1024 tags, err := tagService.All(ctx)
1025 if err != nil {
1026 t.Fatal(err)
1027 }
1028 if len(tags) != 3 {
1029 t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
1030 }
1031
1032 expected := map[string]struct{}{
1033 "tag1": {},
1034 "tag2": {},
1035 "funtag": {},
1036 }
1037 for _, t := range tags {
1038 delete(expected, t)
1039 }
1040 if len(expected) != 0 {
1041 t.Fatalf("unexpected tags returned: %v", expected)
1042 }
1043
1044 }
1045
1046 func TestObtainsErrorForMissingTag(t *testing.T) {
1047 repo, _ := reference.WithName("test.example.com/repo")
1048
1049 var m testutil.RequestResponseMap
1050 var errors errcode.Errors
1051 errors = append(errors, v2.ErrorCodeManifestUnknown.WithDetail("unknown manifest"))
1052 errBytes, err := json.Marshal(errors)
1053 if err != nil {
1054 t.Fatal(err)
1055 }
1056 m = append(m, testutil.RequestResponseMapping{
1057 Request: testutil.Request{
1058 Method: "GET",
1059 Route: "/v2/" + repo.Name() + "/manifests/1.0.0",
1060 },
1061 Response: testutil.Response{
1062 StatusCode: http.StatusNotFound,
1063 Body: errBytes,
1064 Headers: http.Header(map[string][]string{
1065 "Content-Type": {"application/json; charset=utf-8"},
1066 }),
1067 },
1068 })
1069 e, c := testServer(m)
1070 defer c()
1071
1072 ctx := context.Background()
1073 r, err := NewRepository(repo, e, nil)
1074 if err != nil {
1075 t.Fatal(err)
1076 }
1077
1078 tagService := r.Tags(ctx)
1079
1080 _, err = tagService.Get(ctx, "1.0.0")
1081 if err == nil {
1082 t.Fatalf("Expected an error")
1083 }
1084 if !strings.Contains(err.Error(), "manifest unknown") {
1085 t.Fatalf("Expected unknown manifest error message")
1086 }
1087 }
1088
1089 func TestObtainsManifestForTagWithoutHeaders(t *testing.T) {
1090 repo, _ := reference.WithName("test.example.com/repo")
1091
1092 var m testutil.RequestResponseMap
1093 m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
1094 _, pl, err := m1.Payload()
1095 if err != nil {
1096 t.Fatal(err)
1097 }
1098 addTestManifestWithoutDigestHeader(repo, "1.0.0", schema1.MediaTypeSignedManifest, pl, &m)
1099
1100 e, c := testServer(m)
1101 defer c()
1102
1103 ctx := context.Background()
1104 r, err := NewRepository(repo, e, nil)
1105 if err != nil {
1106 t.Fatal(err)
1107 }
1108
1109 tagService := r.Tags(ctx)
1110
1111 desc, err := tagService.Get(ctx, "1.0.0")
1112 if err != nil {
1113 t.Fatalf("Expected no error")
1114 }
1115 if desc.Digest != dgst {
1116 t.Fatalf("Unexpected digest")
1117 }
1118 }
1119 func TestManifestTagsPaginated(t *testing.T) {
1120 s := httptest.NewServer(http.NotFoundHandler())
1121 defer s.Close()
1122
1123 repo, _ := reference.WithName("test.example.com/repo/tags/list")
1124 tagsList := []string{"tag1", "tag2", "funtag"}
1125 var m testutil.RequestResponseMap
1126 for i := 0; i < 3; i++ {
1127 body, err := json.Marshal(map[string]interface{}{
1128 "name": "test.example.com/repo/tags/list",
1129 "tags": []string{tagsList[i]},
1130 })
1131 if err != nil {
1132 t.Fatal(err)
1133 }
1134 queryParams := make(map[string][]string)
1135 if i > 0 {
1136 queryParams["n"] = []string{"1"}
1137 queryParams["last"] = []string{tagsList[i-1]}
1138 }
1139
1140
1141 relativeLink := "/v2/" + repo.Name() + "/tags/list?n=1&last=" + tagsList[i]
1142 var link string
1143 switch i {
1144 case 0:
1145 link = relativeLink
1146 case len(tagsList) - 1:
1147 link = ""
1148 default:
1149 link = s.URL + relativeLink
1150 }
1151
1152 headers := http.Header(map[string][]string{
1153 "Content-Length": {fmt.Sprint(len(body))},
1154 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
1155 })
1156 if link != "" {
1157 headers.Set("Link", fmt.Sprintf(`<%s>; rel="next"`, link))
1158 }
1159
1160 m = append(m, testutil.RequestResponseMapping{
1161 Request: testutil.Request{
1162 Method: "GET",
1163 Route: "/v2/" + repo.Name() + "/tags/list",
1164 QueryParams: queryParams,
1165 },
1166 Response: testutil.Response{
1167 StatusCode: http.StatusOK,
1168 Body: body,
1169 Headers: headers,
1170 },
1171 })
1172 }
1173
1174 s.Config.Handler = testutil.NewHandler(m)
1175
1176 r, err := NewRepository(repo, s.URL, nil)
1177 if err != nil {
1178 t.Fatal(err)
1179 }
1180
1181 ctx := context.Background()
1182 tagService := r.Tags(ctx)
1183
1184 tags, err := tagService.All(ctx)
1185 if err != nil {
1186 t.Fatal(tags, err)
1187 }
1188 if len(tags) != 3 {
1189 t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
1190 }
1191
1192 expected := map[string]struct{}{
1193 "tag1": {},
1194 "tag2": {},
1195 "funtag": {},
1196 }
1197 for _, t := range tags {
1198 delete(expected, t)
1199 }
1200 if len(expected) != 0 {
1201 t.Fatalf("unexpected tags returned: %v", expected)
1202 }
1203 }
1204
1205 func TestManifestUnauthorized(t *testing.T) {
1206 repo, _ := reference.WithName("test.example.com/repo")
1207 _, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
1208 var m testutil.RequestResponseMap
1209
1210 m = append(m, testutil.RequestResponseMapping{
1211 Request: testutil.Request{
1212 Method: "GET",
1213 Route: "/v2/" + repo.Name() + "/manifests/" + dgst.String(),
1214 },
1215 Response: testutil.Response{
1216 StatusCode: http.StatusUnauthorized,
1217 Headers: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
1218 Body: []byte("<html>garbage</html>"),
1219 },
1220 })
1221
1222 e, c := testServer(m)
1223 defer c()
1224
1225 r, err := NewRepository(repo, e, nil)
1226 if err != nil {
1227 t.Fatal(err)
1228 }
1229 ctx := context.Background()
1230 ms, err := r.Manifests(ctx)
1231 if err != nil {
1232 t.Fatal(err)
1233 }
1234
1235 _, err = ms.Get(ctx, dgst)
1236 if err == nil {
1237 t.Fatal("Expected error fetching manifest")
1238 }
1239 v2Err, ok := err.(errcode.Error)
1240 if !ok {
1241 t.Fatalf("Unexpected error type: %#v", err)
1242 }
1243 if v2Err.Code != errcode.ErrorCodeUnauthorized {
1244 t.Fatalf("Unexpected error code: %s", v2Err.Code.String())
1245 }
1246 if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected {
1247 t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
1248 }
1249 }
1250
1251 func TestCatalog(t *testing.T) {
1252 var m testutil.RequestResponseMap
1253 addTestCatalog(
1254 "/v2/_catalog?n=5",
1255 []byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m)
1256
1257 e, c := testServer(m)
1258 defer c()
1259
1260 entries := make([]string, 5)
1261
1262 r, err := NewRegistry(e, nil)
1263 if err != nil {
1264 t.Fatal(err)
1265 }
1266
1267 ctx := context.Background()
1268 numFilled, err := r.Repositories(ctx, entries, "")
1269 if err != io.EOF {
1270 t.Fatal(err)
1271 }
1272
1273 if numFilled != 3 {
1274 t.Fatalf("Got wrong number of repos")
1275 }
1276 }
1277
1278 func TestCatalogInParts(t *testing.T) {
1279 var m testutil.RequestResponseMap
1280 addTestCatalog(
1281 "/v2/_catalog?n=2",
1282 []byte("{\"repositories\":[\"bar\", \"baz\"]}"),
1283 "</v2/_catalog?last=baz&n=2>", &m)
1284 addTestCatalog(
1285 "/v2/_catalog?last=baz&n=2",
1286 []byte("{\"repositories\":[\"foo\"]}"),
1287 "", &m)
1288
1289 e, c := testServer(m)
1290 defer c()
1291
1292 entries := make([]string, 2)
1293
1294 r, err := NewRegistry(e, nil)
1295 if err != nil {
1296 t.Fatal(err)
1297 }
1298
1299 ctx := context.Background()
1300 numFilled, err := r.Repositories(ctx, entries, "")
1301 if err != nil {
1302 t.Fatal(err)
1303 }
1304
1305 if numFilled != 2 {
1306 t.Fatalf("Got wrong number of repos")
1307 }
1308
1309 numFilled, err = r.Repositories(ctx, entries, "baz")
1310 if err != io.EOF {
1311 t.Fatal(err)
1312 }
1313
1314 if numFilled != 1 {
1315 t.Fatalf("Got wrong number of repos")
1316 }
1317 }
1318
1319 func TestSanitizeLocation(t *testing.T) {
1320 for _, testcase := range []struct {
1321 description string
1322 location string
1323 source string
1324 expected string
1325 err error
1326 }{
1327 {
1328 description: "ensure relative location correctly resolved",
1329 location: "/v2/foo/baasdf",
1330 source: "http://blahalaja.com/v1",
1331 expected: "http://blahalaja.com/v2/foo/baasdf",
1332 },
1333 {
1334 description: "ensure parameters are preserved",
1335 location: "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
1336 source: "http://blahalaja.com/v1",
1337 expected: "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
1338 },
1339 {
1340 description: "ensure new hostname overridden",
1341 location: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
1342 source: "http://blahalaja.com/v1",
1343 expected: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
1344 },
1345 } {
1346 fatalf := func(format string, args ...interface{}) {
1347 t.Fatalf(testcase.description+": "+format, args...)
1348 }
1349
1350 s, err := sanitizeLocation(testcase.location, testcase.source)
1351 if err != testcase.err {
1352 if testcase.err != nil {
1353 fatalf("expected error: %v != %v", err, testcase)
1354 } else {
1355 fatalf("unexpected error sanitizing: %v", err)
1356 }
1357 }
1358
1359 if s != testcase.expected {
1360 fatalf("bad sanitize: %q != %q", s, testcase.expected)
1361 }
1362 }
1363 }
1364
View as plain text