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