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