github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/handlers/api_test.go (about) 1 package handlers 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/http/httptest" 11 "net/http/httputil" 12 "net/url" 13 "os" 14 "path" 15 "reflect" 16 "regexp" 17 "strconv" 18 "strings" 19 "testing" 20 21 "github.com/docker/distribution" 22 "github.com/docker/distribution/configuration" 23 "github.com/docker/distribution/context" 24 "github.com/docker/distribution/digest" 25 "github.com/docker/distribution/manifest" 26 "github.com/docker/distribution/manifest/manifestlist" 27 "github.com/docker/distribution/manifest/schema1" 28 "github.com/docker/distribution/manifest/schema2" 29 "github.com/docker/distribution/reference" 30 "github.com/docker/distribution/registry/api/errcode" 31 "github.com/docker/distribution/registry/api/v2" 32 _ "github.com/docker/distribution/registry/storage/driver/inmemory" 33 "github.com/docker/distribution/testutil" 34 "github.com/docker/libtrust" 35 "github.com/gorilla/handlers" 36 ) 37 38 var headerConfig = http.Header{ 39 "X-Content-Type-Options": []string{"nosniff"}, 40 } 41 42 // TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified 43 // 200 OK response. 44 func TestCheckAPI(t *testing.T) { 45 env := newTestEnv(t, false) 46 47 baseURL, err := env.builder.BuildBaseURL() 48 if err != nil { 49 t.Fatalf("unexpected error building base url: %v", err) 50 } 51 52 resp, err := http.Get(baseURL) 53 if err != nil { 54 t.Fatalf("unexpected error issuing request: %v", err) 55 } 56 defer resp.Body.Close() 57 58 checkResponse(t, "issuing api base check", resp, http.StatusOK) 59 checkHeaders(t, resp, http.Header{ 60 "Content-Type": []string{"application/json; charset=utf-8"}, 61 "Content-Length": []string{"2"}, 62 }) 63 64 p, err := ioutil.ReadAll(resp.Body) 65 if err != nil { 66 t.Fatalf("unexpected error reading response body: %v", err) 67 } 68 69 if string(p) != "{}" { 70 t.Fatalf("unexpected response body: %v", string(p)) 71 } 72 } 73 74 // TestCatalogAPI tests the /v2/_catalog endpoint 75 func TestCatalogAPI(t *testing.T) { 76 chunkLen := 2 77 env := newTestEnv(t, false) 78 79 values := url.Values{ 80 "last": []string{""}, 81 "n": []string{strconv.Itoa(chunkLen)}} 82 83 catalogURL, err := env.builder.BuildCatalogURL(values) 84 if err != nil { 85 t.Fatalf("unexpected error building catalog url: %v", err) 86 } 87 88 // ----------------------------------- 89 // try to get an empty catalog 90 resp, err := http.Get(catalogURL) 91 if err != nil { 92 t.Fatalf("unexpected error issuing request: %v", err) 93 } 94 defer resp.Body.Close() 95 96 checkResponse(t, "issuing catalog api check", resp, http.StatusOK) 97 98 var ctlg struct { 99 Repositories []string `json:"repositories"` 100 } 101 102 dec := json.NewDecoder(resp.Body) 103 if err := dec.Decode(&ctlg); err != nil { 104 t.Fatalf("error decoding fetched manifest: %v", err) 105 } 106 107 // we haven't pushed anything to the registry yet 108 if len(ctlg.Repositories) != 0 { 109 t.Fatalf("repositories has unexpected values") 110 } 111 112 if resp.Header.Get("Link") != "" { 113 t.Fatalf("repositories has more data when none expected") 114 } 115 116 // ----------------------------------- 117 // push something to the registry and try again 118 images := []string{"foo/aaaa", "foo/bbbb", "foo/cccc"} 119 120 for _, image := range images { 121 createRepository(env, t, image, "sometag") 122 } 123 124 resp, err = http.Get(catalogURL) 125 if err != nil { 126 t.Fatalf("unexpected error issuing request: %v", err) 127 } 128 defer resp.Body.Close() 129 130 checkResponse(t, "issuing catalog api check", resp, http.StatusOK) 131 132 dec = json.NewDecoder(resp.Body) 133 if err = dec.Decode(&ctlg); err != nil { 134 t.Fatalf("error decoding fetched manifest: %v", err) 135 } 136 137 if len(ctlg.Repositories) != chunkLen { 138 t.Fatalf("repositories has unexpected values") 139 } 140 141 for _, image := range images[:chunkLen] { 142 if !contains(ctlg.Repositories, image) { 143 t.Fatalf("didn't find our repository '%s' in the catalog", image) 144 } 145 } 146 147 link := resp.Header.Get("Link") 148 if link == "" { 149 t.Fatalf("repositories has less data than expected") 150 } 151 152 newValues := checkLink(t, link, chunkLen, ctlg.Repositories[len(ctlg.Repositories)-1]) 153 154 // ----------------------------------- 155 // get the last chunk of data 156 157 catalogURL, err = env.builder.BuildCatalogURL(newValues) 158 if err != nil { 159 t.Fatalf("unexpected error building catalog url: %v", err) 160 } 161 162 resp, err = http.Get(catalogURL) 163 if err != nil { 164 t.Fatalf("unexpected error issuing request: %v", err) 165 } 166 defer resp.Body.Close() 167 168 checkResponse(t, "issuing catalog api check", resp, http.StatusOK) 169 170 dec = json.NewDecoder(resp.Body) 171 if err = dec.Decode(&ctlg); err != nil { 172 t.Fatalf("error decoding fetched manifest: %v", err) 173 } 174 175 if len(ctlg.Repositories) != 1 { 176 t.Fatalf("repositories has unexpected values") 177 } 178 179 lastImage := images[len(images)-1] 180 if !contains(ctlg.Repositories, lastImage) { 181 t.Fatalf("didn't find our repository '%s' in the catalog", lastImage) 182 } 183 184 link = resp.Header.Get("Link") 185 if link != "" { 186 t.Fatalf("catalog has unexpected data") 187 } 188 } 189 190 func checkLink(t *testing.T, urlStr string, numEntries int, last string) url.Values { 191 re := regexp.MustCompile("<(/v2/_catalog.*)>; rel=\"next\"") 192 matches := re.FindStringSubmatch(urlStr) 193 194 if len(matches) != 2 { 195 t.Fatalf("Catalog link address response was incorrect") 196 } 197 linkURL, _ := url.Parse(matches[1]) 198 urlValues := linkURL.Query() 199 200 if urlValues.Get("n") != strconv.Itoa(numEntries) { 201 t.Fatalf("Catalog link entry size is incorrect") 202 } 203 204 if urlValues.Get("last") != last { 205 t.Fatal("Catalog link last entry is incorrect") 206 } 207 208 return urlValues 209 } 210 211 func contains(elems []string, e string) bool { 212 for _, elem := range elems { 213 if elem == e { 214 return true 215 } 216 } 217 return false 218 } 219 220 func TestURLPrefix(t *testing.T) { 221 config := configuration.Configuration{ 222 Storage: configuration.Storage{ 223 "inmemory": configuration.Parameters{}, 224 }, 225 } 226 config.HTTP.Prefix = "/test/" 227 config.HTTP.Headers = headerConfig 228 229 env := newTestEnvWithConfig(t, &config) 230 231 baseURL, err := env.builder.BuildBaseURL() 232 if err != nil { 233 t.Fatalf("unexpected error building base url: %v", err) 234 } 235 236 parsed, _ := url.Parse(baseURL) 237 if !strings.HasPrefix(parsed.Path, config.HTTP.Prefix) { 238 t.Fatalf("Prefix %v not included in test url %v", config.HTTP.Prefix, baseURL) 239 } 240 241 resp, err := http.Get(baseURL) 242 if err != nil { 243 t.Fatalf("unexpected error issuing request: %v", err) 244 } 245 defer resp.Body.Close() 246 247 checkResponse(t, "issuing api base check", resp, http.StatusOK) 248 checkHeaders(t, resp, http.Header{ 249 "Content-Type": []string{"application/json; charset=utf-8"}, 250 "Content-Length": []string{"2"}, 251 }) 252 } 253 254 type blobArgs struct { 255 imageName reference.Named 256 layerFile io.ReadSeeker 257 layerDigest digest.Digest 258 } 259 260 func makeBlobArgs(t *testing.T) blobArgs { 261 layerFile, layerDigest, err := testutil.CreateRandomTarFile() 262 if err != nil { 263 t.Fatalf("error creating random layer file: %v", err) 264 } 265 266 args := blobArgs{ 267 layerFile: layerFile, 268 layerDigest: layerDigest, 269 } 270 args.imageName, _ = reference.ParseNamed("foo/bar") 271 return args 272 } 273 274 // TestBlobAPI conducts a full test of the of the blob api. 275 func TestBlobAPI(t *testing.T) { 276 deleteEnabled := false 277 env := newTestEnv(t, deleteEnabled) 278 args := makeBlobArgs(t) 279 testBlobAPI(t, env, args) 280 281 deleteEnabled = true 282 env = newTestEnv(t, deleteEnabled) 283 args = makeBlobArgs(t) 284 testBlobAPI(t, env, args) 285 286 } 287 288 func TestBlobDelete(t *testing.T) { 289 deleteEnabled := true 290 env := newTestEnv(t, deleteEnabled) 291 292 args := makeBlobArgs(t) 293 env = testBlobAPI(t, env, args) 294 testBlobDelete(t, env, args) 295 } 296 297 func TestBlobDeleteDisabled(t *testing.T) { 298 deleteEnabled := false 299 env := newTestEnv(t, deleteEnabled) 300 args := makeBlobArgs(t) 301 302 imageName := args.imageName 303 layerDigest := args.layerDigest 304 ref, _ := reference.WithDigest(imageName, layerDigest) 305 layerURL, err := env.builder.BuildBlobURL(ref) 306 if err != nil { 307 t.Fatalf("error building url: %v", err) 308 } 309 310 resp, err := httpDelete(layerURL) 311 if err != nil { 312 t.Fatalf("unexpected error deleting when disabled: %v", err) 313 } 314 315 checkResponse(t, "status of disabled delete", resp, http.StatusMethodNotAllowed) 316 } 317 318 func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { 319 // TODO(stevvooe): This test code is complete junk but it should cover the 320 // complete flow. This must be broken down and checked against the 321 // specification *before* we submit the final to docker core. 322 imageName := args.imageName 323 layerFile := args.layerFile 324 layerDigest := args.layerDigest 325 326 // ----------------------------------- 327 // Test fetch for non-existent content 328 ref, _ := reference.WithDigest(imageName, layerDigest) 329 layerURL, err := env.builder.BuildBlobURL(ref) 330 if err != nil { 331 t.Fatalf("error building url: %v", err) 332 } 333 334 resp, err := http.Get(layerURL) 335 if err != nil { 336 t.Fatalf("unexpected error fetching non-existent layer: %v", err) 337 } 338 339 checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound) 340 341 // ------------------------------------------ 342 // Test head request for non-existent content 343 resp, err = http.Head(layerURL) 344 if err != nil { 345 t.Fatalf("unexpected error checking head on non-existent layer: %v", err) 346 } 347 348 checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound) 349 350 // ------------------------------------------ 351 // Start an upload, check the status then cancel 352 uploadURLBase, uploadUUID := startPushLayer(t, env.builder, imageName) 353 354 // A status check should work 355 resp, err = http.Get(uploadURLBase) 356 if err != nil { 357 t.Fatalf("unexpected error getting upload status: %v", err) 358 } 359 checkResponse(t, "status of deleted upload", resp, http.StatusNoContent) 360 checkHeaders(t, resp, http.Header{ 361 "Location": []string{"*"}, 362 "Range": []string{"0-0"}, 363 "Docker-Upload-UUID": []string{uploadUUID}, 364 }) 365 366 req, err := http.NewRequest("DELETE", uploadURLBase, nil) 367 if err != nil { 368 t.Fatalf("unexpected error creating delete request: %v", err) 369 } 370 371 resp, err = http.DefaultClient.Do(req) 372 if err != nil { 373 t.Fatalf("unexpected error sending delete request: %v", err) 374 } 375 376 checkResponse(t, "deleting upload", resp, http.StatusNoContent) 377 378 // A status check should result in 404 379 resp, err = http.Get(uploadURLBase) 380 if err != nil { 381 t.Fatalf("unexpected error getting upload status: %v", err) 382 } 383 checkResponse(t, "status of deleted upload", resp, http.StatusNotFound) 384 385 // ----------------------------------------- 386 // Do layer push with an empty body and different digest 387 uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName) 388 resp, err = doPushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, bytes.NewReader([]byte{})) 389 if err != nil { 390 t.Fatalf("unexpected error doing bad layer push: %v", err) 391 } 392 393 checkResponse(t, "bad layer push", resp, http.StatusBadRequest) 394 checkBodyHasErrorCodes(t, "bad layer push", resp, v2.ErrorCodeDigestInvalid) 395 396 // ----------------------------------------- 397 // Do layer push with an empty body and correct digest 398 zeroDigest, err := digest.FromReader(bytes.NewReader([]byte{})) 399 if err != nil { 400 t.Fatalf("unexpected error digesting empty buffer: %v", err) 401 } 402 403 uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName) 404 pushLayer(t, env.builder, imageName, zeroDigest, uploadURLBase, bytes.NewReader([]byte{})) 405 406 // ----------------------------------------- 407 // Do layer push with an empty body and correct digest 408 409 // This is a valid but empty tarfile! 410 emptyTar := bytes.Repeat([]byte("\x00"), 1024) 411 emptyDigest, err := digest.FromReader(bytes.NewReader(emptyTar)) 412 if err != nil { 413 t.Fatalf("unexpected error digesting empty tar: %v", err) 414 } 415 416 uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName) 417 pushLayer(t, env.builder, imageName, emptyDigest, uploadURLBase, bytes.NewReader(emptyTar)) 418 419 // ------------------------------------------ 420 // Now, actually do successful upload. 421 layerLength, _ := layerFile.Seek(0, os.SEEK_END) 422 layerFile.Seek(0, os.SEEK_SET) 423 424 uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName) 425 pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) 426 427 // ------------------------------------------ 428 // Now, push just a chunk 429 layerFile.Seek(0, 0) 430 431 canonicalDigester := digest.Canonical.New() 432 if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil { 433 t.Fatalf("error copying to digest: %v", err) 434 } 435 canonicalDigest := canonicalDigester.Digest() 436 437 layerFile.Seek(0, 0) 438 uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName) 439 uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength) 440 finishUpload(t, env.builder, imageName, uploadURLBase, dgst) 441 442 // ------------------------ 443 // Use a head request to see if the layer exists. 444 resp, err = http.Head(layerURL) 445 if err != nil { 446 t.Fatalf("unexpected error checking head on existing layer: %v", err) 447 } 448 449 checkResponse(t, "checking head on existing layer", resp, http.StatusOK) 450 checkHeaders(t, resp, http.Header{ 451 "Content-Length": []string{fmt.Sprint(layerLength)}, 452 "Docker-Content-Digest": []string{canonicalDigest.String()}, 453 }) 454 455 // ---------------- 456 // Fetch the layer! 457 resp, err = http.Get(layerURL) 458 if err != nil { 459 t.Fatalf("unexpected error fetching layer: %v", err) 460 } 461 462 checkResponse(t, "fetching layer", resp, http.StatusOK) 463 checkHeaders(t, resp, http.Header{ 464 "Content-Length": []string{fmt.Sprint(layerLength)}, 465 "Docker-Content-Digest": []string{canonicalDigest.String()}, 466 }) 467 468 // Verify the body 469 verifier, err := digest.NewDigestVerifier(layerDigest) 470 if err != nil { 471 t.Fatalf("unexpected error getting digest verifier: %s", err) 472 } 473 io.Copy(verifier, resp.Body) 474 475 if !verifier.Verified() { 476 t.Fatalf("response body did not pass verification") 477 } 478 479 // ---------------- 480 // Fetch the layer with an invalid digest 481 badURL := strings.Replace(layerURL, "sha256", "sha257", 1) 482 resp, err = http.Get(badURL) 483 if err != nil { 484 t.Fatalf("unexpected error fetching layer: %v", err) 485 } 486 487 checkResponse(t, "fetching layer bad digest", resp, http.StatusBadRequest) 488 489 // Cache headers 490 resp, err = http.Get(layerURL) 491 if err != nil { 492 t.Fatalf("unexpected error fetching layer: %v", err) 493 } 494 495 checkResponse(t, "fetching layer", resp, http.StatusOK) 496 checkHeaders(t, resp, http.Header{ 497 "Content-Length": []string{fmt.Sprint(layerLength)}, 498 "Docker-Content-Digest": []string{canonicalDigest.String()}, 499 "ETag": []string{fmt.Sprintf(`"%s"`, canonicalDigest)}, 500 "Cache-Control": []string{"max-age=31536000"}, 501 }) 502 503 // Matching etag, gives 304 504 etag := resp.Header.Get("Etag") 505 req, err = http.NewRequest("GET", layerURL, nil) 506 if err != nil { 507 t.Fatalf("Error constructing request: %s", err) 508 } 509 req.Header.Set("If-None-Match", etag) 510 511 resp, err = http.DefaultClient.Do(req) 512 if err != nil { 513 t.Fatalf("Error constructing request: %s", err) 514 } 515 516 checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified) 517 518 // Non-matching etag, gives 200 519 req, err = http.NewRequest("GET", layerURL, nil) 520 if err != nil { 521 t.Fatalf("Error constructing request: %s", err) 522 } 523 req.Header.Set("If-None-Match", "") 524 resp, err = http.DefaultClient.Do(req) 525 checkResponse(t, "fetching layer with invalid etag", resp, http.StatusOK) 526 527 // Missing tests: 528 // - Upload the same tar file under and different repository and 529 // ensure the content remains uncorrupted. 530 return env 531 } 532 533 func testBlobDelete(t *testing.T, env *testEnv, args blobArgs) { 534 // Upload a layer 535 imageName := args.imageName 536 layerFile := args.layerFile 537 layerDigest := args.layerDigest 538 539 ref, _ := reference.WithDigest(imageName, layerDigest) 540 layerURL, err := env.builder.BuildBlobURL(ref) 541 if err != nil { 542 t.Fatalf(err.Error()) 543 } 544 // --------------- 545 // Delete a layer 546 resp, err := httpDelete(layerURL) 547 if err != nil { 548 t.Fatalf("unexpected error deleting layer: %v", err) 549 } 550 551 checkResponse(t, "deleting layer", resp, http.StatusAccepted) 552 checkHeaders(t, resp, http.Header{ 553 "Content-Length": []string{"0"}, 554 }) 555 556 // --------------- 557 // Try and get it back 558 // Use a head request to see if the layer exists. 559 resp, err = http.Head(layerURL) 560 if err != nil { 561 t.Fatalf("unexpected error checking head on existing layer: %v", err) 562 } 563 564 checkResponse(t, "checking existence of deleted layer", resp, http.StatusNotFound) 565 566 // Delete already deleted layer 567 resp, err = httpDelete(layerURL) 568 if err != nil { 569 t.Fatalf("unexpected error deleting layer: %v", err) 570 } 571 572 checkResponse(t, "deleting layer", resp, http.StatusNotFound) 573 574 // ---------------- 575 // Attempt to delete a layer with an invalid digest 576 badURL := strings.Replace(layerURL, "sha256", "sha257", 1) 577 resp, err = httpDelete(badURL) 578 if err != nil { 579 t.Fatalf("unexpected error fetching layer: %v", err) 580 } 581 582 checkResponse(t, "deleting layer bad digest", resp, http.StatusBadRequest) 583 584 // ---------------- 585 // Reupload previously deleted blob 586 layerFile.Seek(0, os.SEEK_SET) 587 588 uploadURLBase, _ := startPushLayer(t, env.builder, imageName) 589 pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) 590 591 layerFile.Seek(0, os.SEEK_SET) 592 canonicalDigester := digest.Canonical.New() 593 if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil { 594 t.Fatalf("error copying to digest: %v", err) 595 } 596 canonicalDigest := canonicalDigester.Digest() 597 598 // ------------------------ 599 // Use a head request to see if it exists 600 resp, err = http.Head(layerURL) 601 if err != nil { 602 t.Fatalf("unexpected error checking head on existing layer: %v", err) 603 } 604 605 layerLength, _ := layerFile.Seek(0, os.SEEK_END) 606 checkResponse(t, "checking head on reuploaded layer", resp, http.StatusOK) 607 checkHeaders(t, resp, http.Header{ 608 "Content-Length": []string{fmt.Sprint(layerLength)}, 609 "Docker-Content-Digest": []string{canonicalDigest.String()}, 610 }) 611 } 612 613 func TestDeleteDisabled(t *testing.T) { 614 env := newTestEnv(t, false) 615 616 imageName, _ := reference.ParseNamed("foo/bar") 617 // "build" our layer file 618 layerFile, layerDigest, err := testutil.CreateRandomTarFile() 619 if err != nil { 620 t.Fatalf("error creating random layer file: %v", err) 621 } 622 623 ref, _ := reference.WithDigest(imageName, layerDigest) 624 layerURL, err := env.builder.BuildBlobURL(ref) 625 if err != nil { 626 t.Fatalf("Error building blob URL") 627 } 628 uploadURLBase, _ := startPushLayer(t, env.builder, imageName) 629 pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) 630 631 resp, err := httpDelete(layerURL) 632 if err != nil { 633 t.Fatalf("unexpected error deleting layer: %v", err) 634 } 635 636 checkResponse(t, "deleting layer with delete disabled", resp, http.StatusMethodNotAllowed) 637 } 638 639 func TestDeleteReadOnly(t *testing.T) { 640 env := newTestEnv(t, true) 641 642 imageName, _ := reference.ParseNamed("foo/bar") 643 // "build" our layer file 644 layerFile, layerDigest, err := testutil.CreateRandomTarFile() 645 if err != nil { 646 t.Fatalf("error creating random layer file: %v", err) 647 } 648 649 ref, _ := reference.WithDigest(imageName, layerDigest) 650 layerURL, err := env.builder.BuildBlobURL(ref) 651 if err != nil { 652 t.Fatalf("Error building blob URL") 653 } 654 uploadURLBase, _ := startPushLayer(t, env.builder, imageName) 655 pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) 656 657 env.app.readOnly = true 658 659 resp, err := httpDelete(layerURL) 660 if err != nil { 661 t.Fatalf("unexpected error deleting layer: %v", err) 662 } 663 664 checkResponse(t, "deleting layer in read-only mode", resp, http.StatusMethodNotAllowed) 665 } 666 667 func TestStartPushReadOnly(t *testing.T) { 668 env := newTestEnv(t, true) 669 env.app.readOnly = true 670 671 imageName, _ := reference.ParseNamed("foo/bar") 672 673 layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) 674 if err != nil { 675 t.Fatalf("unexpected error building layer upload url: %v", err) 676 } 677 678 resp, err := http.Post(layerUploadURL, "", nil) 679 if err != nil { 680 t.Fatalf("unexpected error starting layer push: %v", err) 681 } 682 defer resp.Body.Close() 683 684 checkResponse(t, "starting push in read-only mode", resp, http.StatusMethodNotAllowed) 685 } 686 687 func httpDelete(url string) (*http.Response, error) { 688 req, err := http.NewRequest("DELETE", url, nil) 689 if err != nil { 690 return nil, err 691 } 692 693 resp, err := http.DefaultClient.Do(req) 694 if err != nil { 695 return nil, err 696 } 697 // defer resp.Body.Close() 698 return resp, err 699 } 700 701 type manifestArgs struct { 702 imageName reference.Named 703 mediaType string 704 manifest distribution.Manifest 705 dgst digest.Digest 706 } 707 708 func TestManifestAPI(t *testing.T) { 709 schema1Repo, _ := reference.ParseNamed("foo/schema1") 710 schema2Repo, _ := reference.ParseNamed("foo/schema2") 711 712 deleteEnabled := false 713 env := newTestEnv(t, deleteEnabled) 714 testManifestAPISchema1(t, env, schema1Repo) 715 schema2Args := testManifestAPISchema2(t, env, schema2Repo) 716 testManifestAPIManifestList(t, env, schema2Args) 717 718 deleteEnabled = true 719 env = newTestEnv(t, deleteEnabled) 720 testManifestAPISchema1(t, env, schema1Repo) 721 schema2Args = testManifestAPISchema2(t, env, schema2Repo) 722 testManifestAPIManifestList(t, env, schema2Args) 723 } 724 725 func TestManifestDelete(t *testing.T) { 726 schema1Repo, _ := reference.ParseNamed("foo/schema1") 727 schema2Repo, _ := reference.ParseNamed("foo/schema2") 728 729 deleteEnabled := true 730 env := newTestEnv(t, deleteEnabled) 731 schema1Args := testManifestAPISchema1(t, env, schema1Repo) 732 testManifestDelete(t, env, schema1Args) 733 schema2Args := testManifestAPISchema2(t, env, schema2Repo) 734 testManifestDelete(t, env, schema2Args) 735 } 736 737 func TestManifestDeleteDisabled(t *testing.T) { 738 schema1Repo, _ := reference.ParseNamed("foo/schema1") 739 deleteEnabled := false 740 env := newTestEnv(t, deleteEnabled) 741 testManifestDeleteDisabled(t, env, schema1Repo) 742 } 743 744 func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName reference.Named) { 745 ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar) 746 manifestURL, err := env.builder.BuildManifestURL(ref) 747 if err != nil { 748 t.Fatalf("unexpected error getting manifest url: %v", err) 749 } 750 751 resp, err := httpDelete(manifestURL) 752 if err != nil { 753 t.Fatalf("unexpected error deleting manifest %v", err) 754 } 755 defer resp.Body.Close() 756 757 checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed) 758 } 759 760 func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { 761 tag := "thetag" 762 args := manifestArgs{imageName: imageName} 763 764 tagRef, _ := reference.WithTag(imageName, tag) 765 manifestURL, err := env.builder.BuildManifestURL(tagRef) 766 if err != nil { 767 t.Fatalf("unexpected error getting manifest url: %v", err) 768 } 769 770 // ----------------------------- 771 // Attempt to fetch the manifest 772 resp, err := http.Get(manifestURL) 773 if err != nil { 774 t.Fatalf("unexpected error getting manifest: %v", err) 775 } 776 defer resp.Body.Close() 777 778 checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) 779 checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) 780 781 tagsURL, err := env.builder.BuildTagsURL(imageName) 782 if err != nil { 783 t.Fatalf("unexpected error building tags url: %v", err) 784 } 785 786 resp, err = http.Get(tagsURL) 787 if err != nil { 788 t.Fatalf("unexpected error getting unknown tags: %v", err) 789 } 790 defer resp.Body.Close() 791 792 // Check that we get an unknown repository error when asking for tags 793 checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) 794 checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) 795 796 // -------------------------------- 797 // Attempt to push unsigned manifest with missing layers 798 unsignedManifest := &schema1.Manifest{ 799 Versioned: manifest.Versioned{ 800 SchemaVersion: 1, 801 }, 802 Name: imageName.Name(), 803 Tag: tag, 804 FSLayers: []schema1.FSLayer{ 805 { 806 BlobSum: "asdf", 807 }, 808 { 809 BlobSum: "qwer", 810 }, 811 }, 812 History: []schema1.History{ 813 { 814 V1Compatibility: "", 815 }, 816 { 817 V1Compatibility: "", 818 }, 819 }, 820 } 821 822 resp = putManifest(t, "putting unsigned manifest", manifestURL, "", unsignedManifest) 823 defer resp.Body.Close() 824 checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest) 825 _, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp, v2.ErrorCodeManifestInvalid) 826 827 expectedCounts := map[errcode.ErrorCode]int{ 828 v2.ErrorCodeManifestInvalid: 1, 829 } 830 831 if !reflect.DeepEqual(counts, expectedCounts) { 832 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 833 } 834 835 // sign the manifest and still get some interesting errors. 836 sm, err := schema1.Sign(unsignedManifest, env.pk) 837 if err != nil { 838 t.Fatalf("error signing manifest: %v", err) 839 } 840 841 resp = putManifest(t, "putting signed manifest with errors", manifestURL, "", sm) 842 defer resp.Body.Close() 843 checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest) 844 _, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp, 845 v2.ErrorCodeManifestBlobUnknown, v2.ErrorCodeDigestInvalid) 846 847 expectedCounts = map[errcode.ErrorCode]int{ 848 v2.ErrorCodeManifestBlobUnknown: 2, 849 v2.ErrorCodeDigestInvalid: 2, 850 } 851 852 if !reflect.DeepEqual(counts, expectedCounts) { 853 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 854 } 855 856 // TODO(stevvooe): Add a test case where we take a mostly valid registry, 857 // tamper with the content and ensure that we get a unverified manifest 858 // error. 859 860 // Push 2 random layers 861 expectedLayers := make(map[digest.Digest]io.ReadSeeker) 862 863 for i := range unsignedManifest.FSLayers { 864 rs, dgstStr, err := testutil.CreateRandomTarFile() 865 866 if err != nil { 867 t.Fatalf("error creating random layer %d: %v", i, err) 868 } 869 dgst := digest.Digest(dgstStr) 870 871 expectedLayers[dgst] = rs 872 unsignedManifest.FSLayers[i].BlobSum = dgst 873 874 uploadURLBase, _ := startPushLayer(t, env.builder, imageName) 875 pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) 876 } 877 878 // ------------------- 879 // Push the signed manifest with all layers pushed. 880 signedManifest, err := schema1.Sign(unsignedManifest, env.pk) 881 if err != nil { 882 t.Fatalf("unexpected error signing manifest: %v", err) 883 } 884 885 dgst := digest.FromBytes(signedManifest.Canonical) 886 args.manifest = signedManifest 887 args.dgst = dgst 888 889 digestRef, _ := reference.WithDigest(imageName, dgst) 890 manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) 891 checkErr(t, err, "building manifest url") 892 893 resp = putManifest(t, "putting signed manifest no error", manifestURL, "", signedManifest) 894 checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated) 895 checkHeaders(t, resp, http.Header{ 896 "Location": []string{manifestDigestURL}, 897 "Docker-Content-Digest": []string{dgst.String()}, 898 }) 899 900 // -------------------- 901 // Push by digest -- should get same result 902 resp = putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) 903 checkResponse(t, "putting signed manifest", resp, http.StatusCreated) 904 checkHeaders(t, resp, http.Header{ 905 "Location": []string{manifestDigestURL}, 906 "Docker-Content-Digest": []string{dgst.String()}, 907 }) 908 909 // ------------------ 910 // Fetch by tag name 911 resp, err = http.Get(manifestURL) 912 if err != nil { 913 t.Fatalf("unexpected error fetching manifest: %v", err) 914 } 915 defer resp.Body.Close() 916 917 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 918 checkHeaders(t, resp, http.Header{ 919 "Docker-Content-Digest": []string{dgst.String()}, 920 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 921 }) 922 923 var fetchedManifest schema1.SignedManifest 924 dec := json.NewDecoder(resp.Body) 925 926 if err := dec.Decode(&fetchedManifest); err != nil { 927 t.Fatalf("error decoding fetched manifest: %v", err) 928 } 929 930 if !bytes.Equal(fetchedManifest.Canonical, signedManifest.Canonical) { 931 t.Fatalf("manifests do not match") 932 } 933 934 // --------------- 935 // Fetch by digest 936 resp, err = http.Get(manifestDigestURL) 937 checkErr(t, err, "fetching manifest by digest") 938 defer resp.Body.Close() 939 940 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 941 checkHeaders(t, resp, http.Header{ 942 "Docker-Content-Digest": []string{dgst.String()}, 943 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 944 }) 945 946 var fetchedManifestByDigest schema1.SignedManifest 947 dec = json.NewDecoder(resp.Body) 948 if err := dec.Decode(&fetchedManifestByDigest); err != nil { 949 t.Fatalf("error decoding fetched manifest: %v", err) 950 } 951 952 if !bytes.Equal(fetchedManifestByDigest.Canonical, signedManifest.Canonical) { 953 t.Fatalf("manifests do not match") 954 } 955 956 // check signature was roundtripped 957 signatures, err := fetchedManifestByDigest.Signatures() 958 if err != nil { 959 t.Fatal(err) 960 } 961 962 if len(signatures) != 1 { 963 t.Fatalf("expected 1 signature from manifest, got: %d", len(signatures)) 964 } 965 966 // Re-sign, push and pull the same digest 967 sm2, err := schema1.Sign(&fetchedManifestByDigest.Manifest, env.pk) 968 if err != nil { 969 t.Fatal(err) 970 971 } 972 973 // Re-push with a few different Content-Types. The official schema1 974 // content type should work, as should application/json with/without a 975 // charset. 976 resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, schema1.MediaTypeSignedManifest, sm2) 977 checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) 978 resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json; charset=utf-8", sm2) 979 checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) 980 resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json", sm2) 981 checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) 982 983 resp, err = http.Get(manifestDigestURL) 984 checkErr(t, err, "re-fetching manifest by digest") 985 defer resp.Body.Close() 986 987 checkResponse(t, "re-fetching uploaded manifest", resp, http.StatusOK) 988 checkHeaders(t, resp, http.Header{ 989 "Docker-Content-Digest": []string{dgst.String()}, 990 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 991 }) 992 993 dec = json.NewDecoder(resp.Body) 994 if err := dec.Decode(&fetchedManifestByDigest); err != nil { 995 t.Fatalf("error decoding fetched manifest: %v", err) 996 } 997 998 // check two signatures were roundtripped 999 signatures, err = fetchedManifestByDigest.Signatures() 1000 if err != nil { 1001 t.Fatal(err) 1002 } 1003 1004 if len(signatures) != 2 { 1005 t.Fatalf("expected 2 signature from manifest, got: %d", len(signatures)) 1006 } 1007 1008 // Get by name with etag, gives 304 1009 etag := resp.Header.Get("Etag") 1010 req, err := http.NewRequest("GET", manifestURL, nil) 1011 if err != nil { 1012 t.Fatalf("Error constructing request: %s", err) 1013 } 1014 req.Header.Set("If-None-Match", etag) 1015 resp, err = http.DefaultClient.Do(req) 1016 if err != nil { 1017 t.Fatalf("Error constructing request: %s", err) 1018 } 1019 1020 checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) 1021 1022 // Get by digest with etag, gives 304 1023 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1024 if err != nil { 1025 t.Fatalf("Error constructing request: %s", err) 1026 } 1027 req.Header.Set("If-None-Match", etag) 1028 resp, err = http.DefaultClient.Do(req) 1029 if err != nil { 1030 t.Fatalf("Error constructing request: %s", err) 1031 } 1032 1033 checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) 1034 1035 // Ensure that the tag is listed. 1036 resp, err = http.Get(tagsURL) 1037 if err != nil { 1038 t.Fatalf("unexpected error getting unknown tags: %v", err) 1039 } 1040 defer resp.Body.Close() 1041 1042 checkResponse(t, "getting tags", resp, http.StatusOK) 1043 dec = json.NewDecoder(resp.Body) 1044 1045 var tagsResponse tagsAPIResponse 1046 1047 if err := dec.Decode(&tagsResponse); err != nil { 1048 t.Fatalf("unexpected error decoding error response: %v", err) 1049 } 1050 1051 if tagsResponse.Name != imageName.Name() { 1052 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName.Name()) 1053 } 1054 1055 if len(tagsResponse.Tags) != 1 { 1056 t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) 1057 } 1058 1059 if tagsResponse.Tags[0] != tag { 1060 t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) 1061 } 1062 1063 // Attempt to put a manifest with mismatching FSLayer and History array cardinalities 1064 1065 unsignedManifest.History = append(unsignedManifest.History, schema1.History{ 1066 V1Compatibility: "", 1067 }) 1068 invalidSigned, err := schema1.Sign(unsignedManifest, env.pk) 1069 if err != nil { 1070 t.Fatalf("error signing manifest") 1071 } 1072 1073 resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, "", invalidSigned) 1074 checkResponse(t, "putting invalid signed manifest", resp, http.StatusBadRequest) 1075 1076 return args 1077 } 1078 1079 func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { 1080 tag := "schema2tag" 1081 args := manifestArgs{ 1082 imageName: imageName, 1083 mediaType: schema2.MediaTypeManifest, 1084 } 1085 1086 tagRef, _ := reference.WithTag(imageName, tag) 1087 manifestURL, err := env.builder.BuildManifestURL(tagRef) 1088 if err != nil { 1089 t.Fatalf("unexpected error getting manifest url: %v", err) 1090 } 1091 1092 // ----------------------------- 1093 // Attempt to fetch the manifest 1094 resp, err := http.Get(manifestURL) 1095 if err != nil { 1096 t.Fatalf("unexpected error getting manifest: %v", err) 1097 } 1098 defer resp.Body.Close() 1099 1100 checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) 1101 checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) 1102 1103 tagsURL, err := env.builder.BuildTagsURL(imageName) 1104 if err != nil { 1105 t.Fatalf("unexpected error building tags url: %v", err) 1106 } 1107 1108 resp, err = http.Get(tagsURL) 1109 if err != nil { 1110 t.Fatalf("unexpected error getting unknown tags: %v", err) 1111 } 1112 defer resp.Body.Close() 1113 1114 // Check that we get an unknown repository error when asking for tags 1115 checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) 1116 checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) 1117 1118 // -------------------------------- 1119 // Attempt to push manifest with missing config and missing layers 1120 manifest := &schema2.Manifest{ 1121 Versioned: manifest.Versioned{ 1122 SchemaVersion: 2, 1123 MediaType: schema2.MediaTypeManifest, 1124 }, 1125 Config: distribution.Descriptor{ 1126 Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", 1127 Size: 3253, 1128 MediaType: schema2.MediaTypeConfig, 1129 }, 1130 Layers: []distribution.Descriptor{ 1131 { 1132 Digest: "sha256:463434349086340864309863409683460843608348608934092322395278926a", 1133 Size: 6323, 1134 MediaType: schema2.MediaTypeLayer, 1135 }, 1136 { 1137 Digest: "sha256:630923423623623423352523525237238023652897356239852383652aaaaaaa", 1138 Size: 6863, 1139 MediaType: schema2.MediaTypeLayer, 1140 }, 1141 }, 1142 } 1143 1144 resp = putManifest(t, "putting missing config manifest", manifestURL, schema2.MediaTypeManifest, manifest) 1145 defer resp.Body.Close() 1146 checkResponse(t, "putting missing config manifest", resp, http.StatusBadRequest) 1147 _, p, counts := checkBodyHasErrorCodes(t, "putting missing config manifest", resp, v2.ErrorCodeManifestBlobUnknown) 1148 1149 expectedCounts := map[errcode.ErrorCode]int{ 1150 v2.ErrorCodeManifestBlobUnknown: 3, 1151 } 1152 1153 if !reflect.DeepEqual(counts, expectedCounts) { 1154 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1155 } 1156 1157 // Push a config, and reference it in the manifest 1158 sampleConfig := []byte(`{ 1159 "architecture": "amd64", 1160 "history": [ 1161 { 1162 "created": "2015-10-31T22:22:54.690851953Z", 1163 "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" 1164 }, 1165 { 1166 "created": "2015-10-31T22:22:55.613815829Z", 1167 "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]" 1168 } 1169 ], 1170 "rootfs": { 1171 "diff_ids": [ 1172 "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", 1173 "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" 1174 ], 1175 "type": "layers" 1176 } 1177 }`) 1178 sampleConfigDigest := digest.FromBytes(sampleConfig) 1179 1180 uploadURLBase, _ := startPushLayer(t, env.builder, imageName) 1181 pushLayer(t, env.builder, imageName, sampleConfigDigest, uploadURLBase, bytes.NewReader(sampleConfig)) 1182 manifest.Config.Digest = sampleConfigDigest 1183 manifest.Config.Size = int64(len(sampleConfig)) 1184 1185 // The manifest should still be invalid, because its layer doesnt exist 1186 resp = putManifest(t, "putting missing layer manifest", manifestURL, schema2.MediaTypeManifest, manifest) 1187 defer resp.Body.Close() 1188 checkResponse(t, "putting missing layer manifest", resp, http.StatusBadRequest) 1189 _, p, counts = checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestBlobUnknown) 1190 1191 expectedCounts = map[errcode.ErrorCode]int{ 1192 v2.ErrorCodeManifestBlobUnknown: 2, 1193 } 1194 1195 if !reflect.DeepEqual(counts, expectedCounts) { 1196 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1197 } 1198 1199 // Push 2 random layers 1200 expectedLayers := make(map[digest.Digest]io.ReadSeeker) 1201 1202 for i := range manifest.Layers { 1203 rs, dgstStr, err := testutil.CreateRandomTarFile() 1204 1205 if err != nil { 1206 t.Fatalf("error creating random layer %d: %v", i, err) 1207 } 1208 dgst := digest.Digest(dgstStr) 1209 1210 expectedLayers[dgst] = rs 1211 manifest.Layers[i].Digest = dgst 1212 1213 uploadURLBase, _ := startPushLayer(t, env.builder, imageName) 1214 pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) 1215 } 1216 1217 // ------------------- 1218 // Push the manifest with all layers pushed. 1219 deserializedManifest, err := schema2.FromStruct(*manifest) 1220 if err != nil { 1221 t.Fatalf("could not create DeserializedManifest: %v", err) 1222 } 1223 _, canonical, err := deserializedManifest.Payload() 1224 if err != nil { 1225 t.Fatalf("could not get manifest payload: %v", err) 1226 } 1227 dgst := digest.FromBytes(canonical) 1228 args.dgst = dgst 1229 args.manifest = deserializedManifest 1230 1231 digestRef, _ := reference.WithDigest(imageName, dgst) 1232 manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) 1233 checkErr(t, err, "building manifest url") 1234 1235 resp = putManifest(t, "putting manifest no error", manifestURL, schema2.MediaTypeManifest, manifest) 1236 checkResponse(t, "putting manifest no error", resp, http.StatusCreated) 1237 checkHeaders(t, resp, http.Header{ 1238 "Location": []string{manifestDigestURL}, 1239 "Docker-Content-Digest": []string{dgst.String()}, 1240 }) 1241 1242 // -------------------- 1243 // Push by digest -- should get same result 1244 resp = putManifest(t, "putting manifest by digest", manifestDigestURL, schema2.MediaTypeManifest, manifest) 1245 checkResponse(t, "putting manifest by digest", resp, http.StatusCreated) 1246 checkHeaders(t, resp, http.Header{ 1247 "Location": []string{manifestDigestURL}, 1248 "Docker-Content-Digest": []string{dgst.String()}, 1249 }) 1250 1251 // ------------------ 1252 // Fetch by tag name 1253 req, err := http.NewRequest("GET", manifestURL, nil) 1254 if err != nil { 1255 t.Fatalf("Error constructing request: %s", err) 1256 } 1257 req.Header.Set("Accept", schema2.MediaTypeManifest) 1258 resp, err = http.DefaultClient.Do(req) 1259 if err != nil { 1260 t.Fatalf("unexpected error fetching manifest: %v", err) 1261 } 1262 defer resp.Body.Close() 1263 1264 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 1265 checkHeaders(t, resp, http.Header{ 1266 "Docker-Content-Digest": []string{dgst.String()}, 1267 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1268 }) 1269 1270 var fetchedManifest schema2.DeserializedManifest 1271 dec := json.NewDecoder(resp.Body) 1272 1273 if err := dec.Decode(&fetchedManifest); err != nil { 1274 t.Fatalf("error decoding fetched manifest: %v", err) 1275 } 1276 1277 _, fetchedCanonical, err := fetchedManifest.Payload() 1278 if err != nil { 1279 t.Fatalf("error getting manifest payload: %v", err) 1280 } 1281 1282 if !bytes.Equal(fetchedCanonical, canonical) { 1283 t.Fatalf("manifests do not match") 1284 } 1285 1286 // --------------- 1287 // Fetch by digest 1288 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1289 if err != nil { 1290 t.Fatalf("Error constructing request: %s", err) 1291 } 1292 req.Header.Set("Accept", schema2.MediaTypeManifest) 1293 resp, err = http.DefaultClient.Do(req) 1294 checkErr(t, err, "fetching manifest by digest") 1295 defer resp.Body.Close() 1296 1297 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 1298 checkHeaders(t, resp, http.Header{ 1299 "Docker-Content-Digest": []string{dgst.String()}, 1300 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1301 }) 1302 1303 var fetchedManifestByDigest schema2.DeserializedManifest 1304 dec = json.NewDecoder(resp.Body) 1305 if err := dec.Decode(&fetchedManifestByDigest); err != nil { 1306 t.Fatalf("error decoding fetched manifest: %v", err) 1307 } 1308 1309 _, fetchedCanonical, err = fetchedManifest.Payload() 1310 if err != nil { 1311 t.Fatalf("error getting manifest payload: %v", err) 1312 } 1313 1314 if !bytes.Equal(fetchedCanonical, canonical) { 1315 t.Fatalf("manifests do not match") 1316 } 1317 1318 // Get by name with etag, gives 304 1319 etag := resp.Header.Get("Etag") 1320 req, err = http.NewRequest("GET", manifestURL, nil) 1321 if err != nil { 1322 t.Fatalf("Error constructing request: %s", err) 1323 } 1324 req.Header.Set("If-None-Match", etag) 1325 resp, err = http.DefaultClient.Do(req) 1326 if err != nil { 1327 t.Fatalf("Error constructing request: %s", err) 1328 } 1329 1330 checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) 1331 1332 // Get by digest with etag, gives 304 1333 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1334 if err != nil { 1335 t.Fatalf("Error constructing request: %s", err) 1336 } 1337 req.Header.Set("If-None-Match", etag) 1338 resp, err = http.DefaultClient.Do(req) 1339 if err != nil { 1340 t.Fatalf("Error constructing request: %s", err) 1341 } 1342 1343 checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) 1344 1345 // Ensure that the tag is listed. 1346 resp, err = http.Get(tagsURL) 1347 if err != nil { 1348 t.Fatalf("unexpected error getting unknown tags: %v", err) 1349 } 1350 defer resp.Body.Close() 1351 1352 checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK) 1353 dec = json.NewDecoder(resp.Body) 1354 1355 var tagsResponse tagsAPIResponse 1356 1357 if err := dec.Decode(&tagsResponse); err != nil { 1358 t.Fatalf("unexpected error decoding error response: %v", err) 1359 } 1360 1361 if tagsResponse.Name != imageName.Name() { 1362 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) 1363 } 1364 1365 if len(tagsResponse.Tags) != 1 { 1366 t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) 1367 } 1368 1369 if tagsResponse.Tags[0] != tag { 1370 t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) 1371 } 1372 1373 // ------------------ 1374 // Fetch as a schema1 manifest 1375 resp, err = http.Get(manifestURL) 1376 if err != nil { 1377 t.Fatalf("unexpected error fetching manifest as schema1: %v", err) 1378 } 1379 defer resp.Body.Close() 1380 1381 manifestBytes, err := ioutil.ReadAll(resp.Body) 1382 if err != nil { 1383 t.Fatalf("error reading response body: %v", err) 1384 } 1385 1386 checkResponse(t, "fetching uploaded manifest as schema1", resp, http.StatusOK) 1387 1388 m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes) 1389 if err != nil { 1390 t.Fatalf("unexpected error unmarshalling manifest: %v", err) 1391 } 1392 1393 fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest) 1394 if !ok { 1395 t.Fatalf("expecting schema1 manifest") 1396 } 1397 1398 checkHeaders(t, resp, http.Header{ 1399 "Docker-Content-Digest": []string{desc.Digest.String()}, 1400 "ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)}, 1401 }) 1402 1403 if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { 1404 t.Fatal("wrong schema version") 1405 } 1406 if fetchedSchema1Manifest.Architecture != "amd64" { 1407 t.Fatal("wrong architecture") 1408 } 1409 if fetchedSchema1Manifest.Name != imageName.Name() { 1410 t.Fatal("wrong image name") 1411 } 1412 if fetchedSchema1Manifest.Tag != tag { 1413 t.Fatal("wrong tag") 1414 } 1415 if len(fetchedSchema1Manifest.FSLayers) != 2 { 1416 t.Fatal("wrong number of FSLayers") 1417 } 1418 for i := range manifest.Layers { 1419 if fetchedSchema1Manifest.FSLayers[i].BlobSum != manifest.Layers[len(manifest.Layers)-i-1].Digest { 1420 t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) 1421 } 1422 } 1423 if len(fetchedSchema1Manifest.History) != 2 { 1424 t.Fatal("wrong number of History entries") 1425 } 1426 1427 // Don't check V1Compatibility fields becuase we're using randomly-generated 1428 // layers. 1429 1430 return args 1431 } 1432 1433 func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs) { 1434 imageName := args.imageName 1435 tag := "manifestlisttag" 1436 1437 tagRef, _ := reference.WithTag(imageName, tag) 1438 manifestURL, err := env.builder.BuildManifestURL(tagRef) 1439 if err != nil { 1440 t.Fatalf("unexpected error getting manifest url: %v", err) 1441 } 1442 1443 // -------------------------------- 1444 // Attempt to push manifest list that refers to an unknown manifest 1445 manifestList := &manifestlist.ManifestList{ 1446 Versioned: manifest.Versioned{ 1447 SchemaVersion: 2, 1448 MediaType: manifestlist.MediaTypeManifestList, 1449 }, 1450 Manifests: []manifestlist.ManifestDescriptor{ 1451 { 1452 Descriptor: distribution.Descriptor{ 1453 Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", 1454 Size: 3253, 1455 MediaType: schema2.MediaTypeManifest, 1456 }, 1457 Platform: manifestlist.PlatformSpec{ 1458 Architecture: "amd64", 1459 OS: "linux", 1460 }, 1461 }, 1462 }, 1463 } 1464 1465 resp := putManifest(t, "putting missing manifest manifestlist", manifestURL, manifestlist.MediaTypeManifestList, manifestList) 1466 defer resp.Body.Close() 1467 checkResponse(t, "putting missing manifest manifestlist", resp, http.StatusBadRequest) 1468 _, p, counts := checkBodyHasErrorCodes(t, "putting missing manifest manifestlist", resp, v2.ErrorCodeManifestBlobUnknown) 1469 1470 expectedCounts := map[errcode.ErrorCode]int{ 1471 v2.ErrorCodeManifestBlobUnknown: 1, 1472 } 1473 1474 if !reflect.DeepEqual(counts, expectedCounts) { 1475 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1476 } 1477 1478 // ------------------- 1479 // Push a manifest list that references an actual manifest 1480 manifestList.Manifests[0].Digest = args.dgst 1481 deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests) 1482 if err != nil { 1483 t.Fatalf("could not create DeserializedManifestList: %v", err) 1484 } 1485 _, canonical, err := deserializedManifestList.Payload() 1486 if err != nil { 1487 t.Fatalf("could not get manifest list payload: %v", err) 1488 } 1489 dgst := digest.FromBytes(canonical) 1490 1491 digestRef, _ := reference.WithDigest(imageName, dgst) 1492 manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) 1493 checkErr(t, err, "building manifest url") 1494 1495 resp = putManifest(t, "putting manifest list no error", manifestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) 1496 checkResponse(t, "putting manifest list no error", resp, http.StatusCreated) 1497 checkHeaders(t, resp, http.Header{ 1498 "Location": []string{manifestDigestURL}, 1499 "Docker-Content-Digest": []string{dgst.String()}, 1500 }) 1501 1502 // -------------------- 1503 // Push by digest -- should get same result 1504 resp = putManifest(t, "putting manifest list by digest", manifestDigestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) 1505 checkResponse(t, "putting manifest list by digest", resp, http.StatusCreated) 1506 checkHeaders(t, resp, http.Header{ 1507 "Location": []string{manifestDigestURL}, 1508 "Docker-Content-Digest": []string{dgst.String()}, 1509 }) 1510 1511 // ------------------ 1512 // Fetch by tag name 1513 req, err := http.NewRequest("GET", manifestURL, nil) 1514 if err != nil { 1515 t.Fatalf("Error constructing request: %s", err) 1516 } 1517 req.Header.Set("Accept", manifestlist.MediaTypeManifestList) 1518 req.Header.Add("Accept", schema1.MediaTypeSignedManifest) 1519 req.Header.Add("Accept", schema2.MediaTypeManifest) 1520 resp, err = http.DefaultClient.Do(req) 1521 if err != nil { 1522 t.Fatalf("unexpected error fetching manifest list: %v", err) 1523 } 1524 defer resp.Body.Close() 1525 1526 checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) 1527 checkHeaders(t, resp, http.Header{ 1528 "Docker-Content-Digest": []string{dgst.String()}, 1529 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1530 }) 1531 1532 var fetchedManifestList manifestlist.DeserializedManifestList 1533 dec := json.NewDecoder(resp.Body) 1534 1535 if err := dec.Decode(&fetchedManifestList); err != nil { 1536 t.Fatalf("error decoding fetched manifest list: %v", err) 1537 } 1538 1539 _, fetchedCanonical, err := fetchedManifestList.Payload() 1540 if err != nil { 1541 t.Fatalf("error getting manifest list payload: %v", err) 1542 } 1543 1544 if !bytes.Equal(fetchedCanonical, canonical) { 1545 t.Fatalf("manifest lists do not match") 1546 } 1547 1548 // --------------- 1549 // Fetch by digest 1550 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1551 if err != nil { 1552 t.Fatalf("Error constructing request: %s", err) 1553 } 1554 req.Header.Set("Accept", manifestlist.MediaTypeManifestList) 1555 resp, err = http.DefaultClient.Do(req) 1556 checkErr(t, err, "fetching manifest list by digest") 1557 defer resp.Body.Close() 1558 1559 checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) 1560 checkHeaders(t, resp, http.Header{ 1561 "Docker-Content-Digest": []string{dgst.String()}, 1562 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1563 }) 1564 1565 var fetchedManifestListByDigest manifestlist.DeserializedManifestList 1566 dec = json.NewDecoder(resp.Body) 1567 if err := dec.Decode(&fetchedManifestListByDigest); err != nil { 1568 t.Fatalf("error decoding fetched manifest: %v", err) 1569 } 1570 1571 _, fetchedCanonical, err = fetchedManifestListByDigest.Payload() 1572 if err != nil { 1573 t.Fatalf("error getting manifest list payload: %v", err) 1574 } 1575 1576 if !bytes.Equal(fetchedCanonical, canonical) { 1577 t.Fatalf("manifests do not match") 1578 } 1579 1580 // Get by name with etag, gives 304 1581 etag := resp.Header.Get("Etag") 1582 req, err = http.NewRequest("GET", manifestURL, nil) 1583 if err != nil { 1584 t.Fatalf("Error constructing request: %s", err) 1585 } 1586 req.Header.Set("If-None-Match", etag) 1587 resp, err = http.DefaultClient.Do(req) 1588 if err != nil { 1589 t.Fatalf("Error constructing request: %s", err) 1590 } 1591 1592 checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) 1593 1594 // Get by digest with etag, gives 304 1595 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1596 if err != nil { 1597 t.Fatalf("Error constructing request: %s", err) 1598 } 1599 req.Header.Set("If-None-Match", etag) 1600 resp, err = http.DefaultClient.Do(req) 1601 if err != nil { 1602 t.Fatalf("Error constructing request: %s", err) 1603 } 1604 1605 checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) 1606 1607 // ------------------ 1608 // Fetch as a schema1 manifest 1609 resp, err = http.Get(manifestURL) 1610 if err != nil { 1611 t.Fatalf("unexpected error fetching manifest list as schema1: %v", err) 1612 } 1613 defer resp.Body.Close() 1614 1615 manifestBytes, err := ioutil.ReadAll(resp.Body) 1616 if err != nil { 1617 t.Fatalf("error reading response body: %v", err) 1618 } 1619 1620 checkResponse(t, "fetching uploaded manifest list as schema1", resp, http.StatusOK) 1621 1622 m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes) 1623 if err != nil { 1624 t.Fatalf("unexpected error unmarshalling manifest: %v", err) 1625 } 1626 1627 fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest) 1628 if !ok { 1629 t.Fatalf("expecting schema1 manifest") 1630 } 1631 1632 checkHeaders(t, resp, http.Header{ 1633 "Docker-Content-Digest": []string{desc.Digest.String()}, 1634 "ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)}, 1635 }) 1636 1637 if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { 1638 t.Fatal("wrong schema version") 1639 } 1640 if fetchedSchema1Manifest.Architecture != "amd64" { 1641 t.Fatal("wrong architecture") 1642 } 1643 if fetchedSchema1Manifest.Name != imageName.Name() { 1644 t.Fatal("wrong image name") 1645 } 1646 if fetchedSchema1Manifest.Tag != tag { 1647 t.Fatal("wrong tag") 1648 } 1649 if len(fetchedSchema1Manifest.FSLayers) != 2 { 1650 t.Fatal("wrong number of FSLayers") 1651 } 1652 layers := args.manifest.(*schema2.DeserializedManifest).Layers 1653 for i := range layers { 1654 if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(layers)-i-1].Digest { 1655 t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) 1656 } 1657 } 1658 if len(fetchedSchema1Manifest.History) != 2 { 1659 t.Fatal("wrong number of History entries") 1660 } 1661 1662 // Don't check V1Compatibility fields becuase we're using randomly-generated 1663 // layers. 1664 } 1665 1666 func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { 1667 imageName := args.imageName 1668 dgst := args.dgst 1669 manifest := args.manifest 1670 1671 ref, _ := reference.WithDigest(imageName, dgst) 1672 manifestDigestURL, err := env.builder.BuildManifestURL(ref) 1673 // --------------- 1674 // Delete by digest 1675 resp, err := httpDelete(manifestDigestURL) 1676 checkErr(t, err, "deleting manifest by digest") 1677 1678 checkResponse(t, "deleting manifest", resp, http.StatusAccepted) 1679 checkHeaders(t, resp, http.Header{ 1680 "Content-Length": []string{"0"}, 1681 }) 1682 1683 // --------------- 1684 // Attempt to fetch deleted manifest 1685 resp, err = http.Get(manifestDigestURL) 1686 checkErr(t, err, "fetching deleted manifest by digest") 1687 defer resp.Body.Close() 1688 1689 checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound) 1690 1691 // --------------- 1692 // Delete already deleted manifest by digest 1693 resp, err = httpDelete(manifestDigestURL) 1694 checkErr(t, err, "re-deleting manifest by digest") 1695 1696 checkResponse(t, "re-deleting manifest", resp, http.StatusNotFound) 1697 1698 // -------------------- 1699 // Re-upload manifest by digest 1700 resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest) 1701 checkResponse(t, "putting manifest", resp, http.StatusCreated) 1702 checkHeaders(t, resp, http.Header{ 1703 "Location": []string{manifestDigestURL}, 1704 "Docker-Content-Digest": []string{dgst.String()}, 1705 }) 1706 1707 // --------------- 1708 // Attempt to fetch re-uploaded deleted digest 1709 resp, err = http.Get(manifestDigestURL) 1710 checkErr(t, err, "fetching re-uploaded manifest by digest") 1711 defer resp.Body.Close() 1712 1713 checkResponse(t, "fetching re-uploaded manifest", resp, http.StatusOK) 1714 checkHeaders(t, resp, http.Header{ 1715 "Docker-Content-Digest": []string{dgst.String()}, 1716 }) 1717 1718 // --------------- 1719 // Attempt to delete an unknown manifest 1720 unknownDigest := digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") 1721 unknownRef, _ := reference.WithDigest(imageName, unknownDigest) 1722 unknownManifestDigestURL, err := env.builder.BuildManifestURL(unknownRef) 1723 checkErr(t, err, "building unknown manifest url") 1724 1725 resp, err = httpDelete(unknownManifestDigestURL) 1726 checkErr(t, err, "delting unknown manifest by digest") 1727 checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound) 1728 1729 // -------------------- 1730 // Upload manifest by tag 1731 tag := "atag" 1732 tagRef, _ := reference.WithTag(imageName, tag) 1733 manifestTagURL, err := env.builder.BuildManifestURL(tagRef) 1734 resp = putManifest(t, "putting manifest by tag", manifestTagURL, args.mediaType, manifest) 1735 checkResponse(t, "putting manifest by tag", resp, http.StatusCreated) 1736 checkHeaders(t, resp, http.Header{ 1737 "Location": []string{manifestDigestURL}, 1738 "Docker-Content-Digest": []string{dgst.String()}, 1739 }) 1740 1741 tagsURL, err := env.builder.BuildTagsURL(imageName) 1742 if err != nil { 1743 t.Fatalf("unexpected error building tags url: %v", err) 1744 } 1745 1746 // Ensure that the tag is listed. 1747 resp, err = http.Get(tagsURL) 1748 if err != nil { 1749 t.Fatalf("unexpected error getting unknown tags: %v", err) 1750 } 1751 defer resp.Body.Close() 1752 1753 dec := json.NewDecoder(resp.Body) 1754 var tagsResponse tagsAPIResponse 1755 if err := dec.Decode(&tagsResponse); err != nil { 1756 t.Fatalf("unexpected error decoding error response: %v", err) 1757 } 1758 1759 if tagsResponse.Name != imageName.Name() { 1760 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) 1761 } 1762 1763 if len(tagsResponse.Tags) != 1 { 1764 t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) 1765 } 1766 1767 if tagsResponse.Tags[0] != tag { 1768 t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) 1769 } 1770 1771 // --------------- 1772 // Delete by digest 1773 resp, err = httpDelete(manifestDigestURL) 1774 checkErr(t, err, "deleting manifest by digest") 1775 1776 checkResponse(t, "deleting manifest with tag", resp, http.StatusAccepted) 1777 checkHeaders(t, resp, http.Header{ 1778 "Content-Length": []string{"0"}, 1779 }) 1780 1781 // Ensure that the tag is not listed. 1782 resp, err = http.Get(tagsURL) 1783 if err != nil { 1784 t.Fatalf("unexpected error getting unknown tags: %v", err) 1785 } 1786 defer resp.Body.Close() 1787 1788 dec = json.NewDecoder(resp.Body) 1789 if err := dec.Decode(&tagsResponse); err != nil { 1790 t.Fatalf("unexpected error decoding error response: %v", err) 1791 } 1792 1793 if tagsResponse.Name != imageName.Name() { 1794 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) 1795 } 1796 1797 if len(tagsResponse.Tags) != 0 { 1798 t.Fatalf("expected 0 tags in response: %v", tagsResponse.Tags) 1799 } 1800 1801 } 1802 1803 type testEnv struct { 1804 pk libtrust.PrivateKey 1805 ctx context.Context 1806 config configuration.Configuration 1807 app *App 1808 server *httptest.Server 1809 builder *v2.URLBuilder 1810 } 1811 1812 func newTestEnvMirror(t *testing.T, deleteEnabled bool) *testEnv { 1813 config := configuration.Configuration{ 1814 Storage: configuration.Storage{ 1815 "inmemory": configuration.Parameters{}, 1816 "delete": configuration.Parameters{"enabled": deleteEnabled}, 1817 }, 1818 Proxy: configuration.Proxy{ 1819 RemoteURL: "http://example.com", 1820 }, 1821 } 1822 1823 return newTestEnvWithConfig(t, &config) 1824 1825 } 1826 1827 func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv { 1828 config := configuration.Configuration{ 1829 Storage: configuration.Storage{ 1830 "inmemory": configuration.Parameters{}, 1831 "delete": configuration.Parameters{"enabled": deleteEnabled}, 1832 }, 1833 } 1834 1835 config.HTTP.Headers = headerConfig 1836 1837 return newTestEnvWithConfig(t, &config) 1838 } 1839 1840 func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv { 1841 ctx := context.Background() 1842 1843 app := NewApp(ctx, config) 1844 server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) 1845 builder, err := v2.NewURLBuilderFromString(server.URL + config.HTTP.Prefix) 1846 1847 if err != nil { 1848 t.Fatalf("error creating url builder: %v", err) 1849 } 1850 1851 pk, err := libtrust.GenerateECP256PrivateKey() 1852 if err != nil { 1853 t.Fatalf("unexpected error generating private key: %v", err) 1854 } 1855 1856 return &testEnv{ 1857 pk: pk, 1858 ctx: ctx, 1859 config: *config, 1860 app: app, 1861 server: server, 1862 builder: builder, 1863 } 1864 } 1865 1866 func putManifest(t *testing.T, msg, url, contentType string, v interface{}) *http.Response { 1867 var body []byte 1868 1869 switch m := v.(type) { 1870 case *schema1.SignedManifest: 1871 _, pl, err := m.Payload() 1872 if err != nil { 1873 t.Fatalf("error getting payload: %v", err) 1874 } 1875 body = pl 1876 case *manifestlist.DeserializedManifestList: 1877 _, pl, err := m.Payload() 1878 if err != nil { 1879 t.Fatalf("error getting payload: %v", err) 1880 } 1881 body = pl 1882 default: 1883 var err error 1884 body, err = json.MarshalIndent(v, "", " ") 1885 if err != nil { 1886 t.Fatalf("unexpected error marshaling %v: %v", v, err) 1887 } 1888 } 1889 1890 req, err := http.NewRequest("PUT", url, bytes.NewReader(body)) 1891 if err != nil { 1892 t.Fatalf("error creating request for %s: %v", msg, err) 1893 } 1894 1895 if contentType != "" { 1896 req.Header.Set("Content-Type", contentType) 1897 } 1898 1899 resp, err := http.DefaultClient.Do(req) 1900 if err != nil { 1901 t.Fatalf("error doing put request while %s: %v", msg, err) 1902 } 1903 1904 return resp 1905 } 1906 1907 func startPushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named) (location string, uuid string) { 1908 layerUploadURL, err := ub.BuildBlobUploadURL(name) 1909 if err != nil { 1910 t.Fatalf("unexpected error building layer upload url: %v", err) 1911 } 1912 1913 resp, err := http.Post(layerUploadURL, "", nil) 1914 if err != nil { 1915 t.Fatalf("unexpected error starting layer push: %v", err) 1916 } 1917 defer resp.Body.Close() 1918 1919 checkResponse(t, fmt.Sprintf("pushing starting layer push %v", name.String()), resp, http.StatusAccepted) 1920 1921 u, err := url.Parse(resp.Header.Get("Location")) 1922 if err != nil { 1923 t.Fatalf("error parsing location header: %v", err) 1924 } 1925 1926 uuid = path.Base(u.Path) 1927 checkHeaders(t, resp, http.Header{ 1928 "Location": []string{"*"}, 1929 "Content-Length": []string{"0"}, 1930 "Docker-Upload-UUID": []string{uuid}, 1931 }) 1932 1933 return resp.Header.Get("Location"), uuid 1934 } 1935 1936 // doPushLayer pushes the layer content returning the url on success returning 1937 // the response. If you're only expecting a successful response, use pushLayer. 1938 func doPushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) (*http.Response, error) { 1939 u, err := url.Parse(uploadURLBase) 1940 if err != nil { 1941 t.Fatalf("unexpected error parsing pushLayer url: %v", err) 1942 } 1943 1944 u.RawQuery = url.Values{ 1945 "_state": u.Query()["_state"], 1946 1947 "digest": []string{dgst.String()}, 1948 }.Encode() 1949 1950 uploadURL := u.String() 1951 1952 // Just do a monolithic upload 1953 req, err := http.NewRequest("PUT", uploadURL, body) 1954 if err != nil { 1955 t.Fatalf("unexpected error creating new request: %v", err) 1956 } 1957 1958 return http.DefaultClient.Do(req) 1959 } 1960 1961 // pushLayer pushes the layer content returning the url on success. 1962 func pushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) string { 1963 digester := digest.Canonical.New() 1964 1965 resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, io.TeeReader(body, digester.Hash())) 1966 if err != nil { 1967 t.Fatalf("unexpected error doing push layer request: %v", err) 1968 } 1969 defer resp.Body.Close() 1970 1971 checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) 1972 1973 if err != nil { 1974 t.Fatalf("error generating sha256 digest of body") 1975 } 1976 1977 sha256Dgst := digester.Digest() 1978 1979 ref, _ := reference.WithDigest(name, sha256Dgst) 1980 expectedLayerURL, err := ub.BuildBlobURL(ref) 1981 if err != nil { 1982 t.Fatalf("error building expected layer url: %v", err) 1983 } 1984 1985 checkHeaders(t, resp, http.Header{ 1986 "Location": []string{expectedLayerURL}, 1987 "Content-Length": []string{"0"}, 1988 "Docker-Content-Digest": []string{sha256Dgst.String()}, 1989 }) 1990 1991 return resp.Header.Get("Location") 1992 } 1993 1994 func finishUpload(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, dgst digest.Digest) string { 1995 resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, nil) 1996 if err != nil { 1997 t.Fatalf("unexpected error doing push layer request: %v", err) 1998 } 1999 defer resp.Body.Close() 2000 2001 checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) 2002 2003 ref, _ := reference.WithDigest(name, dgst) 2004 expectedLayerURL, err := ub.BuildBlobURL(ref) 2005 if err != nil { 2006 t.Fatalf("error building expected layer url: %v", err) 2007 } 2008 2009 checkHeaders(t, resp, http.Header{ 2010 "Location": []string{expectedLayerURL}, 2011 "Content-Length": []string{"0"}, 2012 "Docker-Content-Digest": []string{dgst.String()}, 2013 }) 2014 2015 return resp.Header.Get("Location") 2016 } 2017 2018 func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) { 2019 u, err := url.Parse(uploadURLBase) 2020 if err != nil { 2021 t.Fatalf("unexpected error parsing pushLayer url: %v", err) 2022 } 2023 2024 u.RawQuery = url.Values{ 2025 "_state": u.Query()["_state"], 2026 }.Encode() 2027 2028 uploadURL := u.String() 2029 2030 digester := digest.Canonical.New() 2031 2032 req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash())) 2033 if err != nil { 2034 t.Fatalf("unexpected error creating new request: %v", err) 2035 } 2036 req.Header.Set("Content-Type", "application/octet-stream") 2037 2038 resp, err := http.DefaultClient.Do(req) 2039 2040 return resp, digester.Digest(), err 2041 } 2042 2043 func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) { 2044 resp, dgst, err := doPushChunk(t, uploadURLBase, body) 2045 if err != nil { 2046 t.Fatalf("unexpected error doing push layer request: %v", err) 2047 } 2048 defer resp.Body.Close() 2049 2050 checkResponse(t, "putting chunk", resp, http.StatusAccepted) 2051 2052 if err != nil { 2053 t.Fatalf("error generating sha256 digest of body") 2054 } 2055 2056 checkHeaders(t, resp, http.Header{ 2057 "Range": []string{fmt.Sprintf("0-%d", length-1)}, 2058 "Content-Length": []string{"0"}, 2059 }) 2060 2061 return resp.Header.Get("Location"), dgst 2062 } 2063 2064 func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) { 2065 if resp.StatusCode != expectedStatus { 2066 t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus) 2067 maybeDumpResponse(t, resp) 2068 2069 t.FailNow() 2070 } 2071 2072 // We expect the headers included in the configuration, unless the 2073 // status code is 405 (Method Not Allowed), which means the handler 2074 // doesn't even get called. 2075 if resp.StatusCode != 405 && !reflect.DeepEqual(resp.Header["X-Content-Type-Options"], []string{"nosniff"}) { 2076 t.Logf("missing or incorrect header X-Content-Type-Options %s", msg) 2077 maybeDumpResponse(t, resp) 2078 2079 t.FailNow() 2080 } 2081 } 2082 2083 // checkBodyHasErrorCodes ensures the body is an error body and has the 2084 // expected error codes, returning the error structure, the json slice and a 2085 // count of the errors by code. 2086 func checkBodyHasErrorCodes(t *testing.T, msg string, resp *http.Response, errorCodes ...errcode.ErrorCode) (errcode.Errors, []byte, map[errcode.ErrorCode]int) { 2087 p, err := ioutil.ReadAll(resp.Body) 2088 if err != nil { 2089 t.Fatalf("unexpected error reading body %s: %v", msg, err) 2090 } 2091 2092 var errs errcode.Errors 2093 if err := json.Unmarshal(p, &errs); err != nil { 2094 t.Fatalf("unexpected error decoding error response: %v", err) 2095 } 2096 2097 if len(errs) == 0 { 2098 t.Fatalf("expected errors in response") 2099 } 2100 2101 // TODO(stevvooe): Shoot. The error setup is not working out. The content- 2102 // type headers are being set after writing the status code. 2103 // if resp.Header.Get("Content-Type") != "application/json; charset=utf-8" { 2104 // t.Fatalf("unexpected content type: %v != 'application/json'", 2105 // resp.Header.Get("Content-Type")) 2106 // } 2107 2108 expected := map[errcode.ErrorCode]struct{}{} 2109 counts := map[errcode.ErrorCode]int{} 2110 2111 // Initialize map with zeros for expected 2112 for _, code := range errorCodes { 2113 expected[code] = struct{}{} 2114 counts[code] = 0 2115 } 2116 2117 for _, e := range errs { 2118 err, ok := e.(errcode.ErrorCoder) 2119 if !ok { 2120 t.Fatalf("not an ErrorCoder: %#v", e) 2121 } 2122 if _, ok := expected[err.ErrorCode()]; !ok { 2123 t.Fatalf("unexpected error code %v encountered during %s: %s ", err.ErrorCode(), msg, string(p)) 2124 } 2125 counts[err.ErrorCode()]++ 2126 } 2127 2128 // Ensure that counts of expected errors were all non-zero 2129 for code := range expected { 2130 if counts[code] == 0 { 2131 t.Fatalf("expected error code %v not encounterd during %s: %s", code, msg, string(p)) 2132 } 2133 } 2134 2135 return errs, p, counts 2136 } 2137 2138 func maybeDumpResponse(t *testing.T, resp *http.Response) { 2139 if d, err := httputil.DumpResponse(resp, true); err != nil { 2140 t.Logf("error dumping response: %v", err) 2141 } else { 2142 t.Logf("response:\n%s", string(d)) 2143 } 2144 } 2145 2146 // matchHeaders checks that the response has at least the headers. If not, the 2147 // test will fail. If a passed in header value is "*", any non-zero value will 2148 // suffice as a match. 2149 func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) { 2150 for k, vs := range headers { 2151 if resp.Header.Get(k) == "" { 2152 t.Fatalf("response missing header %q", k) 2153 } 2154 2155 for _, v := range vs { 2156 if v == "*" { 2157 // Just ensure there is some value. 2158 if len(resp.Header[http.CanonicalHeaderKey(k)]) > 0 { 2159 continue 2160 } 2161 } 2162 2163 for _, hv := range resp.Header[http.CanonicalHeaderKey(k)] { 2164 if hv != v { 2165 t.Fatalf("%+v %v header value not matched in response: %q != %q", resp.Header, k, hv, v) 2166 } 2167 } 2168 } 2169 } 2170 } 2171 2172 func checkErr(t *testing.T, err error, msg string) { 2173 if err != nil { 2174 t.Fatalf("unexpected error %s: %v", msg, err) 2175 } 2176 } 2177 2178 func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest { 2179 imageNameRef, err := reference.ParseNamed(imageName) 2180 if err != nil { 2181 t.Fatalf("unable to parse reference: %v", err) 2182 } 2183 2184 unsignedManifest := &schema1.Manifest{ 2185 Versioned: manifest.Versioned{ 2186 SchemaVersion: 1, 2187 }, 2188 Name: imageName, 2189 Tag: tag, 2190 FSLayers: []schema1.FSLayer{ 2191 { 2192 BlobSum: "asdf", 2193 }, 2194 }, 2195 History: []schema1.History{ 2196 { 2197 V1Compatibility: "", 2198 }, 2199 }, 2200 } 2201 2202 // Push 2 random layers 2203 expectedLayers := make(map[digest.Digest]io.ReadSeeker) 2204 2205 for i := range unsignedManifest.FSLayers { 2206 rs, dgstStr, err := testutil.CreateRandomTarFile() 2207 if err != nil { 2208 t.Fatalf("error creating random layer %d: %v", i, err) 2209 } 2210 dgst := digest.Digest(dgstStr) 2211 2212 expectedLayers[dgst] = rs 2213 unsignedManifest.FSLayers[i].BlobSum = dgst 2214 2215 uploadURLBase, _ := startPushLayer(t, env.builder, imageNameRef) 2216 pushLayer(t, env.builder, imageNameRef, dgst, uploadURLBase, rs) 2217 } 2218 2219 signedManifest, err := schema1.Sign(unsignedManifest, env.pk) 2220 if err != nil { 2221 t.Fatalf("unexpected error signing manifest: %v", err) 2222 } 2223 2224 dgst := digest.FromBytes(signedManifest.Canonical) 2225 2226 // Create this repository by tag to ensure the tag mapping is made in the registry 2227 tagRef, _ := reference.WithTag(imageNameRef, tag) 2228 manifestDigestURL, err := env.builder.BuildManifestURL(tagRef) 2229 checkErr(t, err, "building manifest url") 2230 2231 digestRef, _ := reference.WithDigest(imageNameRef, dgst) 2232 location, err := env.builder.BuildManifestURL(digestRef) 2233 checkErr(t, err, "building location URL") 2234 2235 resp := putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) 2236 checkResponse(t, "putting signed manifest", resp, http.StatusCreated) 2237 checkHeaders(t, resp, http.Header{ 2238 "Location": []string{location}, 2239 "Docker-Content-Digest": []string{dgst.String()}, 2240 }) 2241 return dgst 2242 } 2243 2244 // Test mutation operations on a registry configured as a cache. Ensure that they return 2245 // appropriate errors. 2246 func TestRegistryAsCacheMutationAPIs(t *testing.T) { 2247 deleteEnabled := true 2248 env := newTestEnvMirror(t, deleteEnabled) 2249 2250 imageName, _ := reference.ParseNamed("foo/bar") 2251 tag := "latest" 2252 tagRef, _ := reference.WithTag(imageName, tag) 2253 manifestURL, err := env.builder.BuildManifestURL(tagRef) 2254 if err != nil { 2255 t.Fatalf("unexpected error building base url: %v", err) 2256 } 2257 2258 // Manifest upload 2259 m := &schema1.Manifest{ 2260 Versioned: manifest.Versioned{ 2261 SchemaVersion: 1, 2262 }, 2263 Name: imageName.Name(), 2264 Tag: tag, 2265 FSLayers: []schema1.FSLayer{}, 2266 History: []schema1.History{}, 2267 } 2268 2269 sm, err := schema1.Sign(m, env.pk) 2270 if err != nil { 2271 t.Fatalf("error signing manifest: %v", err) 2272 } 2273 2274 resp := putManifest(t, "putting unsigned manifest", manifestURL, "", sm) 2275 checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2276 2277 // Manifest Delete 2278 resp, err = httpDelete(manifestURL) 2279 checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2280 2281 // Blob upload initialization 2282 layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) 2283 if err != nil { 2284 t.Fatalf("unexpected error building layer upload url: %v", err) 2285 } 2286 2287 resp, err = http.Post(layerUploadURL, "", nil) 2288 if err != nil { 2289 t.Fatalf("unexpected error starting layer push: %v", err) 2290 } 2291 defer resp.Body.Close() 2292 2293 checkResponse(t, fmt.Sprintf("starting layer push to cache %v", imageName), resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2294 2295 // Blob Delete 2296 ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar) 2297 blobURL, err := env.builder.BuildBlobURL(ref) 2298 resp, err = httpDelete(blobURL) 2299 checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2300 2301 } 2302 2303 // TestCheckContextNotifier makes sure the API endpoints get a ResponseWriter 2304 // that implements http.ContextNotifier. 2305 func TestCheckContextNotifier(t *testing.T) { 2306 env := newTestEnv(t, false) 2307 2308 // Register a new endpoint for testing 2309 env.app.router.Handle("/unittest/{name}/", env.app.dispatcher(func(ctx *Context, r *http.Request) http.Handler { 2310 return handlers.MethodHandler{ 2311 "GET": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2312 if _, ok := w.(http.CloseNotifier); !ok { 2313 t.Fatal("could not cast ResponseWriter to CloseNotifier") 2314 } 2315 w.WriteHeader(200) 2316 }), 2317 } 2318 })) 2319 2320 resp, err := http.Get(env.server.URL + "/unittest/reponame/") 2321 if err != nil { 2322 t.Fatalf("unexpected error issuing request: %v", err) 2323 } 2324 defer resp.Body.Close() 2325 2326 if resp.StatusCode != 200 { 2327 t.Fatalf("wrong status code - expected 200, got %d", resp.StatusCode) 2328 } 2329 } 2330 2331 func TestProxyManifestGetByTag(t *testing.T) { 2332 truthConfig := configuration.Configuration{ 2333 Storage: configuration.Storage{ 2334 "inmemory": configuration.Parameters{}, 2335 }, 2336 } 2337 truthConfig.HTTP.Headers = headerConfig 2338 2339 imageName, _ := reference.ParseNamed("foo/bar") 2340 tag := "latest" 2341 2342 truthEnv := newTestEnvWithConfig(t, &truthConfig) 2343 // create a repository in the truth registry 2344 dgst := createRepository(truthEnv, t, imageName.Name(), tag) 2345 2346 proxyConfig := configuration.Configuration{ 2347 Storage: configuration.Storage{ 2348 "inmemory": configuration.Parameters{}, 2349 }, 2350 Proxy: configuration.Proxy{ 2351 RemoteURL: truthEnv.server.URL, 2352 }, 2353 } 2354 proxyConfig.HTTP.Headers = headerConfig 2355 2356 proxyEnv := newTestEnvWithConfig(t, &proxyConfig) 2357 2358 digestRef, _ := reference.WithDigest(imageName, dgst) 2359 manifestDigestURL, err := proxyEnv.builder.BuildManifestURL(digestRef) 2360 checkErr(t, err, "building manifest url") 2361 2362 resp, err := http.Get(manifestDigestURL) 2363 checkErr(t, err, "fetching manifest from proxy by digest") 2364 defer resp.Body.Close() 2365 2366 tagRef, _ := reference.WithTag(imageName, tag) 2367 manifestTagURL, err := proxyEnv.builder.BuildManifestURL(tagRef) 2368 checkErr(t, err, "building manifest url") 2369 2370 resp, err = http.Get(manifestTagURL) 2371 checkErr(t, err, "fetching manifest from proxy by tag") 2372 defer resp.Body.Close() 2373 checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK) 2374 checkHeaders(t, resp, http.Header{ 2375 "Docker-Content-Digest": []string{dgst.String()}, 2376 }) 2377 2378 // Create another manifest in the remote with the same image/tag pair 2379 newDigest := createRepository(truthEnv, t, imageName.Name(), tag) 2380 if dgst == newDigest { 2381 t.Fatalf("non-random test data") 2382 } 2383 2384 // fetch it with the same proxy URL as before. Ensure the updated content is at the same tag 2385 resp, err = http.Get(manifestTagURL) 2386 checkErr(t, err, "fetching manifest from proxy by tag") 2387 defer resp.Body.Close() 2388 checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK) 2389 checkHeaders(t, resp, http.Header{ 2390 "Docker-Content-Digest": []string{newDigest.String()}, 2391 }) 2392 }