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