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