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