github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/client/repository.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "strconv" 13 "time" 14 15 "github.com/docker/distribution" 16 "github.com/docker/distribution/context" 17 "github.com/docker/distribution/digest" 18 "github.com/docker/distribution/reference" 19 "github.com/docker/distribution/registry/api/v2" 20 "github.com/docker/distribution/registry/client/transport" 21 "github.com/docker/distribution/registry/storage/cache" 22 "github.com/docker/distribution/registry/storage/cache/memory" 23 ) 24 25 // Registry provides an interface for calling Repositories, which returns a catalog of repositories. 26 type Registry interface { 27 Repositories(ctx context.Context, repos []string, last string) (n int, err error) 28 } 29 30 // NewRegistry creates a registry namespace which can be used to get a listing of repositories 31 func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) { 32 ub, err := v2.NewURLBuilderFromString(baseURL) 33 if err != nil { 34 return nil, err 35 } 36 37 client := &http.Client{ 38 Transport: transport, 39 Timeout: 1 * time.Minute, 40 } 41 42 return ®istry{ 43 client: client, 44 ub: ub, 45 context: ctx, 46 }, nil 47 } 48 49 type registry struct { 50 client *http.Client 51 ub *v2.URLBuilder 52 context context.Context 53 } 54 55 // Repositories returns a lexigraphically sorted catalog given a base URL. The 'entries' slice will be filled up to the size 56 // of the slice, starting at the value provided in 'last'. The number of entries will be returned along with io.EOF if there 57 // are no more entries 58 func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) { 59 var numFilled int 60 var returnErr error 61 62 values := buildCatalogValues(len(entries), last) 63 u, err := r.ub.BuildCatalogURL(values) 64 if err != nil { 65 return 0, err 66 } 67 68 resp, err := r.client.Get(u) 69 if err != nil { 70 return 0, err 71 } 72 defer resp.Body.Close() 73 74 if SuccessStatus(resp.StatusCode) { 75 var ctlg struct { 76 Repositories []string `json:"repositories"` 77 } 78 decoder := json.NewDecoder(resp.Body) 79 80 if err := decoder.Decode(&ctlg); err != nil { 81 return 0, err 82 } 83 84 for cnt := range ctlg.Repositories { 85 entries[cnt] = ctlg.Repositories[cnt] 86 } 87 numFilled = len(ctlg.Repositories) 88 89 link := resp.Header.Get("Link") 90 if link == "" { 91 returnErr = io.EOF 92 } 93 } else { 94 return 0, HandleErrorResponse(resp) 95 } 96 97 return numFilled, returnErr 98 } 99 100 // NewRepository creates a new Repository for the given repository name and base URL. 101 func NewRepository(ctx context.Context, name reference.Named, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { 102 ub, err := v2.NewURLBuilderFromString(baseURL) 103 if err != nil { 104 return nil, err 105 } 106 107 client := &http.Client{ 108 Transport: transport, 109 // TODO(dmcgowan): create cookie jar 110 } 111 112 return &repository{ 113 client: client, 114 ub: ub, 115 name: name, 116 context: ctx, 117 }, nil 118 } 119 120 type repository struct { 121 client *http.Client 122 ub *v2.URLBuilder 123 context context.Context 124 name reference.Named 125 } 126 127 func (r *repository) Name() reference.Named { 128 return r.name 129 } 130 131 func (r *repository) Blobs(ctx context.Context) distribution.BlobStore { 132 statter := &blobStatter{ 133 name: r.name, 134 ub: r.ub, 135 client: r.client, 136 } 137 return &blobs{ 138 name: r.name, 139 ub: r.ub, 140 client: r.client, 141 statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter), 142 } 143 } 144 145 func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { 146 // todo(richardscothern): options should be sent over the wire 147 return &manifests{ 148 name: r.name, 149 ub: r.ub, 150 client: r.client, 151 etags: make(map[string]string), 152 }, nil 153 } 154 155 func (r *repository) Tags(ctx context.Context) distribution.TagService { 156 return &tags{ 157 client: r.client, 158 ub: r.ub, 159 context: r.context, 160 name: r.Name(), 161 } 162 } 163 164 // tags implements remote tagging operations. 165 type tags struct { 166 client *http.Client 167 ub *v2.URLBuilder 168 context context.Context 169 name reference.Named 170 } 171 172 // All returns all tags 173 func (t *tags) All(ctx context.Context) ([]string, error) { 174 var tags []string 175 176 u, err := t.ub.BuildTagsURL(t.name) 177 if err != nil { 178 return tags, err 179 } 180 181 resp, err := t.client.Get(u) 182 if err != nil { 183 return tags, err 184 } 185 defer resp.Body.Close() 186 187 if SuccessStatus(resp.StatusCode) { 188 b, err := ioutil.ReadAll(resp.Body) 189 if err != nil { 190 return tags, err 191 } 192 193 tagsResponse := struct { 194 Tags []string `json:"tags"` 195 }{} 196 if err := json.Unmarshal(b, &tagsResponse); err != nil { 197 return tags, err 198 } 199 tags = tagsResponse.Tags 200 return tags, nil 201 } 202 return tags, HandleErrorResponse(resp) 203 } 204 205 func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) { 206 desc := distribution.Descriptor{} 207 headers := response.Header 208 209 ctHeader := headers.Get("Content-Type") 210 if ctHeader == "" { 211 return distribution.Descriptor{}, errors.New("missing or empty Content-Type header") 212 } 213 desc.MediaType = ctHeader 214 215 digestHeader := headers.Get("Docker-Content-Digest") 216 if digestHeader == "" { 217 bytes, err := ioutil.ReadAll(response.Body) 218 if err != nil { 219 return distribution.Descriptor{}, err 220 } 221 _, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) 222 if err != nil { 223 return distribution.Descriptor{}, err 224 } 225 return desc, nil 226 } 227 228 dgst, err := digest.ParseDigest(digestHeader) 229 if err != nil { 230 return distribution.Descriptor{}, err 231 } 232 desc.Digest = dgst 233 234 lengthHeader := headers.Get("Content-Length") 235 if lengthHeader == "" { 236 return distribution.Descriptor{}, errors.New("missing or empty Content-Length header") 237 } 238 length, err := strconv.ParseInt(lengthHeader, 10, 64) 239 if err != nil { 240 return distribution.Descriptor{}, err 241 } 242 desc.Size = length 243 244 return desc, nil 245 246 } 247 248 // Get issues a HEAD request for a Manifest against its named endpoint in order 249 // to construct a descriptor for the tag. If the registry doesn't support HEADing 250 // a manifest, fallback to GET. 251 func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { 252 ref, err := reference.WithTag(t.name, tag) 253 if err != nil { 254 return distribution.Descriptor{}, err 255 } 256 u, err := t.ub.BuildManifestURL(ref) 257 if err != nil { 258 return distribution.Descriptor{}, err 259 } 260 261 req, err := http.NewRequest("HEAD", u, nil) 262 if err != nil { 263 return distribution.Descriptor{}, err 264 } 265 266 for _, t := range distribution.ManifestMediaTypes() { 267 req.Header.Add("Accept", t) 268 } 269 270 var attempts int 271 resp, err := t.client.Do(req) 272 check: 273 if err != nil { 274 return distribution.Descriptor{}, err 275 } 276 277 switch { 278 case resp.StatusCode >= 200 && resp.StatusCode < 400: 279 return descriptorFromResponse(resp) 280 case resp.StatusCode == http.StatusMethodNotAllowed: 281 req, err = http.NewRequest("GET", u, nil) 282 if err != nil { 283 return distribution.Descriptor{}, err 284 } 285 286 for _, t := range distribution.ManifestMediaTypes() { 287 req.Header.Add("Accept", t) 288 } 289 290 resp, err = t.client.Do(req) 291 attempts++ 292 if attempts > 1 { 293 return distribution.Descriptor{}, err 294 } 295 goto check 296 default: 297 return distribution.Descriptor{}, HandleErrorResponse(resp) 298 } 299 } 300 301 func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { 302 panic("not implemented") 303 } 304 305 func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { 306 panic("not implemented") 307 } 308 309 func (t *tags) Untag(ctx context.Context, tag string) error { 310 panic("not implemented") 311 } 312 313 type manifests struct { 314 name reference.Named 315 ub *v2.URLBuilder 316 client *http.Client 317 etags map[string]string 318 } 319 320 func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { 321 ref, err := reference.WithDigest(ms.name, dgst) 322 if err != nil { 323 return false, err 324 } 325 u, err := ms.ub.BuildManifestURL(ref) 326 if err != nil { 327 return false, err 328 } 329 330 resp, err := ms.client.Head(u) 331 if err != nil { 332 return false, err 333 } 334 335 if SuccessStatus(resp.StatusCode) { 336 return true, nil 337 } else if resp.StatusCode == http.StatusNotFound { 338 return false, nil 339 } 340 return false, HandleErrorResponse(resp) 341 } 342 343 // AddEtagToTag allows a client to supply an eTag to Get which will be 344 // used for a conditional HTTP request. If the eTag matches, a nil manifest 345 // and ErrManifestNotModified error will be returned. etag is automatically 346 // quoted when added to this map. 347 func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption { 348 return etagOption{tag, etag} 349 } 350 351 type etagOption struct{ tag, etag string } 352 353 func (o etagOption) Apply(ms distribution.ManifestService) error { 354 if ms, ok := ms.(*manifests); ok { 355 ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) 356 return nil 357 } 358 return fmt.Errorf("etag options is a client-only option") 359 } 360 361 func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { 362 var ( 363 digestOrTag string 364 ref reference.Named 365 err error 366 ) 367 368 for _, option := range options { 369 if opt, ok := option.(withTagOption); ok { 370 digestOrTag = opt.tag 371 ref, err = reference.WithTag(ms.name, opt.tag) 372 if err != nil { 373 return nil, err 374 } 375 } else { 376 err := option.Apply(ms) 377 if err != nil { 378 return nil, err 379 } 380 } 381 } 382 383 if digestOrTag == "" { 384 digestOrTag = dgst.String() 385 ref, err = reference.WithDigest(ms.name, dgst) 386 if err != nil { 387 return nil, err 388 } 389 } 390 391 u, err := ms.ub.BuildManifestURL(ref) 392 if err != nil { 393 return nil, err 394 } 395 396 req, err := http.NewRequest("GET", u, nil) 397 if err != nil { 398 return nil, err 399 } 400 401 for _, t := range distribution.ManifestMediaTypes() { 402 req.Header.Add("Accept", t) 403 } 404 405 if _, ok := ms.etags[digestOrTag]; ok { 406 req.Header.Set("If-None-Match", ms.etags[digestOrTag]) 407 } 408 409 resp, err := ms.client.Do(req) 410 if err != nil { 411 return nil, err 412 } 413 defer resp.Body.Close() 414 if resp.StatusCode == http.StatusNotModified { 415 return nil, distribution.ErrManifestNotModified 416 } else if SuccessStatus(resp.StatusCode) { 417 mt := resp.Header.Get("Content-Type") 418 body, err := ioutil.ReadAll(resp.Body) 419 420 if err != nil { 421 return nil, err 422 } 423 m, _, err := distribution.UnmarshalManifest(mt, body) 424 if err != nil { 425 return nil, err 426 } 427 return m, nil 428 } 429 return nil, HandleErrorResponse(resp) 430 } 431 432 // WithTag allows a tag to be passed into Put which enables the client 433 // to build a correct URL. 434 func WithTag(tag string) distribution.ManifestServiceOption { 435 return withTagOption{tag} 436 } 437 438 type withTagOption struct{ tag string } 439 440 func (o withTagOption) Apply(m distribution.ManifestService) error { 441 if _, ok := m.(*manifests); ok { 442 return nil 443 } 444 return fmt.Errorf("withTagOption is a client-only option") 445 } 446 447 // Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the 448 // tag name in order to build the correct upload URL. This state is written and read under a lock. 449 func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { 450 ref := ms.name 451 452 for _, option := range options { 453 if opt, ok := option.(withTagOption); ok { 454 var err error 455 ref, err = reference.WithTag(ref, opt.tag) 456 if err != nil { 457 return "", err 458 } 459 } else { 460 err := option.Apply(ms) 461 if err != nil { 462 return "", err 463 } 464 } 465 } 466 467 manifestURL, err := ms.ub.BuildManifestURL(ref) 468 if err != nil { 469 return "", err 470 } 471 472 mediaType, p, err := m.Payload() 473 if err != nil { 474 return "", err 475 } 476 477 putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p)) 478 if err != nil { 479 return "", err 480 } 481 482 putRequest.Header.Set("Content-Type", mediaType) 483 484 resp, err := ms.client.Do(putRequest) 485 if err != nil { 486 return "", err 487 } 488 defer resp.Body.Close() 489 490 if SuccessStatus(resp.StatusCode) { 491 dgstHeader := resp.Header.Get("Docker-Content-Digest") 492 dgst, err := digest.ParseDigest(dgstHeader) 493 if err != nil { 494 return "", err 495 } 496 497 return dgst, nil 498 } 499 500 return "", HandleErrorResponse(resp) 501 } 502 503 func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error { 504 ref, err := reference.WithDigest(ms.name, dgst) 505 if err != nil { 506 return err 507 } 508 u, err := ms.ub.BuildManifestURL(ref) 509 if err != nil { 510 return err 511 } 512 req, err := http.NewRequest("DELETE", u, nil) 513 if err != nil { 514 return err 515 } 516 517 resp, err := ms.client.Do(req) 518 if err != nil { 519 return err 520 } 521 defer resp.Body.Close() 522 523 if SuccessStatus(resp.StatusCode) { 524 return nil 525 } 526 return HandleErrorResponse(resp) 527 } 528 529 // todo(richardscothern): Restore interface and implementation with merge of #1050 530 /*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { 531 panic("not supported") 532 }*/ 533 534 type blobs struct { 535 name reference.Named 536 ub *v2.URLBuilder 537 client *http.Client 538 539 statter distribution.BlobDescriptorService 540 distribution.BlobDeleter 541 } 542 543 func sanitizeLocation(location, base string) (string, error) { 544 baseURL, err := url.Parse(base) 545 if err != nil { 546 return "", err 547 } 548 549 locationURL, err := url.Parse(location) 550 if err != nil { 551 return "", err 552 } 553 554 return baseURL.ResolveReference(locationURL).String(), nil 555 } 556 557 func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { 558 return bs.statter.Stat(ctx, dgst) 559 560 } 561 562 func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { 563 reader, err := bs.Open(ctx, dgst) 564 if err != nil { 565 return nil, err 566 } 567 defer reader.Close() 568 569 return ioutil.ReadAll(reader) 570 } 571 572 func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { 573 ref, err := reference.WithDigest(bs.name, dgst) 574 if err != nil { 575 return nil, err 576 } 577 blobURL, err := bs.ub.BuildBlobURL(ref) 578 if err != nil { 579 return nil, err 580 } 581 582 return transport.NewHTTPReadSeeker(bs.client, blobURL, 583 func(resp *http.Response) error { 584 if resp.StatusCode == http.StatusNotFound { 585 return distribution.ErrBlobUnknown 586 } 587 return HandleErrorResponse(resp) 588 }), nil 589 } 590 591 func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { 592 panic("not implemented") 593 } 594 595 func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { 596 writer, err := bs.Create(ctx) 597 if err != nil { 598 return distribution.Descriptor{}, err 599 } 600 dgstr := digest.Canonical.New() 601 n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash())) 602 if err != nil { 603 return distribution.Descriptor{}, err 604 } 605 if n < int64(len(p)) { 606 return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p)) 607 } 608 609 desc := distribution.Descriptor{ 610 MediaType: mediaType, 611 Size: int64(len(p)), 612 Digest: dgstr.Digest(), 613 } 614 615 return writer.Commit(ctx, desc) 616 } 617 618 // createOptions is a collection of blob creation modifiers relevant to general 619 // blob storage intended to be configured by the BlobCreateOption.Apply method. 620 type createOptions struct { 621 Mount struct { 622 ShouldMount bool 623 From reference.Canonical 624 } 625 } 626 627 type optionFunc func(interface{}) error 628 629 func (f optionFunc) Apply(v interface{}) error { 630 return f(v) 631 } 632 633 // WithMountFrom returns a BlobCreateOption which designates that the blob should be 634 // mounted from the given canonical reference. 635 func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption { 636 return optionFunc(func(v interface{}) error { 637 opts, ok := v.(*createOptions) 638 if !ok { 639 return fmt.Errorf("unexpected options type: %T", v) 640 } 641 642 opts.Mount.ShouldMount = true 643 opts.Mount.From = ref 644 645 return nil 646 }) 647 } 648 649 func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { 650 var opts createOptions 651 652 for _, option := range options { 653 err := option.Apply(&opts) 654 if err != nil { 655 return nil, err 656 } 657 } 658 659 var values []url.Values 660 661 if opts.Mount.ShouldMount { 662 values = append(values, url.Values{"from": {opts.Mount.From.Name()}, "mount": {opts.Mount.From.Digest().String()}}) 663 } 664 665 u, err := bs.ub.BuildBlobUploadURL(bs.name, values...) 666 if err != nil { 667 return nil, err 668 } 669 670 resp, err := bs.client.Post(u, "", nil) 671 if err != nil { 672 return nil, err 673 } 674 defer resp.Body.Close() 675 676 switch resp.StatusCode { 677 case http.StatusCreated: 678 desc, err := bs.statter.Stat(ctx, opts.Mount.From.Digest()) 679 if err != nil { 680 return nil, err 681 } 682 return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc} 683 case http.StatusAccepted: 684 // TODO(dmcgowan): Check for invalid UUID 685 uuid := resp.Header.Get("Docker-Upload-UUID") 686 location, err := sanitizeLocation(resp.Header.Get("Location"), u) 687 if err != nil { 688 return nil, err 689 } 690 691 return &httpBlobUpload{ 692 statter: bs.statter, 693 client: bs.client, 694 uuid: uuid, 695 startedAt: time.Now(), 696 location: location, 697 }, nil 698 default: 699 return nil, HandleErrorResponse(resp) 700 } 701 } 702 703 func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { 704 panic("not implemented") 705 } 706 707 func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error { 708 return bs.statter.Clear(ctx, dgst) 709 } 710 711 type blobStatter struct { 712 name reference.Named 713 ub *v2.URLBuilder 714 client *http.Client 715 } 716 717 func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { 718 ref, err := reference.WithDigest(bs.name, dgst) 719 if err != nil { 720 return distribution.Descriptor{}, err 721 } 722 u, err := bs.ub.BuildBlobURL(ref) 723 if err != nil { 724 return distribution.Descriptor{}, err 725 } 726 727 resp, err := bs.client.Head(u) 728 if err != nil { 729 return distribution.Descriptor{}, err 730 } 731 defer resp.Body.Close() 732 733 if SuccessStatus(resp.StatusCode) { 734 lengthHeader := resp.Header.Get("Content-Length") 735 if lengthHeader == "" { 736 return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u) 737 } 738 739 length, err := strconv.ParseInt(lengthHeader, 10, 64) 740 if err != nil { 741 return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err) 742 } 743 744 return distribution.Descriptor{ 745 MediaType: resp.Header.Get("Content-Type"), 746 Size: length, 747 Digest: dgst, 748 }, nil 749 } else if resp.StatusCode == http.StatusNotFound { 750 return distribution.Descriptor{}, distribution.ErrBlobUnknown 751 } 752 return distribution.Descriptor{}, HandleErrorResponse(resp) 753 } 754 755 func buildCatalogValues(maxEntries int, last string) url.Values { 756 values := url.Values{} 757 758 if maxEntries > 0 { 759 values.Add("n", strconv.Itoa(maxEntries)) 760 } 761 762 if last != "" { 763 values.Add("last", last) 764 } 765 766 return values 767 } 768 769 func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error { 770 ref, err := reference.WithDigest(bs.name, dgst) 771 if err != nil { 772 return err 773 } 774 blobURL, err := bs.ub.BuildBlobURL(ref) 775 if err != nil { 776 return err 777 } 778 779 req, err := http.NewRequest("DELETE", blobURL, nil) 780 if err != nil { 781 return err 782 } 783 784 resp, err := bs.client.Do(req) 785 if err != nil { 786 return err 787 } 788 defer resp.Body.Close() 789 790 if SuccessStatus(resp.StatusCode) { 791 return nil 792 } 793 return HandleErrorResponse(resp) 794 } 795 796 func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { 797 return nil 798 }