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