github.com/lalkh/containerd@v1.4.3/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(®istryErr); 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 }