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} 963 964func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { 965 tag := "thetag" 966 args := manifestArgs{imageName: imageName} 967 968 tagRef, _ := reference.WithTag(imageName, tag) 969 manifestURL, err := env.builder.BuildManifestURL(tagRef) 970 if err != nil { 971 t.Fatalf("unexpected error getting manifest url: %v", err) 972 } 973 974 // ----------------------------- 975 // Attempt to fetch the manifest 976 resp, err := http.Get(manifestURL) 977 if err != nil { 978 t.Fatalf("unexpected error getting manifest: %v", err) 979 } 980 defer resp.Body.Close() 981 982 checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) 983 checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) 984 985 tagsURL, err := env.builder.BuildTagsURL(imageName) 986 if err != nil { 987 t.Fatalf("unexpected error building tags url: %v", err) 988 } 989 990 resp, err = http.Get(tagsURL) 991 if err != nil { 992 t.Fatalf("unexpected error getting unknown tags: %v", err) 993 } 994 defer resp.Body.Close() 995 996 // Check that we get an unknown repository error when asking for tags 997 checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) 998 checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) 999 1000 // -------------------------------- 1001 // Attempt to push unsigned manifest with missing layers 1002 unsignedManifest := &schema1.Manifest{ 1003 Versioned: manifest.Versioned{ 1004 SchemaVersion: 1, 1005 }, 1006 Name: imageName.Name(), 1007 Tag: tag, 1008 FSLayers: []schema1.FSLayer{ 1009 { 1010 BlobSum: "asdf", 1011 }, 1012 { 1013 BlobSum: "qwer", 1014 }, 1015 }, 1016 History: []schema1.History{ 1017 { 1018 V1Compatibility: "", 1019 }, 1020 { 1021 V1Compatibility: "", 1022 }, 1023 }, 1024 } 1025 1026 resp = putManifest(t, "putting unsigned manifest", manifestURL, "", unsignedManifest) 1027 defer resp.Body.Close() 1028 checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest) 1029 _, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp, v2.ErrorCodeManifestInvalid) 1030 1031 expectedCounts := map[errcode.ErrorCode]int{ 1032 v2.ErrorCodeManifestInvalid: 1, 1033 } 1034 1035 if !reflect.DeepEqual(counts, expectedCounts) { 1036 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1037 } 1038 1039 // sign the manifest and still get some interesting errors. 1040 sm, err := schema1.Sign(unsignedManifest, env.pk) 1041 if err != nil { 1042 t.Fatalf("error signing manifest: %v", err) 1043 } 1044 1045 resp = putManifest(t, "putting signed manifest with errors", manifestURL, "", sm) 1046 defer resp.Body.Close() 1047 checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest) 1048 _, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp, 1049 v2.ErrorCodeManifestBlobUnknown, v2.ErrorCodeDigestInvalid) 1050 1051 expectedCounts = map[errcode.ErrorCode]int{ 1052 v2.ErrorCodeManifestBlobUnknown: 2, 1053 v2.ErrorCodeDigestInvalid: 2, 1054 } 1055 1056 if !reflect.DeepEqual(counts, expectedCounts) { 1057 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1058 } 1059 1060 // TODO(stevvooe): Add a test case where we take a mostly valid registry, 1061 // tamper with the content and ensure that we get an unverified manifest 1062 // error. 1063 1064 // Push 2 random layers 1065 expectedLayers := make(map[digest.Digest]io.ReadSeeker) 1066 1067 for i := range unsignedManifest.FSLayers { 1068 rs, dgst, err := testutil.CreateRandomTarFile() 1069 1070 if err != nil { 1071 t.Fatalf("error creating random layer %d: %v", i, err) 1072 } 1073 1074 expectedLayers[dgst] = rs 1075 unsignedManifest.FSLayers[i].BlobSum = dgst 1076 1077 uploadURLBase, _ := startPushLayer(t, env, imageName) 1078 pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) 1079 } 1080 1081 // ------------------- 1082 // Push the signed manifest with all layers pushed. 1083 signedManifest, err := schema1.Sign(unsignedManifest, env.pk) 1084 if err != nil { 1085 t.Fatalf("unexpected error signing manifest: %v", err) 1086 } 1087 1088 dgst := digest.FromBytes(signedManifest.Canonical) 1089 args.manifest = signedManifest 1090 args.dgst = dgst 1091 1092 digestRef, _ := reference.WithDigest(imageName, dgst) 1093 manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) 1094 checkErr(t, err, "building manifest url") 1095 1096 resp = putManifest(t, "putting signed manifest no error", manifestURL, "", signedManifest) 1097 checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated) 1098 checkHeaders(t, resp, http.Header{ 1099 "Location": []string{manifestDigestURL}, 1100 "Docker-Content-Digest": []string{dgst.String()}, 1101 }) 1102 1103 // -------------------- 1104 // Push by digest -- should get same result 1105 resp = putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) 1106 checkResponse(t, "putting signed manifest", resp, http.StatusCreated) 1107 checkHeaders(t, resp, http.Header{ 1108 "Location": []string{manifestDigestURL}, 1109 "Docker-Content-Digest": []string{dgst.String()}, 1110 }) 1111 1112 // ------------------ 1113 // Fetch by tag name 1114 resp, err = http.Get(manifestURL) 1115 if err != nil { 1116 t.Fatalf("unexpected error fetching manifest: %v", err) 1117 } 1118 defer resp.Body.Close() 1119 1120 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 1121 checkHeaders(t, resp, http.Header{ 1122 "Docker-Content-Digest": []string{dgst.String()}, 1123 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1124 }) 1125 1126 var fetchedManifest schema1.SignedManifest 1127 dec := json.NewDecoder(resp.Body) 1128 1129 if err := dec.Decode(&fetchedManifest); err != nil { 1130 t.Fatalf("error decoding fetched manifest: %v", err) 1131 } 1132 1133 if !bytes.Equal(fetchedManifest.Canonical, signedManifest.Canonical) { 1134 t.Fatalf("manifests do not match") 1135 } 1136 1137 // --------------- 1138 // Fetch by digest 1139 resp, err = http.Get(manifestDigestURL) 1140 checkErr(t, err, "fetching manifest by digest") 1141 defer resp.Body.Close() 1142 1143 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 1144 checkHeaders(t, resp, http.Header{ 1145 "Docker-Content-Digest": []string{dgst.String()}, 1146 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1147 }) 1148 1149 var fetchedManifestByDigest schema1.SignedManifest 1150 dec = json.NewDecoder(resp.Body) 1151 if err := dec.Decode(&fetchedManifestByDigest); err != nil { 1152 t.Fatalf("error decoding fetched manifest: %v", err) 1153 } 1154 1155 if !bytes.Equal(fetchedManifestByDigest.Canonical, signedManifest.Canonical) { 1156 t.Fatalf("manifests do not match") 1157 } 1158 1159 // check signature was roundtripped 1160 signatures, err := fetchedManifestByDigest.Signatures() 1161 if err != nil { 1162 t.Fatal(err) 1163 } 1164 1165 if len(signatures) != 1 { 1166 t.Fatalf("expected 1 signature from manifest, got: %d", len(signatures)) 1167 } 1168 1169 // Re-sign, push and pull the same digest 1170 sm2, err := schema1.Sign(&fetchedManifestByDigest.Manifest, env.pk) 1171 if err != nil { 1172 t.Fatal(err) 1173 1174 } 1175 1176 // Re-push with a few different Content-Types. The official schema1 1177 // content type should work, as should application/json with/without a 1178 // charset. 1179 resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, schema1.MediaTypeSignedManifest, sm2) 1180 checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) 1181 resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json; charset=utf-8", sm2) 1182 checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) 1183 resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json", sm2) 1184 checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) 1185 1186 resp, err = http.Get(manifestDigestURL) 1187 checkErr(t, err, "re-fetching manifest by digest") 1188 defer resp.Body.Close() 1189 1190 checkResponse(t, "re-fetching uploaded manifest", resp, http.StatusOK) 1191 checkHeaders(t, resp, http.Header{ 1192 "Docker-Content-Digest": []string{dgst.String()}, 1193 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1194 }) 1195 1196 dec = json.NewDecoder(resp.Body) 1197 if err := dec.Decode(&fetchedManifestByDigest); err != nil { 1198 t.Fatalf("error decoding fetched manifest: %v", err) 1199 } 1200 1201 // check only 1 signature is returned 1202 signatures, err = fetchedManifestByDigest.Signatures() 1203 if err != nil { 1204 t.Fatal(err) 1205 } 1206 1207 if len(signatures) != 1 { 1208 t.Fatalf("expected 2 signature from manifest, got: %d", len(signatures)) 1209 } 1210 1211 // Get by name with etag, gives 304 1212 etag := resp.Header.Get("Etag") 1213 req, err := http.NewRequest("GET", manifestURL, nil) 1214 if err != nil { 1215 t.Fatalf("Error constructing request: %s", err) 1216 } 1217 req.Header.Set("If-None-Match", etag) 1218 resp, err = http.DefaultClient.Do(req) 1219 if err != nil { 1220 t.Fatalf("Error constructing request: %s", err) 1221 } 1222 1223 checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) 1224 1225 // Get by digest with etag, gives 304 1226 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1227 if err != nil { 1228 t.Fatalf("Error constructing request: %s", err) 1229 } 1230 req.Header.Set("If-None-Match", etag) 1231 resp, err = http.DefaultClient.Do(req) 1232 if err != nil { 1233 t.Fatalf("Error constructing request: %s", err) 1234 } 1235 1236 checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) 1237 1238 // Ensure that the tag is listed. 1239 resp, err = http.Get(tagsURL) 1240 if err != nil { 1241 t.Fatalf("unexpected error getting unknown tags: %v", err) 1242 } 1243 defer resp.Body.Close() 1244 1245 checkResponse(t, "getting tags", resp, http.StatusOK) 1246 dec = json.NewDecoder(resp.Body) 1247 1248 var tagsResponse tagsAPIResponse 1249 1250 if err := dec.Decode(&tagsResponse); err != nil { 1251 t.Fatalf("unexpected error decoding error response: %v", err) 1252 } 1253 1254 if tagsResponse.Name != imageName.Name() { 1255 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName.Name()) 1256 } 1257 1258 if len(tagsResponse.Tags) != 1 { 1259 t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) 1260 } 1261 1262 if tagsResponse.Tags[0] != tag { 1263 t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) 1264 } 1265 1266 // Attempt to put a manifest with mismatching FSLayer and History array cardinalities 1267 1268 unsignedManifest.History = append(unsignedManifest.History, schema1.History{ 1269 V1Compatibility: "", 1270 }) 1271 invalidSigned, err := schema1.Sign(unsignedManifest, env.pk) 1272 if err != nil { 1273 t.Fatalf("error signing manifest") 1274 } 1275 1276 resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, "", invalidSigned) 1277 checkResponse(t, "putting invalid signed manifest", resp, http.StatusBadRequest) 1278 1279 return args 1280} 1281 1282func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { 1283 tag := "schema2tag" 1284 args := manifestArgs{ 1285 imageName: imageName, 1286 mediaType: schema2.MediaTypeManifest, 1287 } 1288 1289 tagRef, _ := reference.WithTag(imageName, tag) 1290 manifestURL, err := env.builder.BuildManifestURL(tagRef) 1291 if err != nil { 1292 t.Fatalf("unexpected error getting manifest url: %v", err) 1293 } 1294 1295 // ----------------------------- 1296 // Attempt to fetch the manifest 1297 resp, err := http.Get(manifestURL) 1298 if err != nil { 1299 t.Fatalf("unexpected error getting manifest: %v", err) 1300 } 1301 defer resp.Body.Close() 1302 1303 checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) 1304 checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) 1305 1306 tagsURL, err := env.builder.BuildTagsURL(imageName) 1307 if err != nil { 1308 t.Fatalf("unexpected error building tags url: %v", err) 1309 } 1310 1311 resp, err = http.Get(tagsURL) 1312 if err != nil { 1313 t.Fatalf("unexpected error getting unknown tags: %v", err) 1314 } 1315 defer resp.Body.Close() 1316 1317 // Check that we get an unknown repository error when asking for tags 1318 checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) 1319 checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) 1320 1321 // -------------------------------- 1322 // Attempt to push manifest with missing config and missing layers 1323 manifest := &schema2.Manifest{ 1324 Versioned: manifest.Versioned{ 1325 SchemaVersion: 2, 1326 MediaType: schema2.MediaTypeManifest, 1327 }, 1328 Config: distribution.Descriptor{ 1329 Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", 1330 Size: 3253, 1331 MediaType: schema2.MediaTypeImageConfig, 1332 }, 1333 Layers: []distribution.Descriptor{ 1334 { 1335 Digest: "sha256:463434349086340864309863409683460843608348608934092322395278926a", 1336 Size: 6323, 1337 MediaType: schema2.MediaTypeLayer, 1338 }, 1339 { 1340 Digest: "sha256:630923423623623423352523525237238023652897356239852383652aaaaaaa", 1341 Size: 6863, 1342 MediaType: schema2.MediaTypeLayer, 1343 }, 1344 }, 1345 } 1346 1347 resp = putManifest(t, "putting missing config manifest", manifestURL, schema2.MediaTypeManifest, manifest) 1348 defer resp.Body.Close() 1349 checkResponse(t, "putting missing config manifest", resp, http.StatusBadRequest) 1350 _, p, counts := checkBodyHasErrorCodes(t, "putting missing config manifest", resp, v2.ErrorCodeManifestBlobUnknown) 1351 1352 expectedCounts := map[errcode.ErrorCode]int{ 1353 v2.ErrorCodeManifestBlobUnknown: 3, 1354 } 1355 1356 if !reflect.DeepEqual(counts, expectedCounts) { 1357 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1358 } 1359 1360 // Push a config, and reference it in the manifest 1361 sampleConfig := []byte(`{ 1362 "architecture": "amd64", 1363 "history": [ 1364 { 1365 "created": "2015-10-31T22:22:54.690851953Z", 1366 "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" 1367 }, 1368 { 1369 "created": "2015-10-31T22:22:55.613815829Z", 1370 "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]" 1371 } 1372 ], 1373 "rootfs": { 1374 "diff_ids": [ 1375 "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", 1376 "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" 1377 ], 1378 "type": "layers" 1379 } 1380 }`) 1381 sampleConfigDigest := digest.FromBytes(sampleConfig) 1382 1383 uploadURLBase, _ := startPushLayer(t, env, imageName) 1384 pushLayer(t, env.builder, imageName, sampleConfigDigest, uploadURLBase, bytes.NewReader(sampleConfig)) 1385 manifest.Config.Digest = sampleConfigDigest 1386 manifest.Config.Size = int64(len(sampleConfig)) 1387 1388 // The manifest should still be invalid, because its layer doesn't exist 1389 resp = putManifest(t, "putting missing layer manifest", manifestURL, schema2.MediaTypeManifest, manifest) 1390 defer resp.Body.Close() 1391 checkResponse(t, "putting missing layer manifest", resp, http.StatusBadRequest) 1392 _, p, counts = checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestBlobUnknown) 1393 1394 expectedCounts = map[errcode.ErrorCode]int{ 1395 v2.ErrorCodeManifestBlobUnknown: 2, 1396 } 1397 1398 if !reflect.DeepEqual(counts, expectedCounts) { 1399 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1400 } 1401 1402 // Push 2 random layers 1403 expectedLayers := make(map[digest.Digest]io.ReadSeeker) 1404 1405 for i := range manifest.Layers { 1406 rs, dgst, err := testutil.CreateRandomTarFile() 1407 1408 if err != nil { 1409 t.Fatalf("error creating random layer %d: %v", i, err) 1410 } 1411 1412 expectedLayers[dgst] = rs 1413 manifest.Layers[i].Digest = dgst 1414 1415 uploadURLBase, _ := startPushLayer(t, env, imageName) 1416 pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) 1417 } 1418 1419 // ------------------- 1420 // Push the manifest with all layers pushed. 1421 deserializedManifest, err := schema2.FromStruct(*manifest) 1422 if err != nil { 1423 t.Fatalf("could not create DeserializedManifest: %v", err) 1424 } 1425 _, canonical, err := deserializedManifest.Payload() 1426 if err != nil { 1427 t.Fatalf("could not get manifest payload: %v", err) 1428 } 1429 dgst := digest.FromBytes(canonical) 1430 args.dgst = dgst 1431 args.manifest = deserializedManifest 1432 1433 digestRef, _ := reference.WithDigest(imageName, dgst) 1434 manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) 1435 checkErr(t, err, "building manifest url") 1436 1437 resp = putManifest(t, "putting manifest no error", manifestURL, schema2.MediaTypeManifest, manifest) 1438 checkResponse(t, "putting manifest no error", resp, http.StatusCreated) 1439 checkHeaders(t, resp, http.Header{ 1440 "Location": []string{manifestDigestURL}, 1441 "Docker-Content-Digest": []string{dgst.String()}, 1442 }) 1443 1444 // -------------------- 1445 // Push by digest -- should get same result 1446 resp = putManifest(t, "putting manifest by digest", manifestDigestURL, schema2.MediaTypeManifest, manifest) 1447 checkResponse(t, "putting manifest by digest", resp, http.StatusCreated) 1448 checkHeaders(t, resp, http.Header{ 1449 "Location": []string{manifestDigestURL}, 1450 "Docker-Content-Digest": []string{dgst.String()}, 1451 }) 1452 1453 // ------------------ 1454 // Fetch by tag name 1455 req, err := http.NewRequest("GET", manifestURL, nil) 1456 if err != nil { 1457 t.Fatalf("Error constructing request: %s", err) 1458 } 1459 req.Header.Set("Accept", schema2.MediaTypeManifest) 1460 resp, err = http.DefaultClient.Do(req) 1461 if err != nil { 1462 t.Fatalf("unexpected error fetching manifest: %v", err) 1463 } 1464 defer resp.Body.Close() 1465 1466 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 1467 checkHeaders(t, resp, http.Header{ 1468 "Docker-Content-Digest": []string{dgst.String()}, 1469 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1470 }) 1471 1472 var fetchedManifest schema2.DeserializedManifest 1473 dec := json.NewDecoder(resp.Body) 1474 1475 if err := dec.Decode(&fetchedManifest); err != nil { 1476 t.Fatalf("error decoding fetched manifest: %v", err) 1477 } 1478 1479 _, fetchedCanonical, err := fetchedManifest.Payload() 1480 if err != nil { 1481 t.Fatalf("error getting manifest payload: %v", err) 1482 } 1483 1484 if !bytes.Equal(fetchedCanonical, canonical) { 1485 t.Fatalf("manifests do not match") 1486 } 1487 1488 // --------------- 1489 // Fetch by digest 1490 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1491 if err != nil { 1492 t.Fatalf("Error constructing request: %s", err) 1493 } 1494 req.Header.Set("Accept", schema2.MediaTypeManifest) 1495 resp, err = http.DefaultClient.Do(req) 1496 checkErr(t, err, "fetching manifest by digest") 1497 defer resp.Body.Close() 1498 1499 checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) 1500 checkHeaders(t, resp, http.Header{ 1501 "Docker-Content-Digest": []string{dgst.String()}, 1502 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1503 }) 1504 1505 var fetchedManifestByDigest schema2.DeserializedManifest 1506 dec = json.NewDecoder(resp.Body) 1507 if err := dec.Decode(&fetchedManifestByDigest); err != nil { 1508 t.Fatalf("error decoding fetched manifest: %v", err) 1509 } 1510 1511 _, fetchedCanonical, err = fetchedManifest.Payload() 1512 if err != nil { 1513 t.Fatalf("error getting manifest payload: %v", err) 1514 } 1515 1516 if !bytes.Equal(fetchedCanonical, canonical) { 1517 t.Fatalf("manifests do not match") 1518 } 1519 1520 // Get by name with etag, gives 304 1521 etag := resp.Header.Get("Etag") 1522 req, err = http.NewRequest("GET", manifestURL, nil) 1523 if err != nil { 1524 t.Fatalf("Error constructing request: %s", err) 1525 } 1526 req.Header.Set("If-None-Match", etag) 1527 resp, err = http.DefaultClient.Do(req) 1528 if err != nil { 1529 t.Fatalf("Error constructing request: %s", err) 1530 } 1531 1532 checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) 1533 1534 // Get by digest with etag, gives 304 1535 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1536 if err != nil { 1537 t.Fatalf("Error constructing request: %s", err) 1538 } 1539 req.Header.Set("If-None-Match", etag) 1540 resp, err = http.DefaultClient.Do(req) 1541 if err != nil { 1542 t.Fatalf("Error constructing request: %s", err) 1543 } 1544 1545 checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) 1546 1547 // Ensure that the tag is listed. 1548 resp, err = http.Get(tagsURL) 1549 if err != nil { 1550 t.Fatalf("unexpected error getting unknown tags: %v", err) 1551 } 1552 defer resp.Body.Close() 1553 1554 checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK) 1555 dec = json.NewDecoder(resp.Body) 1556 1557 var tagsResponse tagsAPIResponse 1558 1559 if err := dec.Decode(&tagsResponse); err != nil { 1560 t.Fatalf("unexpected error decoding error response: %v", err) 1561 } 1562 1563 if tagsResponse.Name != imageName.Name() { 1564 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) 1565 } 1566 1567 if len(tagsResponse.Tags) != 1 { 1568 t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) 1569 } 1570 1571 if tagsResponse.Tags[0] != tag { 1572 t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) 1573 } 1574 1575 // ------------------ 1576 // Fetch as a schema1 manifest 1577 resp, err = http.Get(manifestURL) 1578 if err != nil { 1579 t.Fatalf("unexpected error fetching manifest as schema1: %v", err) 1580 } 1581 defer resp.Body.Close() 1582 1583 manifestBytes, err := ioutil.ReadAll(resp.Body) 1584 if err != nil { 1585 t.Fatalf("error reading response body: %v", err) 1586 } 1587 1588 checkResponse(t, "fetching uploaded manifest as schema1", resp, http.StatusOK) 1589 1590 m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes) 1591 if err != nil { 1592 t.Fatalf("unexpected error unmarshalling manifest: %v", err) 1593 } 1594 1595 fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest) 1596 if !ok { 1597 t.Fatalf("expecting schema1 manifest") 1598 } 1599 1600 checkHeaders(t, resp, http.Header{ 1601 "Docker-Content-Digest": []string{desc.Digest.String()}, 1602 "ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)}, 1603 }) 1604 1605 if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { 1606 t.Fatal("wrong schema version") 1607 } 1608 if fetchedSchema1Manifest.Architecture != "amd64" { 1609 t.Fatal("wrong architecture") 1610 } 1611 if fetchedSchema1Manifest.Name != imageName.Name() { 1612 t.Fatal("wrong image name") 1613 } 1614 if fetchedSchema1Manifest.Tag != tag { 1615 t.Fatal("wrong tag") 1616 } 1617 if len(fetchedSchema1Manifest.FSLayers) != 2 { 1618 t.Fatal("wrong number of FSLayers") 1619 } 1620 for i := range manifest.Layers { 1621 if fetchedSchema1Manifest.FSLayers[i].BlobSum != manifest.Layers[len(manifest.Layers)-i-1].Digest { 1622 t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) 1623 } 1624 } 1625 if len(fetchedSchema1Manifest.History) != 2 { 1626 t.Fatal("wrong number of History entries") 1627 } 1628 1629 // Don't check V1Compatibility fields because we're using randomly-generated 1630 // layers. 1631 1632 return args 1633} 1634 1635func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs) { 1636 imageName := args.imageName 1637 tag := "manifestlisttag" 1638 1639 tagRef, _ := reference.WithTag(imageName, tag) 1640 manifestURL, err := env.builder.BuildManifestURL(tagRef) 1641 if err != nil { 1642 t.Fatalf("unexpected error getting manifest url: %v", err) 1643 } 1644 1645 // -------------------------------- 1646 // Attempt to push manifest list that refers to an unknown manifest 1647 manifestList := &manifestlist.ManifestList{ 1648 Versioned: manifest.Versioned{ 1649 SchemaVersion: 2, 1650 MediaType: manifestlist.MediaTypeManifestList, 1651 }, 1652 Manifests: []manifestlist.ManifestDescriptor{ 1653 { 1654 Descriptor: distribution.Descriptor{ 1655 Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", 1656 Size: 3253, 1657 MediaType: schema2.MediaTypeManifest, 1658 }, 1659 Platform: manifestlist.PlatformSpec{ 1660 Architecture: "amd64", 1661 OS: "linux", 1662 }, 1663 }, 1664 }, 1665 } 1666 1667 resp := putManifest(t, "putting missing manifest manifestlist", manifestURL, manifestlist.MediaTypeManifestList, manifestList) 1668 defer resp.Body.Close() 1669 checkResponse(t, "putting missing manifest manifestlist", resp, http.StatusBadRequest) 1670 _, p, counts := checkBodyHasErrorCodes(t, "putting missing manifest manifestlist", resp, v2.ErrorCodeManifestBlobUnknown) 1671 1672 expectedCounts := map[errcode.ErrorCode]int{ 1673 v2.ErrorCodeManifestBlobUnknown: 1, 1674 } 1675 1676 if !reflect.DeepEqual(counts, expectedCounts) { 1677 t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) 1678 } 1679 1680 // ------------------- 1681 // Push a manifest list that references an actual manifest 1682 manifestList.Manifests[0].Digest = args.dgst 1683 deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests) 1684 if err != nil { 1685 t.Fatalf("could not create DeserializedManifestList: %v", err) 1686 } 1687 _, canonical, err := deserializedManifestList.Payload() 1688 if err != nil { 1689 t.Fatalf("could not get manifest list payload: %v", err) 1690 } 1691 dgst := digest.FromBytes(canonical) 1692 1693 digestRef, _ := reference.WithDigest(imageName, dgst) 1694 manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) 1695 checkErr(t, err, "building manifest url") 1696 1697 resp = putManifest(t, "putting manifest list no error", manifestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) 1698 checkResponse(t, "putting manifest list no error", resp, http.StatusCreated) 1699 checkHeaders(t, resp, http.Header{ 1700 "Location": []string{manifestDigestURL}, 1701 "Docker-Content-Digest": []string{dgst.String()}, 1702 }) 1703 1704 // -------------------- 1705 // Push by digest -- should get same result 1706 resp = putManifest(t, "putting manifest list by digest", manifestDigestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) 1707 checkResponse(t, "putting manifest list by digest", resp, http.StatusCreated) 1708 checkHeaders(t, resp, http.Header{ 1709 "Location": []string{manifestDigestURL}, 1710 "Docker-Content-Digest": []string{dgst.String()}, 1711 }) 1712 1713 // ------------------ 1714 // Fetch by tag name 1715 req, err := http.NewRequest("GET", manifestURL, nil) 1716 if err != nil { 1717 t.Fatalf("Error constructing request: %s", err) 1718 } 1719 // multiple headers in mixed list format to ensure we parse correctly server-side 1720 req.Header.Set("Accept", fmt.Sprintf(` %s ; q=0.8 , %s ; q=0.5 `, manifestlist.MediaTypeManifestList, schema1.MediaTypeSignedManifest)) 1721 req.Header.Add("Accept", schema2.MediaTypeManifest) 1722 resp, err = http.DefaultClient.Do(req) 1723 if err != nil { 1724 t.Fatalf("unexpected error fetching manifest list: %v", err) 1725 } 1726 defer resp.Body.Close() 1727 1728 checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) 1729 checkHeaders(t, resp, http.Header{ 1730 "Docker-Content-Digest": []string{dgst.String()}, 1731 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1732 }) 1733 1734 var fetchedManifestList manifestlist.DeserializedManifestList 1735 dec := json.NewDecoder(resp.Body) 1736 1737 if err := dec.Decode(&fetchedManifestList); err != nil { 1738 t.Fatalf("error decoding fetched manifest list: %v", err) 1739 } 1740 1741 _, fetchedCanonical, err := fetchedManifestList.Payload() 1742 if err != nil { 1743 t.Fatalf("error getting manifest list payload: %v", err) 1744 } 1745 1746 if !bytes.Equal(fetchedCanonical, canonical) { 1747 t.Fatalf("manifest lists do not match") 1748 } 1749 1750 // --------------- 1751 // Fetch by digest 1752 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1753 if err != nil { 1754 t.Fatalf("Error constructing request: %s", err) 1755 } 1756 req.Header.Set("Accept", manifestlist.MediaTypeManifestList) 1757 resp, err = http.DefaultClient.Do(req) 1758 checkErr(t, err, "fetching manifest list by digest") 1759 defer resp.Body.Close() 1760 1761 checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) 1762 checkHeaders(t, resp, http.Header{ 1763 "Docker-Content-Digest": []string{dgst.String()}, 1764 "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, 1765 }) 1766 1767 var fetchedManifestListByDigest manifestlist.DeserializedManifestList 1768 dec = json.NewDecoder(resp.Body) 1769 if err := dec.Decode(&fetchedManifestListByDigest); err != nil { 1770 t.Fatalf("error decoding fetched manifest: %v", err) 1771 } 1772 1773 _, fetchedCanonical, err = fetchedManifestListByDigest.Payload() 1774 if err != nil { 1775 t.Fatalf("error getting manifest list payload: %v", err) 1776 } 1777 1778 if !bytes.Equal(fetchedCanonical, canonical) { 1779 t.Fatalf("manifests do not match") 1780 } 1781 1782 // Get by name with etag, gives 304 1783 etag := resp.Header.Get("Etag") 1784 req, err = http.NewRequest("GET", manifestURL, nil) 1785 if err != nil { 1786 t.Fatalf("Error constructing request: %s", err) 1787 } 1788 req.Header.Set("If-None-Match", etag) 1789 resp, err = http.DefaultClient.Do(req) 1790 if err != nil { 1791 t.Fatalf("Error constructing request: %s", err) 1792 } 1793 1794 checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) 1795 1796 // Get by digest with etag, gives 304 1797 req, err = http.NewRequest("GET", manifestDigestURL, nil) 1798 if err != nil { 1799 t.Fatalf("Error constructing request: %s", err) 1800 } 1801 req.Header.Set("If-None-Match", etag) 1802 resp, err = http.DefaultClient.Do(req) 1803 if err != nil { 1804 t.Fatalf("Error constructing request: %s", err) 1805 } 1806 1807 checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) 1808 1809 // ------------------ 1810 // Fetch as a schema1 manifest 1811 resp, err = http.Get(manifestURL) 1812 if err != nil { 1813 t.Fatalf("unexpected error fetching manifest list as schema1: %v", err) 1814 } 1815 defer resp.Body.Close() 1816 1817 manifestBytes, err := ioutil.ReadAll(resp.Body) 1818 if err != nil { 1819 t.Fatalf("error reading response body: %v", err) 1820 } 1821 1822 checkResponse(t, "fetching uploaded manifest list as schema1", resp, http.StatusOK) 1823 1824 m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes) 1825 if err != nil { 1826 t.Fatalf("unexpected error unmarshalling manifest: %v", err) 1827 } 1828 1829 fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest) 1830 if !ok { 1831 t.Fatalf("expecting schema1 manifest") 1832 } 1833 1834 checkHeaders(t, resp, http.Header{ 1835 "Docker-Content-Digest": []string{desc.Digest.String()}, 1836 "ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)}, 1837 }) 1838 1839 if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { 1840 t.Fatal("wrong schema version") 1841 } 1842 if fetchedSchema1Manifest.Architecture != "amd64" { 1843 t.Fatal("wrong architecture") 1844 } 1845 if fetchedSchema1Manifest.Name != imageName.Name() { 1846 t.Fatal("wrong image name") 1847 } 1848 if fetchedSchema1Manifest.Tag != tag { 1849 t.Fatal("wrong tag") 1850 } 1851 if len(fetchedSchema1Manifest.FSLayers) != 2 { 1852 t.Fatal("wrong number of FSLayers") 1853 } 1854 layers := args.manifest.(*schema2.DeserializedManifest).Layers 1855 for i := range layers { 1856 if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(layers)-i-1].Digest { 1857 t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) 1858 } 1859 } 1860 if len(fetchedSchema1Manifest.History) != 2 { 1861 t.Fatal("wrong number of History entries") 1862 } 1863 1864 // Don't check V1Compatibility fields because we're using randomly-generated 1865 // layers. 1866} 1867 1868func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { 1869 imageName := args.imageName 1870 dgst := args.dgst 1871 manifest := args.manifest 1872 1873 ref, _ := reference.WithDigest(imageName, dgst) 1874 manifestDigestURL, _ := env.builder.BuildManifestURL(ref) 1875 // --------------- 1876 // Delete by digest 1877 resp, err := httpDelete(manifestDigestURL) 1878 checkErr(t, err, "deleting manifest by digest") 1879 1880 checkResponse(t, "deleting manifest", resp, http.StatusAccepted) 1881 checkHeaders(t, resp, http.Header{ 1882 "Content-Length": []string{"0"}, 1883 }) 1884 1885 // --------------- 1886 // Attempt to fetch deleted manifest 1887 resp, err = http.Get(manifestDigestURL) 1888 checkErr(t, err, "fetching deleted manifest by digest") 1889 defer resp.Body.Close() 1890 1891 checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound) 1892 1893 // --------------- 1894 // Delete already deleted manifest by digest 1895 resp, err = httpDelete(manifestDigestURL) 1896 checkErr(t, err, "re-deleting manifest by digest") 1897 1898 checkResponse(t, "re-deleting manifest", resp, http.StatusNotFound) 1899 1900 // -------------------- 1901 // Re-upload manifest by digest 1902 resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest) 1903 checkResponse(t, "putting manifest", resp, http.StatusCreated) 1904 checkHeaders(t, resp, http.Header{ 1905 "Location": []string{manifestDigestURL}, 1906 "Docker-Content-Digest": []string{dgst.String()}, 1907 }) 1908 1909 // --------------- 1910 // Attempt to fetch re-uploaded deleted digest 1911 resp, err = http.Get(manifestDigestURL) 1912 checkErr(t, err, "fetching re-uploaded manifest by digest") 1913 defer resp.Body.Close() 1914 1915 checkResponse(t, "fetching re-uploaded manifest", resp, http.StatusOK) 1916 checkHeaders(t, resp, http.Header{ 1917 "Docker-Content-Digest": []string{dgst.String()}, 1918 }) 1919 1920 // --------------- 1921 // Attempt to delete an unknown manifest 1922 unknownDigest := digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") 1923 unknownRef, _ := reference.WithDigest(imageName, unknownDigest) 1924 unknownManifestDigestURL, err := env.builder.BuildManifestURL(unknownRef) 1925 checkErr(t, err, "building unknown manifest url") 1926 1927 resp, err = httpDelete(unknownManifestDigestURL) 1928 checkErr(t, err, "delting unknown manifest by digest") 1929 checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound) 1930 1931 // -------------------- 1932 // Upload manifest by tag 1933 tag := "atag" 1934 tagRef, _ := reference.WithTag(imageName, tag) 1935 manifestTagURL, _ := env.builder.BuildManifestURL(tagRef) 1936 resp = putManifest(t, "putting manifest by tag", manifestTagURL, args.mediaType, manifest) 1937 checkResponse(t, "putting manifest by tag", resp, http.StatusCreated) 1938 checkHeaders(t, resp, http.Header{ 1939 "Location": []string{manifestDigestURL}, 1940 "Docker-Content-Digest": []string{dgst.String()}, 1941 }) 1942 1943 tagsURL, err := env.builder.BuildTagsURL(imageName) 1944 if err != nil { 1945 t.Fatalf("unexpected error building tags url: %v", err) 1946 } 1947 1948 // Ensure that the tag is listed. 1949 resp, err = http.Get(tagsURL) 1950 if err != nil { 1951 t.Fatalf("unexpected error getting unknown tags: %v", err) 1952 } 1953 defer resp.Body.Close() 1954 1955 dec := json.NewDecoder(resp.Body) 1956 var tagsResponse tagsAPIResponse 1957 if err := dec.Decode(&tagsResponse); err != nil { 1958 t.Fatalf("unexpected error decoding error response: %v", err) 1959 } 1960 1961 if tagsResponse.Name != imageName.Name() { 1962 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) 1963 } 1964 1965 if len(tagsResponse.Tags) != 1 { 1966 t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) 1967 } 1968 1969 if tagsResponse.Tags[0] != tag { 1970 t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) 1971 } 1972 1973 // --------------- 1974 // Delete by digest 1975 resp, err = httpDelete(manifestDigestURL) 1976 checkErr(t, err, "deleting manifest by digest") 1977 1978 checkResponse(t, "deleting manifest with tag", resp, http.StatusAccepted) 1979 checkHeaders(t, resp, http.Header{ 1980 "Content-Length": []string{"0"}, 1981 }) 1982 1983 // Ensure that the tag is not listed. 1984 resp, err = http.Get(tagsURL) 1985 if err != nil { 1986 t.Fatalf("unexpected error getting unknown tags: %v", err) 1987 } 1988 defer resp.Body.Close() 1989 1990 dec = json.NewDecoder(resp.Body) 1991 if err := dec.Decode(&tagsResponse); err != nil { 1992 t.Fatalf("unexpected error decoding error response: %v", err) 1993 } 1994 1995 if tagsResponse.Name != imageName.Name() { 1996 t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) 1997 } 1998 1999 if len(tagsResponse.Tags) != 0 { 2000 t.Fatalf("expected 0 tags in response: %v", tagsResponse.Tags) 2001 } 2002 2003} 2004 2005type testEnv struct { 2006 pk libtrust.PrivateKey 2007 ctx context.Context 2008 config configuration.Configuration 2009 app *App 2010 server *httptest.Server 2011 builder *v2.URLBuilder 2012} 2013 2014func newTestEnvMirror(t *testing.T, deleteEnabled bool) *testEnv { 2015 config := configuration.Configuration{ 2016 Storage: configuration.Storage{ 2017 "testdriver": configuration.Parameters{}, 2018 "delete": configuration.Parameters{"enabled": deleteEnabled}, 2019 "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ 2020 "enabled": false, 2021 }}, 2022 }, 2023 Proxy: configuration.Proxy{ 2024 RemoteURL: "http://example.com", 2025 }, 2026 } 2027 config.Compatibility.Schema1.Enabled = true 2028 2029 return newTestEnvWithConfig(t, &config) 2030 2031} 2032 2033func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv { 2034 config := configuration.Configuration{ 2035 Storage: configuration.Storage{ 2036 "testdriver": configuration.Parameters{}, 2037 "delete": configuration.Parameters{"enabled": deleteEnabled}, 2038 "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ 2039 "enabled": false, 2040 }}, 2041 }, 2042 } 2043 2044 config.Compatibility.Schema1.Enabled = true 2045 config.HTTP.Headers = headerConfig 2046 2047 return newTestEnvWithConfig(t, &config) 2048} 2049 2050func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv { 2051 ctx := context.Background() 2052 2053 app := NewApp(ctx, config) 2054 server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) 2055 builder, err := v2.NewURLBuilderFromString(server.URL+config.HTTP.Prefix, false) 2056 2057 if err != nil { 2058 t.Fatalf("error creating url builder: %v", err) 2059 } 2060 2061 pk, err := libtrust.GenerateECP256PrivateKey() 2062 if err != nil { 2063 t.Fatalf("unexpected error generating private key: %v", err) 2064 } 2065 2066 return &testEnv{ 2067 pk: pk, 2068 ctx: ctx, 2069 config: *config, 2070 app: app, 2071 server: server, 2072 builder: builder, 2073 } 2074} 2075 2076func (t *testEnv) Shutdown() { 2077 t.server.CloseClientConnections() 2078 t.server.Close() 2079} 2080 2081func putManifest(t *testing.T, msg, url, contentType string, v interface{}) *http.Response { 2082 var body []byte 2083 2084 switch m := v.(type) { 2085 case *schema1.SignedManifest: 2086 _, pl, err := m.Payload() 2087 if err != nil { 2088 t.Fatalf("error getting payload: %v", err) 2089 } 2090 body = pl 2091 case *manifestlist.DeserializedManifestList: 2092 _, pl, err := m.Payload() 2093 if err != nil { 2094 t.Fatalf("error getting payload: %v", err) 2095 } 2096 body = pl 2097 default: 2098 var err error 2099 body, err = json.MarshalIndent(v, "", " ") 2100 if err != nil { 2101 t.Fatalf("unexpected error marshaling %v: %v", v, err) 2102 } 2103 } 2104 2105 req, err := http.NewRequest("PUT", url, bytes.NewReader(body)) 2106 if err != nil { 2107 t.Fatalf("error creating request for %s: %v", msg, err) 2108 } 2109 2110 if contentType != "" { 2111 req.Header.Set("Content-Type", contentType) 2112 } 2113 2114 resp, err := http.DefaultClient.Do(req) 2115 if err != nil { 2116 t.Fatalf("error doing put request while %s: %v", msg, err) 2117 } 2118 2119 return resp 2120} 2121 2122func startPushLayer(t *testing.T, env *testEnv, name reference.Named) (location string, uuid string) { 2123 layerUploadURL, err := env.builder.BuildBlobUploadURL(name) 2124 if err != nil { 2125 t.Fatalf("unexpected error building layer upload url: %v", err) 2126 } 2127 2128 u, err := url.Parse(layerUploadURL) 2129 if err != nil { 2130 t.Fatalf("error parsing layer upload URL: %v", err) 2131 } 2132 2133 base, err := url.Parse(env.server.URL) 2134 if err != nil { 2135 t.Fatalf("error parsing server URL: %v", err) 2136 } 2137 2138 layerUploadURL = base.ResolveReference(u).String() 2139 resp, err := http.Post(layerUploadURL, "", nil) 2140 if err != nil { 2141 t.Fatalf("unexpected error starting layer push: %v", err) 2142 } 2143 2144 defer resp.Body.Close() 2145 2146 checkResponse(t, fmt.Sprintf("pushing starting layer push %v", name.String()), resp, http.StatusAccepted) 2147 2148 u, err = url.Parse(resp.Header.Get("Location")) 2149 if err != nil { 2150 t.Fatalf("error parsing location header: %v", err) 2151 } 2152 2153 uuid = path.Base(u.Path) 2154 checkHeaders(t, resp, http.Header{ 2155 "Location": []string{"*"}, 2156 "Content-Length": []string{"0"}, 2157 "Docker-Upload-UUID": []string{uuid}, 2158 }) 2159 2160 return resp.Header.Get("Location"), uuid 2161} 2162 2163// doPushLayer pushes the layer content returning the url on success returning 2164// the response. If you're only expecting a successful response, use pushLayer. 2165func doPushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) (*http.Response, error) { 2166 u, err := url.Parse(uploadURLBase) 2167 if err != nil { 2168 t.Fatalf("unexpected error parsing pushLayer url: %v", err) 2169 } 2170 2171 u.RawQuery = url.Values{ 2172 "_state": u.Query()["_state"], 2173 "digest": []string{dgst.String()}, 2174 }.Encode() 2175 2176 uploadURL := u.String() 2177 2178 // Just do a monolithic upload 2179 req, err := http.NewRequest("PUT", uploadURL, body) 2180 if err != nil { 2181 t.Fatalf("unexpected error creating new request: %v", err) 2182 } 2183 2184 return http.DefaultClient.Do(req) 2185} 2186 2187// pushLayer pushes the layer content returning the url on success. 2188func pushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) string { 2189 digester := digest.Canonical.Digester() 2190 2191 resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, io.TeeReader(body, digester.Hash())) 2192 if err != nil { 2193 t.Fatalf("unexpected error doing push layer request: %v", err) 2194 } 2195 defer resp.Body.Close() 2196 2197 checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) 2198 2199 if err != nil { 2200 t.Fatalf("error generating sha256 digest of body") 2201 } 2202 2203 sha256Dgst := digester.Digest() 2204 2205 ref, _ := reference.WithDigest(name, sha256Dgst) 2206 expectedLayerURL, err := ub.BuildBlobURL(ref) 2207 if err != nil { 2208 t.Fatalf("error building expected layer url: %v", err) 2209 } 2210 2211 checkHeaders(t, resp, http.Header{ 2212 "Location": []string{expectedLayerURL}, 2213 "Content-Length": []string{"0"}, 2214 "Docker-Content-Digest": []string{sha256Dgst.String()}, 2215 }) 2216 2217 return resp.Header.Get("Location") 2218} 2219 2220func finishUpload(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, dgst digest.Digest) string { 2221 resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, nil) 2222 if err != nil { 2223 t.Fatalf("unexpected error doing push layer request: %v", err) 2224 } 2225 defer resp.Body.Close() 2226 2227 checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) 2228 2229 ref, _ := reference.WithDigest(name, dgst) 2230 expectedLayerURL, err := ub.BuildBlobURL(ref) 2231 if err != nil { 2232 t.Fatalf("error building expected layer url: %v", err) 2233 } 2234 2235 checkHeaders(t, resp, http.Header{ 2236 "Location": []string{expectedLayerURL}, 2237 "Content-Length": []string{"0"}, 2238 "Docker-Content-Digest": []string{dgst.String()}, 2239 }) 2240 2241 return resp.Header.Get("Location") 2242} 2243 2244func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) { 2245 u, err := url.Parse(uploadURLBase) 2246 if err != nil { 2247 t.Fatalf("unexpected error parsing pushLayer url: %v", err) 2248 } 2249 2250 u.RawQuery = url.Values{ 2251 "_state": u.Query()["_state"], 2252 }.Encode() 2253 2254 uploadURL := u.String() 2255 2256 digester := digest.Canonical.Digester() 2257 2258 req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash())) 2259 if err != nil { 2260 t.Fatalf("unexpected error creating new request: %v", err) 2261 } 2262 req.Header.Set("Content-Type", "application/octet-stream") 2263 2264 resp, err := http.DefaultClient.Do(req) 2265 2266 return resp, digester.Digest(), err 2267} 2268 2269func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) { 2270 resp, dgst, err := doPushChunk(t, uploadURLBase, body) 2271 if err != nil { 2272 t.Fatalf("unexpected error doing push layer request: %v", err) 2273 } 2274 defer resp.Body.Close() 2275 2276 checkResponse(t, "putting chunk", resp, http.StatusAccepted) 2277 2278 if err != nil { 2279 t.Fatalf("error generating sha256 digest of body") 2280 } 2281 2282 checkHeaders(t, resp, http.Header{ 2283 "Range": []string{fmt.Sprintf("0-%d", length-1)}, 2284 "Content-Length": []string{"0"}, 2285 }) 2286 2287 return resp.Header.Get("Location"), dgst 2288} 2289 2290func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) { 2291 if resp.StatusCode != expectedStatus { 2292 t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus) 2293 maybeDumpResponse(t, resp) 2294 2295 t.FailNow() 2296 } 2297 2298 // We expect the headers included in the configuration, unless the 2299 // status code is 405 (Method Not Allowed), which means the handler 2300 // doesn't even get called. 2301 if resp.StatusCode != 405 && !reflect.DeepEqual(resp.Header["X-Content-Type-Options"], []string{"nosniff"}) { 2302 t.Logf("missing or incorrect header X-Content-Type-Options %s", msg) 2303 maybeDumpResponse(t, resp) 2304 2305 t.FailNow() 2306 } 2307} 2308 2309// checkBodyHasErrorCodes ensures the body is an error body and has the 2310// expected error codes, returning the error structure, the json slice and a 2311// count of the errors by code. 2312func checkBodyHasErrorCodes(t *testing.T, msg string, resp *http.Response, errorCodes ...errcode.ErrorCode) (errcode.Errors, []byte, map[errcode.ErrorCode]int) { 2313 p, err := ioutil.ReadAll(resp.Body) 2314 if err != nil { 2315 t.Fatalf("unexpected error reading body %s: %v", msg, err) 2316 } 2317 2318 var errs errcode.Errors 2319 if err := json.Unmarshal(p, &errs); err != nil { 2320 t.Fatalf("unexpected error decoding error response: %v", err) 2321 } 2322 2323 if len(errs) == 0 { 2324 t.Fatalf("expected errors in response") 2325 } 2326 2327 // TODO(stevvooe): Shoot. The error setup is not working out. The content- 2328 // type headers are being set after writing the status code. 2329 // if resp.Header.Get("Content-Type") != "application/json; charset=utf-8" { 2330 // t.Fatalf("unexpected content type: %v != 'application/json'", 2331 // resp.Header.Get("Content-Type")) 2332 // } 2333 2334 expected := map[errcode.ErrorCode]struct{}{} 2335 counts := map[errcode.ErrorCode]int{} 2336 2337 // Initialize map with zeros for expected 2338 for _, code := range errorCodes { 2339 expected[code] = struct{}{} 2340 counts[code] = 0 2341 } 2342 2343 for _, e := range errs { 2344 err, ok := e.(errcode.ErrorCoder) 2345 if !ok { 2346 t.Fatalf("not an ErrorCoder: %#v", e) 2347 } 2348 if _, ok := expected[err.ErrorCode()]; !ok { 2349 t.Fatalf("unexpected error code %v encountered during %s: %s ", err.ErrorCode(), msg, string(p)) 2350 } 2351 counts[err.ErrorCode()]++ 2352 } 2353 2354 // Ensure that counts of expected errors were all non-zero 2355 for code := range expected { 2356 if counts[code] == 0 { 2357 t.Fatalf("expected error code %v not encounterd during %s: %s", code, msg, string(p)) 2358 } 2359 } 2360 2361 return errs, p, counts 2362} 2363 2364func maybeDumpResponse(t *testing.T, resp *http.Response) { 2365 if d, err := httputil.DumpResponse(resp, true); err != nil { 2366 t.Logf("error dumping response: %v", err) 2367 } else { 2368 t.Logf("response:\n%s", string(d)) 2369 } 2370} 2371 2372// matchHeaders checks that the response has at least the headers. If not, the 2373// test will fail. If a passed in header value is "*", any non-zero value will 2374// suffice as a match. 2375func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) { 2376 for k, vs := range headers { 2377 if resp.Header.Get(k) == "" { 2378 t.Fatalf("response missing header %q", k) 2379 } 2380 2381 for _, v := range vs { 2382 if v == "*" { 2383 // Just ensure there is some value. 2384 if len(resp.Header[http.CanonicalHeaderKey(k)]) > 0 { 2385 continue 2386 } 2387 } 2388 2389 for _, hv := range resp.Header[http.CanonicalHeaderKey(k)] { 2390 if hv != v { 2391 t.Fatalf("%+v %v header value not matched in response: %q != %q", resp.Header, k, hv, v) 2392 } 2393 } 2394 } 2395 } 2396} 2397 2398func checkErr(t *testing.T, err error, msg string) { 2399 if err != nil { 2400 t.Fatalf("unexpected error %s: %v", msg, err) 2401 } 2402} 2403 2404func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest { 2405 imageNameRef, err := reference.WithName(imageName) 2406 if err != nil { 2407 t.Fatalf("unable to parse reference: %v", err) 2408 } 2409 2410 unsignedManifest := &schema1.Manifest{ 2411 Versioned: manifest.Versioned{ 2412 SchemaVersion: 1, 2413 }, 2414 Name: imageName, 2415 Tag: tag, 2416 FSLayers: []schema1.FSLayer{ 2417 { 2418 BlobSum: "asdf", 2419 }, 2420 }, 2421 History: []schema1.History{ 2422 { 2423 V1Compatibility: "", 2424 }, 2425 }, 2426 } 2427 2428 // Push 2 random layers 2429 expectedLayers := make(map[digest.Digest]io.ReadSeeker) 2430 2431 for i := range unsignedManifest.FSLayers { 2432 rs, dgst, err := testutil.CreateRandomTarFile() 2433 if err != nil { 2434 t.Fatalf("error creating random layer %d: %v", i, err) 2435 } 2436 2437 expectedLayers[dgst] = rs 2438 unsignedManifest.FSLayers[i].BlobSum = dgst 2439 uploadURLBase, _ := startPushLayer(t, env, imageNameRef) 2440 pushLayer(t, env.builder, imageNameRef, dgst, uploadURLBase, rs) 2441 } 2442 2443 signedManifest, err := schema1.Sign(unsignedManifest, env.pk) 2444 if err != nil { 2445 t.Fatalf("unexpected error signing manifest: %v", err) 2446 } 2447 2448 dgst := digest.FromBytes(signedManifest.Canonical) 2449 2450 // Create this repository by tag to ensure the tag mapping is made in the registry 2451 tagRef, _ := reference.WithTag(imageNameRef, tag) 2452 manifestDigestURL, err := env.builder.BuildManifestURL(tagRef) 2453 checkErr(t, err, "building manifest url") 2454 2455 digestRef, _ := reference.WithDigest(imageNameRef, dgst) 2456 location, err := env.builder.BuildManifestURL(digestRef) 2457 checkErr(t, err, "building location URL") 2458 2459 resp := putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) 2460 checkResponse(t, "putting signed manifest", resp, http.StatusCreated) 2461 checkHeaders(t, resp, http.Header{ 2462 "Location": []string{location}, 2463 "Docker-Content-Digest": []string{dgst.String()}, 2464 }) 2465 return dgst 2466} 2467 2468// Test mutation operations on a registry configured as a cache. Ensure that they return 2469// appropriate errors. 2470func TestRegistryAsCacheMutationAPIs(t *testing.T) { 2471 deleteEnabled := true 2472 env := newTestEnvMirror(t, deleteEnabled) 2473 defer env.Shutdown() 2474 2475 imageName, _ := reference.WithName("foo/bar") 2476 tag := "latest" 2477 tagRef, _ := reference.WithTag(imageName, tag) 2478 manifestURL, err := env.builder.BuildManifestURL(tagRef) 2479 if err != nil { 2480 t.Fatalf("unexpected error building base url: %v", err) 2481 } 2482 2483 // Manifest upload 2484 m := &schema1.Manifest{ 2485 Versioned: manifest.Versioned{ 2486 SchemaVersion: 1, 2487 }, 2488 Name: imageName.Name(), 2489 Tag: tag, 2490 FSLayers: []schema1.FSLayer{}, 2491 History: []schema1.History{}, 2492 } 2493 2494 sm, err := schema1.Sign(m, env.pk) 2495 if err != nil { 2496 t.Fatalf("error signing manifest: %v", err) 2497 } 2498 2499 resp := putManifest(t, "putting unsigned manifest", manifestURL, "", sm) 2500 checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2501 2502 // Manifest Delete 2503 resp, _ = httpDelete(manifestURL) 2504 checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2505 2506 // Blob upload initialization 2507 layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) 2508 if err != nil { 2509 t.Fatalf("unexpected error building layer upload url: %v", err) 2510 } 2511 2512 resp, err = http.Post(layerUploadURL, "", nil) 2513 if err != nil { 2514 t.Fatalf("unexpected error starting layer push: %v", err) 2515 } 2516 defer resp.Body.Close() 2517 2518 checkResponse(t, fmt.Sprintf("starting layer push to cache %v", imageName), resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2519 2520 // Blob Delete 2521 ref, _ := reference.WithDigest(imageName, digestSha256EmptyTar) 2522 blobURL, _ := env.builder.BuildBlobURL(ref) 2523 resp, _ = httpDelete(blobURL) 2524 checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) 2525 2526} 2527 2528func TestProxyManifestGetByTag(t *testing.T) { 2529 truthConfig := configuration.Configuration{ 2530 Storage: configuration.Storage{ 2531 "testdriver": configuration.Parameters{}, 2532 "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ 2533 "enabled": false, 2534 }}, 2535 }, 2536 } 2537 truthConfig.Compatibility.Schema1.Enabled = true 2538 truthConfig.HTTP.Headers = headerConfig 2539 2540 imageName, _ := reference.WithName("foo/bar") 2541 tag := "latest" 2542 2543 truthEnv := newTestEnvWithConfig(t, &truthConfig) 2544 defer truthEnv.Shutdown() 2545 // create a repository in the truth registry 2546 dgst := createRepository(truthEnv, t, imageName.Name(), tag) 2547 2548 proxyConfig := configuration.Configuration{ 2549 Storage: configuration.Storage{ 2550 "testdriver": configuration.Parameters{}, 2551 }, 2552 Proxy: configuration.Proxy{ 2553 RemoteURL: truthEnv.server.URL, 2554 }, 2555 } 2556 proxyConfig.Compatibility.Schema1.Enabled = true 2557 proxyConfig.HTTP.Headers = headerConfig 2558 2559 proxyEnv := newTestEnvWithConfig(t, &proxyConfig) 2560 defer proxyEnv.Shutdown() 2561 2562 digestRef, _ := reference.WithDigest(imageName, dgst) 2563 manifestDigestURL, err := proxyEnv.builder.BuildManifestURL(digestRef) 2564 checkErr(t, err, "building manifest url") 2565 2566 resp, err := http.Get(manifestDigestURL) 2567 checkErr(t, err, "fetching manifest from proxy by digest") 2568 defer resp.Body.Close() 2569 2570 tagRef, _ := reference.WithTag(imageName, tag) 2571 manifestTagURL, err := proxyEnv.builder.BuildManifestURL(tagRef) 2572 checkErr(t, err, "building manifest url") 2573 2574 resp, err = http.Get(manifestTagURL) 2575 checkErr(t, err, "fetching manifest from proxy by tag (error check 1)") 2576 defer resp.Body.Close() 2577 checkResponse(t, "fetching manifest from proxy by tag (response check 1)", resp, http.StatusOK) 2578 checkHeaders(t, resp, http.Header{ 2579 "Docker-Content-Digest": []string{dgst.String()}, 2580 }) 2581 2582 // Create another manifest in the remote with the same image/tag pair 2583 newDigest := createRepository(truthEnv, t, imageName.Name(), tag) 2584 if dgst == newDigest { 2585 t.Fatalf("non-random test data") 2586 } 2587 2588 // fetch it with the same proxy URL as before. Ensure the updated content is at the same tag 2589 resp, err = http.Get(manifestTagURL) 2590 checkErr(t, err, "fetching manifest from proxy by tag (error check 2)") 2591 defer resp.Body.Close() 2592 checkResponse(t, "fetching manifest from proxy by tag (response check 2)", resp, http.StatusOK) 2593 checkHeaders(t, resp, http.Header{ 2594 "Docker-Content-Digest": []string{newDigest.String()}, 2595 }) 2596} 2597