github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/client/repository_test.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "crypto/rand" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 "net/http/httptest" 11 "strconv" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/docker/distribution" 17 "github.com/docker/distribution/context" 18 "github.com/docker/distribution/digest" 19 "github.com/docker/distribution/manifest" 20 "github.com/docker/distribution/manifest/schema1" 21 "github.com/docker/distribution/reference" 22 "github.com/docker/distribution/registry/api/errcode" 23 "github.com/docker/distribution/testutil" 24 "github.com/docker/distribution/uuid" 25 "github.com/docker/libtrust" 26 ) 27 28 func testServer(rrm testutil.RequestResponseMap) (string, func()) { 29 h := testutil.NewHandler(rrm) 30 s := httptest.NewServer(h) 31 return s.URL, s.Close 32 } 33 34 func newRandomBlob(size int) (digest.Digest, []byte) { 35 b := make([]byte, size) 36 if n, err := rand.Read(b); err != nil { 37 panic(err) 38 } else if n != size { 39 panic("unable to read enough bytes") 40 } 41 42 return digest.FromBytes(b), b 43 } 44 45 func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) { 46 *m = append(*m, testutil.RequestResponseMapping{ 47 Request: testutil.Request{ 48 Method: "GET", 49 Route: "/v2/" + repo + "/blobs/" + dgst.String(), 50 }, 51 Response: testutil.Response{ 52 StatusCode: http.StatusOK, 53 Body: content, 54 Headers: http.Header(map[string][]string{ 55 "Content-Length": {fmt.Sprint(len(content))}, 56 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 57 }), 58 }, 59 }) 60 61 *m = append(*m, testutil.RequestResponseMapping{ 62 Request: testutil.Request{ 63 Method: "HEAD", 64 Route: "/v2/" + repo + "/blobs/" + dgst.String(), 65 }, 66 Response: testutil.Response{ 67 StatusCode: http.StatusOK, 68 Headers: http.Header(map[string][]string{ 69 "Content-Length": {fmt.Sprint(len(content))}, 70 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 71 }), 72 }, 73 }) 74 } 75 76 func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) { 77 headers := map[string][]string{ 78 "Content-Length": {strconv.Itoa(len(content))}, 79 "Content-Type": {"application/json; charset=utf-8"}, 80 } 81 if link != "" { 82 headers["Link"] = append(headers["Link"], link) 83 } 84 85 *m = append(*m, testutil.RequestResponseMapping{ 86 Request: testutil.Request{ 87 Method: "GET", 88 Route: route, 89 }, 90 Response: testutil.Response{ 91 StatusCode: http.StatusOK, 92 Body: content, 93 Headers: http.Header(headers), 94 }, 95 }) 96 } 97 98 func TestBlobDelete(t *testing.T) { 99 dgst, _ := newRandomBlob(1024) 100 var m testutil.RequestResponseMap 101 repo, _ := reference.ParseNamed("test.example.com/repo1") 102 m = append(m, testutil.RequestResponseMapping{ 103 Request: testutil.Request{ 104 Method: "DELETE", 105 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), 106 }, 107 Response: testutil.Response{ 108 StatusCode: http.StatusAccepted, 109 Headers: http.Header(map[string][]string{ 110 "Content-Length": {"0"}, 111 }), 112 }, 113 }) 114 115 e, c := testServer(m) 116 defer c() 117 118 ctx := context.Background() 119 r, err := NewRepository(ctx, repo, e, nil) 120 if err != nil { 121 t.Fatal(err) 122 } 123 l := r.Blobs(ctx) 124 err = l.Delete(ctx, dgst) 125 if err != nil { 126 t.Errorf("Error deleting blob: %s", err.Error()) 127 } 128 129 } 130 131 func TestBlobFetch(t *testing.T) { 132 d1, b1 := newRandomBlob(1024) 133 var m testutil.RequestResponseMap 134 addTestFetch("test.example.com/repo1", d1, b1, &m) 135 136 e, c := testServer(m) 137 defer c() 138 139 ctx := context.Background() 140 repo, _ := reference.ParseNamed("test.example.com/repo1") 141 r, err := NewRepository(ctx, repo, e, nil) 142 if err != nil { 143 t.Fatal(err) 144 } 145 l := r.Blobs(ctx) 146 147 b, err := l.Get(ctx, d1) 148 if err != nil { 149 t.Fatal(err) 150 } 151 if bytes.Compare(b, b1) != 0 { 152 t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1)) 153 } 154 155 // TODO(dmcgowan): Test for unknown blob case 156 } 157 158 func TestBlobExistsNoContentLength(t *testing.T) { 159 var m testutil.RequestResponseMap 160 161 repo, _ := reference.ParseNamed("biff") 162 dgst, content := newRandomBlob(1024) 163 m = append(m, testutil.RequestResponseMapping{ 164 Request: testutil.Request{ 165 Method: "GET", 166 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), 167 }, 168 Response: testutil.Response{ 169 StatusCode: http.StatusOK, 170 Body: content, 171 Headers: http.Header(map[string][]string{ 172 // "Content-Length": {fmt.Sprint(len(content))}, 173 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 174 }), 175 }, 176 }) 177 178 m = append(m, testutil.RequestResponseMapping{ 179 Request: testutil.Request{ 180 Method: "HEAD", 181 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), 182 }, 183 Response: testutil.Response{ 184 StatusCode: http.StatusOK, 185 Headers: http.Header(map[string][]string{ 186 // "Content-Length": {fmt.Sprint(len(content))}, 187 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 188 }), 189 }, 190 }) 191 e, c := testServer(m) 192 defer c() 193 194 ctx := context.Background() 195 r, err := NewRepository(ctx, repo, e, nil) 196 if err != nil { 197 t.Fatal(err) 198 } 199 l := r.Blobs(ctx) 200 201 _, err = l.Stat(ctx, dgst) 202 if err == nil { 203 t.Fatal(err) 204 } 205 if !strings.Contains(err.Error(), "missing content-length heade") { 206 t.Fatalf("Expected missing content-length error message") 207 } 208 209 } 210 211 func TestBlobExists(t *testing.T) { 212 d1, b1 := newRandomBlob(1024) 213 var m testutil.RequestResponseMap 214 addTestFetch("test.example.com/repo1", d1, b1, &m) 215 216 e, c := testServer(m) 217 defer c() 218 219 ctx := context.Background() 220 repo, _ := reference.ParseNamed("test.example.com/repo1") 221 r, err := NewRepository(ctx, repo, e, nil) 222 if err != nil { 223 t.Fatal(err) 224 } 225 l := r.Blobs(ctx) 226 227 stat, err := l.Stat(ctx, d1) 228 if err != nil { 229 t.Fatal(err) 230 } 231 232 if stat.Digest != d1 { 233 t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1) 234 } 235 236 if stat.Size != int64(len(b1)) { 237 t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1)) 238 } 239 240 // TODO(dmcgowan): Test error cases and ErrBlobUnknown case 241 } 242 243 func TestBlobUploadChunked(t *testing.T) { 244 dgst, b1 := newRandomBlob(1024) 245 var m testutil.RequestResponseMap 246 chunks := [][]byte{ 247 b1[0:256], 248 b1[256:512], 249 b1[512:513], 250 b1[513:1024], 251 } 252 repo, _ := reference.ParseNamed("test.example.com/uploadrepo") 253 uuids := []string{uuid.Generate().String()} 254 m = append(m, testutil.RequestResponseMapping{ 255 Request: testutil.Request{ 256 Method: "POST", 257 Route: "/v2/" + repo.Name() + "/blobs/uploads/", 258 }, 259 Response: testutil.Response{ 260 StatusCode: http.StatusAccepted, 261 Headers: http.Header(map[string][]string{ 262 "Content-Length": {"0"}, 263 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[0]}, 264 "Docker-Upload-UUID": {uuids[0]}, 265 "Range": {"0-0"}, 266 }), 267 }, 268 }) 269 offset := 0 270 for i, chunk := range chunks { 271 uuids = append(uuids, uuid.Generate().String()) 272 newOffset := offset + len(chunk) 273 m = append(m, testutil.RequestResponseMapping{ 274 Request: testutil.Request{ 275 Method: "PATCH", 276 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i], 277 Body: chunk, 278 }, 279 Response: testutil.Response{ 280 StatusCode: http.StatusAccepted, 281 Headers: http.Header(map[string][]string{ 282 "Content-Length": {"0"}, 283 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i+1]}, 284 "Docker-Upload-UUID": {uuids[i+1]}, 285 "Range": {fmt.Sprintf("%d-%d", offset, newOffset-1)}, 286 }), 287 }, 288 }) 289 offset = newOffset 290 } 291 m = append(m, testutil.RequestResponseMapping{ 292 Request: testutil.Request{ 293 Method: "PUT", 294 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[len(uuids)-1], 295 QueryParams: map[string][]string{ 296 "digest": {dgst.String()}, 297 }, 298 }, 299 Response: testutil.Response{ 300 StatusCode: http.StatusCreated, 301 Headers: http.Header(map[string][]string{ 302 "Content-Length": {"0"}, 303 "Docker-Content-Digest": {dgst.String()}, 304 "Content-Range": {fmt.Sprintf("0-%d", offset-1)}, 305 }), 306 }, 307 }) 308 m = append(m, testutil.RequestResponseMapping{ 309 Request: testutil.Request{ 310 Method: "HEAD", 311 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), 312 }, 313 Response: testutil.Response{ 314 StatusCode: http.StatusOK, 315 Headers: http.Header(map[string][]string{ 316 "Content-Length": {fmt.Sprint(offset)}, 317 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 318 }), 319 }, 320 }) 321 322 e, c := testServer(m) 323 defer c() 324 325 ctx := context.Background() 326 r, err := NewRepository(ctx, repo, e, nil) 327 if err != nil { 328 t.Fatal(err) 329 } 330 l := r.Blobs(ctx) 331 332 upload, err := l.Create(ctx) 333 if err != nil { 334 t.Fatal(err) 335 } 336 337 if upload.ID() != uuids[0] { 338 log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0]) 339 } 340 341 for _, chunk := range chunks { 342 n, err := upload.Write(chunk) 343 if err != nil { 344 t.Fatal(err) 345 } 346 if n != len(chunk) { 347 t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk)) 348 } 349 } 350 351 blob, err := upload.Commit(ctx, distribution.Descriptor{ 352 Digest: dgst, 353 Size: int64(len(b1)), 354 }) 355 if err != nil { 356 t.Fatal(err) 357 } 358 359 if blob.Size != int64(len(b1)) { 360 t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1)) 361 } 362 } 363 364 func TestBlobUploadMonolithic(t *testing.T) { 365 dgst, b1 := newRandomBlob(1024) 366 var m testutil.RequestResponseMap 367 repo, _ := reference.ParseNamed("test.example.com/uploadrepo") 368 uploadID := uuid.Generate().String() 369 m = append(m, testutil.RequestResponseMapping{ 370 Request: testutil.Request{ 371 Method: "POST", 372 Route: "/v2/" + repo.Name() + "/blobs/uploads/", 373 }, 374 Response: testutil.Response{ 375 StatusCode: http.StatusAccepted, 376 Headers: http.Header(map[string][]string{ 377 "Content-Length": {"0"}, 378 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID}, 379 "Docker-Upload-UUID": {uploadID}, 380 "Range": {"0-0"}, 381 }), 382 }, 383 }) 384 m = append(m, testutil.RequestResponseMapping{ 385 Request: testutil.Request{ 386 Method: "PATCH", 387 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID, 388 Body: b1, 389 }, 390 Response: testutil.Response{ 391 StatusCode: http.StatusAccepted, 392 Headers: http.Header(map[string][]string{ 393 "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID}, 394 "Docker-Upload-UUID": {uploadID}, 395 "Content-Length": {"0"}, 396 "Docker-Content-Digest": {dgst.String()}, 397 "Range": {fmt.Sprintf("0-%d", len(b1)-1)}, 398 }), 399 }, 400 }) 401 m = append(m, testutil.RequestResponseMapping{ 402 Request: testutil.Request{ 403 Method: "PUT", 404 Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID, 405 QueryParams: map[string][]string{ 406 "digest": {dgst.String()}, 407 }, 408 }, 409 Response: testutil.Response{ 410 StatusCode: http.StatusCreated, 411 Headers: http.Header(map[string][]string{ 412 "Content-Length": {"0"}, 413 "Docker-Content-Digest": {dgst.String()}, 414 "Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)}, 415 }), 416 }, 417 }) 418 m = append(m, testutil.RequestResponseMapping{ 419 Request: testutil.Request{ 420 Method: "HEAD", 421 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), 422 }, 423 Response: testutil.Response{ 424 StatusCode: http.StatusOK, 425 Headers: http.Header(map[string][]string{ 426 "Content-Length": {fmt.Sprint(len(b1))}, 427 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 428 }), 429 }, 430 }) 431 432 e, c := testServer(m) 433 defer c() 434 435 ctx := context.Background() 436 r, err := NewRepository(ctx, repo, e, nil) 437 if err != nil { 438 t.Fatal(err) 439 } 440 l := r.Blobs(ctx) 441 442 upload, err := l.Create(ctx) 443 if err != nil { 444 t.Fatal(err) 445 } 446 447 if upload.ID() != uploadID { 448 log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID) 449 } 450 451 n, err := upload.ReadFrom(bytes.NewReader(b1)) 452 if err != nil { 453 t.Fatal(err) 454 } 455 if n != int64(len(b1)) { 456 t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1)) 457 } 458 459 blob, err := upload.Commit(ctx, distribution.Descriptor{ 460 Digest: dgst, 461 Size: int64(len(b1)), 462 }) 463 if err != nil { 464 t.Fatal(err) 465 } 466 467 if blob.Size != int64(len(b1)) { 468 t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1)) 469 } 470 } 471 472 func TestBlobMount(t *testing.T) { 473 dgst, content := newRandomBlob(1024) 474 var m testutil.RequestResponseMap 475 repo, _ := reference.ParseNamed("test.example.com/uploadrepo") 476 477 sourceRepo, _ := reference.ParseNamed("test.example.com/sourcerepo") 478 canonicalRef, _ := reference.WithDigest(sourceRepo, dgst) 479 480 m = append(m, testutil.RequestResponseMapping{ 481 Request: testutil.Request{ 482 Method: "POST", 483 Route: "/v2/" + repo.Name() + "/blobs/uploads/", 484 QueryParams: map[string][]string{"from": {sourceRepo.Name()}, "mount": {dgst.String()}}, 485 }, 486 Response: testutil.Response{ 487 StatusCode: http.StatusCreated, 488 Headers: http.Header(map[string][]string{ 489 "Content-Length": {"0"}, 490 "Location": {"/v2/" + repo.Name() + "/blobs/" + dgst.String()}, 491 "Docker-Content-Digest": {dgst.String()}, 492 }), 493 }, 494 }) 495 m = append(m, testutil.RequestResponseMapping{ 496 Request: testutil.Request{ 497 Method: "HEAD", 498 Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), 499 }, 500 Response: testutil.Response{ 501 StatusCode: http.StatusOK, 502 Headers: http.Header(map[string][]string{ 503 "Content-Length": {fmt.Sprint(len(content))}, 504 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 505 }), 506 }, 507 }) 508 509 e, c := testServer(m) 510 defer c() 511 512 ctx := context.Background() 513 r, err := NewRepository(ctx, repo, e, nil) 514 if err != nil { 515 t.Fatal(err) 516 } 517 518 l := r.Blobs(ctx) 519 520 bw, err := l.Create(ctx, WithMountFrom(canonicalRef)) 521 if bw != nil { 522 t.Fatalf("Expected blob writer to be nil, was %v", bw) 523 } 524 525 if ebm, ok := err.(distribution.ErrBlobMounted); ok { 526 if ebm.From.Digest() != dgst { 527 t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst) 528 } 529 if ebm.From.Name() != sourceRepo.Name() { 530 t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo) 531 } 532 } else { 533 t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err) 534 } 535 } 536 537 func newRandomSchemaV1Manifest(name reference.Named, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) { 538 blobs := make([]schema1.FSLayer, blobCount) 539 history := make([]schema1.History, blobCount) 540 541 for i := 0; i < blobCount; i++ { 542 dgst, blob := newRandomBlob((i % 5) * 16) 543 544 blobs[i] = schema1.FSLayer{BlobSum: dgst} 545 history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)} 546 } 547 548 m := schema1.Manifest{ 549 Name: name.String(), 550 Tag: tag, 551 Architecture: "x86", 552 FSLayers: blobs, 553 History: history, 554 Versioned: manifest.Versioned{ 555 SchemaVersion: 1, 556 }, 557 } 558 559 pk, err := libtrust.GenerateECP256PrivateKey() 560 if err != nil { 561 panic(err) 562 } 563 564 sm, err := schema1.Sign(&m, pk) 565 if err != nil { 566 panic(err) 567 } 568 569 return sm, digest.FromBytes(sm.Canonical), sm.Canonical 570 } 571 572 func addTestManifestWithEtag(repo reference.Named, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) { 573 actualDigest := digest.FromBytes(content) 574 getReqWithEtag := testutil.Request{ 575 Method: "GET", 576 Route: "/v2/" + repo.Name() + "/manifests/" + reference, 577 Headers: http.Header(map[string][]string{ 578 "If-None-Match": {fmt.Sprintf(`"%s"`, dgst)}, 579 }), 580 } 581 582 var getRespWithEtag testutil.Response 583 if actualDigest.String() == dgst { 584 getRespWithEtag = testutil.Response{ 585 StatusCode: http.StatusNotModified, 586 Body: []byte{}, 587 Headers: http.Header(map[string][]string{ 588 "Content-Length": {"0"}, 589 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 590 "Content-Type": {schema1.MediaTypeSignedManifest}, 591 }), 592 } 593 } else { 594 getRespWithEtag = testutil.Response{ 595 StatusCode: http.StatusOK, 596 Body: content, 597 Headers: http.Header(map[string][]string{ 598 "Content-Length": {fmt.Sprint(len(content))}, 599 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 600 "Content-Type": {schema1.MediaTypeSignedManifest}, 601 }), 602 } 603 604 } 605 *m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag}) 606 } 607 608 func addTestManifest(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) { 609 *m = append(*m, testutil.RequestResponseMapping{ 610 Request: testutil.Request{ 611 Method: "GET", 612 Route: "/v2/" + repo.Name() + "/manifests/" + reference, 613 }, 614 Response: testutil.Response{ 615 StatusCode: http.StatusOK, 616 Body: content, 617 Headers: http.Header(map[string][]string{ 618 "Content-Length": {fmt.Sprint(len(content))}, 619 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 620 "Content-Type": {mediatype}, 621 }), 622 }, 623 }) 624 *m = append(*m, testutil.RequestResponseMapping{ 625 Request: testutil.Request{ 626 Method: "HEAD", 627 Route: "/v2/" + repo.Name() + "/manifests/" + reference, 628 }, 629 Response: testutil.Response{ 630 StatusCode: http.StatusOK, 631 Headers: http.Header(map[string][]string{ 632 "Content-Length": {fmt.Sprint(len(content))}, 633 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 634 "Content-Type": {mediatype}, 635 }), 636 }, 637 }) 638 639 } 640 641 func checkEqualManifest(m1, m2 *schema1.SignedManifest) error { 642 if m1.Name != m2.Name { 643 return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name) 644 } 645 if m1.Tag != m2.Tag { 646 return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag) 647 } 648 if len(m1.FSLayers) != len(m2.FSLayers) { 649 return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers)) 650 } 651 for i := range m1.FSLayers { 652 if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum { 653 return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum) 654 } 655 } 656 if len(m1.History) != len(m2.History) { 657 return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History)) 658 } 659 for i := range m1.History { 660 if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility { 661 return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility) 662 } 663 } 664 return nil 665 } 666 667 func TestV1ManifestFetch(t *testing.T) { 668 ctx := context.Background() 669 repo, _ := reference.ParseNamed("test.example.com/repo") 670 m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) 671 var m testutil.RequestResponseMap 672 _, pl, err := m1.Payload() 673 if err != nil { 674 t.Fatal(err) 675 } 676 addTestManifest(repo, dgst.String(), schema1.MediaTypeSignedManifest, pl, &m) 677 addTestManifest(repo, "latest", schema1.MediaTypeSignedManifest, pl, &m) 678 addTestManifest(repo, "badcontenttype", "text/html", pl, &m) 679 680 e, c := testServer(m) 681 defer c() 682 683 r, err := NewRepository(context.Background(), repo, e, nil) 684 if err != nil { 685 t.Fatal(err) 686 } 687 ms, err := r.Manifests(ctx) 688 if err != nil { 689 t.Fatal(err) 690 } 691 692 ok, err := ms.Exists(ctx, dgst) 693 if err != nil { 694 t.Fatal(err) 695 } 696 if !ok { 697 t.Fatal("Manifest does not exist") 698 } 699 700 manifest, err := ms.Get(ctx, dgst) 701 if err != nil { 702 t.Fatal(err) 703 } 704 v1manifest, ok := manifest.(*schema1.SignedManifest) 705 if !ok { 706 t.Fatalf("Unexpected manifest type from Get: %T", manifest) 707 } 708 709 if err := checkEqualManifest(v1manifest, m1); err != nil { 710 t.Fatal(err) 711 } 712 713 manifest, err = ms.Get(ctx, dgst, WithTag("latest")) 714 if err != nil { 715 t.Fatal(err) 716 } 717 v1manifest, ok = manifest.(*schema1.SignedManifest) 718 if !ok { 719 t.Fatalf("Unexpected manifest type from Get: %T", manifest) 720 } 721 722 if err = checkEqualManifest(v1manifest, m1); err != nil { 723 t.Fatal(err) 724 } 725 726 manifest, err = ms.Get(ctx, dgst, WithTag("badcontenttype")) 727 if err != nil { 728 t.Fatal(err) 729 } 730 v1manifest, ok = manifest.(*schema1.SignedManifest) 731 if !ok { 732 t.Fatalf("Unexpected manifest type from Get: %T", manifest) 733 } 734 735 if err = checkEqualManifest(v1manifest, m1); err != nil { 736 t.Fatal(err) 737 } 738 } 739 740 func TestManifestFetchWithEtag(t *testing.T) { 741 repo, _ := reference.ParseNamed("test.example.com/repo/by/tag") 742 _, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6) 743 var m testutil.RequestResponseMap 744 addTestManifestWithEtag(repo, "latest", p1, &m, d1.String()) 745 746 e, c := testServer(m) 747 defer c() 748 749 ctx := context.Background() 750 r, err := NewRepository(ctx, repo, e, nil) 751 if err != nil { 752 t.Fatal(err) 753 } 754 755 ms, err := r.Manifests(ctx) 756 if err != nil { 757 t.Fatal(err) 758 } 759 760 clientManifestService, ok := ms.(*manifests) 761 if !ok { 762 panic("wrong type for client manifest service") 763 } 764 _, err = clientManifestService.Get(ctx, d1, WithTag("latest"), AddEtagToTag("latest", d1.String())) 765 if err != distribution.ErrManifestNotModified { 766 t.Fatal(err) 767 } 768 } 769 770 func TestManifestDelete(t *testing.T) { 771 repo, _ := reference.ParseNamed("test.example.com/repo/delete") 772 _, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6) 773 _, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6) 774 var m testutil.RequestResponseMap 775 m = append(m, testutil.RequestResponseMapping{ 776 Request: testutil.Request{ 777 Method: "DELETE", 778 Route: "/v2/" + repo.Name() + "/manifests/" + dgst1.String(), 779 }, 780 Response: testutil.Response{ 781 StatusCode: http.StatusAccepted, 782 Headers: http.Header(map[string][]string{ 783 "Content-Length": {"0"}, 784 }), 785 }, 786 }) 787 788 e, c := testServer(m) 789 defer c() 790 791 r, err := NewRepository(context.Background(), repo, e, nil) 792 if err != nil { 793 t.Fatal(err) 794 } 795 ctx := context.Background() 796 ms, err := r.Manifests(ctx) 797 if err != nil { 798 t.Fatal(err) 799 } 800 801 if err := ms.Delete(ctx, dgst1); err != nil { 802 t.Fatal(err) 803 } 804 if err := ms.Delete(ctx, dgst2); err == nil { 805 t.Fatal("Expected error deleting unknown manifest") 806 } 807 // TODO(dmcgowan): Check for specific unknown error 808 } 809 810 func TestManifestPut(t *testing.T) { 811 repo, _ := reference.ParseNamed("test.example.com/repo/delete") 812 m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6) 813 814 _, payload, err := m1.Payload() 815 if err != nil { 816 t.Fatal(err) 817 } 818 var m testutil.RequestResponseMap 819 m = append(m, testutil.RequestResponseMapping{ 820 Request: testutil.Request{ 821 Method: "PUT", 822 Route: "/v2/" + repo.Name() + "/manifests/other", 823 Body: payload, 824 }, 825 Response: testutil.Response{ 826 StatusCode: http.StatusAccepted, 827 Headers: http.Header(map[string][]string{ 828 "Content-Length": {"0"}, 829 "Docker-Content-Digest": {dgst.String()}, 830 }), 831 }, 832 }) 833 834 e, c := testServer(m) 835 defer c() 836 837 r, err := NewRepository(context.Background(), repo, e, nil) 838 if err != nil { 839 t.Fatal(err) 840 } 841 ctx := context.Background() 842 ms, err := r.Manifests(ctx) 843 if err != nil { 844 t.Fatal(err) 845 } 846 847 if _, err := ms.Put(ctx, m1, WithTag(m1.Tag)); err != nil { 848 t.Fatal(err) 849 } 850 851 // TODO(dmcgowan): Check for invalid input error 852 } 853 854 func TestManifestTags(t *testing.T) { 855 repo, _ := reference.ParseNamed("test.example.com/repo/tags/list") 856 tagsList := []byte(strings.TrimSpace(` 857 { 858 "name": "test.example.com/repo/tags/list", 859 "tags": [ 860 "tag1", 861 "tag2", 862 "funtag" 863 ] 864 } 865 `)) 866 var m testutil.RequestResponseMap 867 for i := 0; i < 3; i++ { 868 m = append(m, testutil.RequestResponseMapping{ 869 Request: testutil.Request{ 870 Method: "GET", 871 Route: "/v2/" + repo.Name() + "/tags/list", 872 }, 873 Response: testutil.Response{ 874 StatusCode: http.StatusOK, 875 Body: tagsList, 876 Headers: http.Header(map[string][]string{ 877 "Content-Length": {fmt.Sprint(len(tagsList))}, 878 "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, 879 }), 880 }, 881 }) 882 } 883 e, c := testServer(m) 884 defer c() 885 886 r, err := NewRepository(context.Background(), repo, e, nil) 887 if err != nil { 888 t.Fatal(err) 889 } 890 891 ctx := context.Background() 892 tagService := r.Tags(ctx) 893 894 tags, err := tagService.All(ctx) 895 if err != nil { 896 t.Fatal(err) 897 } 898 if len(tags) != 3 { 899 t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags)) 900 } 901 902 expected := map[string]struct{}{ 903 "tag1": {}, 904 "tag2": {}, 905 "funtag": {}, 906 } 907 for _, t := range tags { 908 delete(expected, t) 909 } 910 if len(expected) != 0 { 911 t.Fatalf("unexpected tags returned: %v", expected) 912 } 913 // TODO(dmcgowan): Check for error cases 914 } 915 916 func TestManifestUnauthorized(t *testing.T) { 917 repo, _ := reference.ParseNamed("test.example.com/repo") 918 _, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) 919 var m testutil.RequestResponseMap 920 921 m = append(m, testutil.RequestResponseMapping{ 922 Request: testutil.Request{ 923 Method: "GET", 924 Route: "/v2/" + repo.Name() + "/manifests/" + dgst.String(), 925 }, 926 Response: testutil.Response{ 927 StatusCode: http.StatusUnauthorized, 928 Body: []byte("<html>garbage</html>"), 929 }, 930 }) 931 932 e, c := testServer(m) 933 defer c() 934 935 r, err := NewRepository(context.Background(), repo, e, nil) 936 if err != nil { 937 t.Fatal(err) 938 } 939 ctx := context.Background() 940 ms, err := r.Manifests(ctx) 941 if err != nil { 942 t.Fatal(err) 943 } 944 945 _, err = ms.Get(ctx, dgst) 946 if err == nil { 947 t.Fatal("Expected error fetching manifest") 948 } 949 v2Err, ok := err.(errcode.Error) 950 if !ok { 951 t.Fatalf("Unexpected error type: %#v", err) 952 } 953 if v2Err.Code != errcode.ErrorCodeUnauthorized { 954 t.Fatalf("Unexpected error code: %s", v2Err.Code.String()) 955 } 956 if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected { 957 t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected) 958 } 959 } 960 961 func TestCatalog(t *testing.T) { 962 var m testutil.RequestResponseMap 963 addTestCatalog( 964 "/v2/_catalog?n=5", 965 []byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m) 966 967 e, c := testServer(m) 968 defer c() 969 970 entries := make([]string, 5) 971 972 r, err := NewRegistry(context.Background(), e, nil) 973 if err != nil { 974 t.Fatal(err) 975 } 976 977 ctx := context.Background() 978 numFilled, err := r.Repositories(ctx, entries, "") 979 if err != io.EOF { 980 t.Fatal(err) 981 } 982 983 if numFilled != 3 { 984 t.Fatalf("Got wrong number of repos") 985 } 986 } 987 988 func TestCatalogInParts(t *testing.T) { 989 var m testutil.RequestResponseMap 990 addTestCatalog( 991 "/v2/_catalog?n=2", 992 []byte("{\"repositories\":[\"bar\", \"baz\"]}"), 993 "</v2/_catalog?last=baz&n=2>", &m) 994 addTestCatalog( 995 "/v2/_catalog?last=baz&n=2", 996 []byte("{\"repositories\":[\"foo\"]}"), 997 "", &m) 998 999 e, c := testServer(m) 1000 defer c() 1001 1002 entries := make([]string, 2) 1003 1004 r, err := NewRegistry(context.Background(), e, nil) 1005 if err != nil { 1006 t.Fatal(err) 1007 } 1008 1009 ctx := context.Background() 1010 numFilled, err := r.Repositories(ctx, entries, "") 1011 if err != nil { 1012 t.Fatal(err) 1013 } 1014 1015 if numFilled != 2 { 1016 t.Fatalf("Got wrong number of repos") 1017 } 1018 1019 numFilled, err = r.Repositories(ctx, entries, "baz") 1020 if err != io.EOF { 1021 t.Fatal(err) 1022 } 1023 1024 if numFilled != 1 { 1025 t.Fatalf("Got wrong number of repos") 1026 } 1027 } 1028 1029 func TestSanitizeLocation(t *testing.T) { 1030 for _, testcase := range []struct { 1031 description string 1032 location string 1033 source string 1034 expected string 1035 err error 1036 }{ 1037 { 1038 description: "ensure relative location correctly resolved", 1039 location: "/v2/foo/baasdf", 1040 source: "http://blahalaja.com/v1", 1041 expected: "http://blahalaja.com/v2/foo/baasdf", 1042 }, 1043 { 1044 description: "ensure parameters are preserved", 1045 location: "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo", 1046 source: "http://blahalaja.com/v1", 1047 expected: "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo", 1048 }, 1049 { 1050 description: "ensure new hostname overidden", 1051 location: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf", 1052 source: "http://blahalaja.com/v1", 1053 expected: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf", 1054 }, 1055 } { 1056 fatalf := func(format string, args ...interface{}) { 1057 t.Fatalf(testcase.description+": "+format, args...) 1058 } 1059 1060 s, err := sanitizeLocation(testcase.location, testcase.source) 1061 if err != testcase.err { 1062 if testcase.err != nil { 1063 fatalf("expected error: %v != %v", err, testcase) 1064 } else { 1065 fatalf("unexpected error sanitizing: %v", err) 1066 } 1067 } 1068 1069 if s != testcase.expected { 1070 fatalf("bad sanitize: %q != %q", s, testcase.expected) 1071 } 1072 } 1073 }