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(®istryErr); 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 }