github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/remotes/docker/fetcher.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package docker
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  
    29  	"github.com/containerd/containerd/errdefs"
    30  	"github.com/containerd/containerd/images"
    31  	"github.com/containerd/containerd/log"
    32  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    33  	"github.com/pkg/errors"
    34  )
    35  
    36  type dockerFetcher struct {
    37  	*dockerBase
    38  }
    39  
    40  func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
    41  	ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", desc.Digest))
    42  
    43  	hosts := r.filterHosts(HostCapabilityPull)
    44  	if len(hosts) == 0 {
    45  		return nil, errors.Wrap(errdefs.ErrNotFound, "no pull hosts")
    46  	}
    47  
    48  	ctx, err := contextWithRepositoryScope(ctx, r.refspec, false)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	return newHTTPReadSeeker(desc.Size, func(offset int64) (io.ReadCloser, error) {
    54  		// firstly try fetch via external urls
    55  		for _, us := range desc.URLs {
    56  			ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", us))
    57  
    58  			u, err := url.Parse(us)
    59  			if err != nil {
    60  				log.G(ctx).WithError(err).Debug("failed to parse")
    61  				continue
    62  			}
    63  			log.G(ctx).Debug("trying alternative url")
    64  
    65  			// Try this first, parse it
    66  			host := RegistryHost{
    67  				Client:       http.DefaultClient,
    68  				Host:         u.Host,
    69  				Scheme:       u.Scheme,
    70  				Path:         u.Path,
    71  				Capabilities: HostCapabilityPull,
    72  			}
    73  			req := r.request(host, http.MethodGet)
    74  			// Strip namespace from base
    75  			req.path = u.Path
    76  			if u.RawQuery != "" {
    77  				req.path = req.path + "?" + u.RawQuery
    78  			}
    79  
    80  			rc, err := r.open(ctx, req, desc.MediaType, offset)
    81  			if err != nil {
    82  				if errdefs.IsNotFound(err) {
    83  					continue // try one of the other urls.
    84  				}
    85  
    86  				return nil, err
    87  			}
    88  
    89  			return rc, nil
    90  		}
    91  
    92  		// Try manifests endpoints for manifests types
    93  		switch desc.MediaType {
    94  		case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
    95  			images.MediaTypeDockerSchema1Manifest,
    96  			ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
    97  
    98  			var firstErr error
    99  			for _, host := range r.hosts {
   100  				req := r.request(host, http.MethodGet, "manifests", desc.Digest.String())
   101  				if err := req.addNamespace(r.refspec.Hostname()); err != nil {
   102  					return nil, err
   103  				}
   104  
   105  				rc, err := r.open(ctx, req, desc.MediaType, offset)
   106  				if err != nil {
   107  					// Store the error for referencing later
   108  					if firstErr == nil {
   109  						firstErr = err
   110  					}
   111  					continue // try another host
   112  				}
   113  
   114  				return rc, nil
   115  			}
   116  
   117  			return nil, firstErr
   118  		}
   119  
   120  		// Finally use blobs endpoints
   121  		var firstErr error
   122  		for _, host := range r.hosts {
   123  			req := r.request(host, http.MethodGet, "blobs", desc.Digest.String())
   124  			if err := req.addNamespace(r.refspec.Hostname()); err != nil {
   125  				return nil, err
   126  			}
   127  
   128  			rc, err := r.open(ctx, req, desc.MediaType, offset)
   129  			if err != nil {
   130  				// Store the error for referencing later
   131  				if firstErr == nil {
   132  					firstErr = err
   133  				}
   134  				continue // try another host
   135  			}
   136  
   137  			return rc, nil
   138  		}
   139  
   140  		if errdefs.IsNotFound(firstErr) {
   141  			firstErr = errors.Wrapf(errdefs.ErrNotFound,
   142  				"could not fetch content descriptor %v (%v) from remote",
   143  				desc.Digest, desc.MediaType)
   144  		}
   145  
   146  		return nil, firstErr
   147  
   148  	})
   149  }
   150  
   151  func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string, offset int64) (io.ReadCloser, error) {
   152  	req.header.Set("Accept", strings.Join([]string{mediatype, `*/*`}, ", "))
   153  
   154  	if offset > 0 {
   155  		// Note: "Accept-Ranges: bytes" cannot be trusted as some endpoints
   156  		// will return the header without supporting the range. The content
   157  		// range must always be checked.
   158  		req.header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
   159  	}
   160  
   161  	resp, err := req.doWithRetries(ctx, nil)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	if resp.StatusCode > 299 {
   167  		// TODO(stevvooe): When doing a offset specific request, we should
   168  		// really distinguish between a 206 and a 200. In the case of 200, we
   169  		// can discard the bytes, hiding the seek behavior from the
   170  		// implementation.
   171  		defer resp.Body.Close()
   172  
   173  		if resp.StatusCode == http.StatusNotFound {
   174  			return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", req.String())
   175  		}
   176  		var registryErr Errors
   177  		if err := json.NewDecoder(resp.Body).Decode(&registryErr); err != nil || registryErr.Len() < 1 {
   178  			return nil, errors.Errorf("unexpected status code %v: %v", req.String(), resp.Status)
   179  		}
   180  		return nil, errors.Errorf("unexpected status code %v: %s - Server message: %s", req.String(), resp.Status, registryErr.Error())
   181  	}
   182  	if offset > 0 {
   183  		cr := resp.Header.Get("content-range")
   184  		if cr != "" {
   185  			if !strings.HasPrefix(cr, fmt.Sprintf("bytes %d-", offset)) {
   186  				return nil, errors.Errorf("unhandled content range in response: %v", cr)
   187  
   188  			}
   189  		} else {
   190  			// TODO: Should any cases where use of content range
   191  			// without the proper header be considered?
   192  			// 206 responses?
   193  
   194  			// Discard up to offset
   195  			// Could use buffer pool here but this case should be rare
   196  			n, err := io.Copy(ioutil.Discard, io.LimitReader(resp.Body, offset))
   197  			if err != nil {
   198  				return nil, errors.Wrap(err, "failed to discard to offset")
   199  			}
   200  			if n != offset {
   201  				return nil, errors.Errorf("unable to discard to offset")
   202  			}
   203  
   204  		}
   205  	}
   206  
   207  	return resp.Body, nil
   208  }