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