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