github.com/containerd/Containerd@v1.4.13/remotes/docker/pusher.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 "io" 22 "io/ioutil" 23 "net/http" 24 "net/url" 25 "strings" 26 "time" 27 28 "github.com/containerd/containerd/content" 29 "github.com/containerd/containerd/errdefs" 30 "github.com/containerd/containerd/images" 31 "github.com/containerd/containerd/log" 32 "github.com/containerd/containerd/remotes" 33 digest "github.com/opencontainers/go-digest" 34 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 35 "github.com/pkg/errors" 36 ) 37 38 type dockerPusher struct { 39 *dockerBase 40 object string 41 42 // TODO: namespace tracker 43 tracker StatusTracker 44 } 45 46 func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (content.Writer, error) { 47 ctx, err := contextWithRepositoryScope(ctx, p.refspec, true) 48 if err != nil { 49 return nil, err 50 } 51 ref := remotes.MakeRefKey(ctx, desc) 52 status, err := p.tracker.GetStatus(ref) 53 if err == nil { 54 if status.Offset == status.Total { 55 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "ref %v", ref) 56 } 57 // TODO: Handle incomplete status 58 } else if !errdefs.IsNotFound(err) { 59 return nil, errors.Wrap(err, "failed to get status") 60 } 61 62 hosts := p.filterHosts(HostCapabilityPush) 63 if len(hosts) == 0 { 64 return nil, errors.Wrap(errdefs.ErrNotFound, "no push hosts") 65 } 66 67 var ( 68 isManifest bool 69 existCheck []string 70 host = hosts[0] 71 ) 72 73 switch desc.MediaType { 74 case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList, 75 ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: 76 isManifest = true 77 existCheck = getManifestPath(p.object, desc.Digest) 78 default: 79 existCheck = []string{"blobs", desc.Digest.String()} 80 } 81 82 req := p.request(host, http.MethodHead, existCheck...) 83 req.header.Set("Accept", strings.Join([]string{desc.MediaType, `*/*`}, ", ")) 84 85 log.G(ctx).WithField("url", req.String()).Debugf("checking and pushing to") 86 87 resp, err := req.doWithRetries(ctx, nil) 88 if err != nil { 89 if !errors.Is(err, ErrInvalidAuthorization) { 90 return nil, err 91 } 92 log.G(ctx).WithError(err).Debugf("Unable to check existence, continuing with push") 93 } else { 94 if resp.StatusCode == http.StatusOK { 95 var exists bool 96 if isManifest && existCheck[1] != desc.Digest.String() { 97 dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest")) 98 if dgstHeader == desc.Digest { 99 exists = true 100 } 101 } else { 102 exists = true 103 } 104 105 if exists { 106 p.tracker.SetStatus(ref, Status{ 107 Status: content.Status{ 108 Ref: ref, 109 // TODO: Set updated time? 110 }, 111 }) 112 resp.Body.Close() 113 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) 114 } 115 } else if resp.StatusCode != http.StatusNotFound { 116 resp.Body.Close() 117 // TODO: log error 118 return nil, errors.Errorf("unexpected response: %s", resp.Status) 119 } 120 resp.Body.Close() 121 } 122 123 if isManifest { 124 putPath := getManifestPath(p.object, desc.Digest) 125 req = p.request(host, http.MethodPut, putPath...) 126 req.header.Add("Content-Type", desc.MediaType) 127 } else { 128 // Start upload request 129 req = p.request(host, http.MethodPost, "blobs", "uploads/") 130 131 var resp *http.Response 132 if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" { 133 preq := requestWithMountFrom(req, desc.Digest.String(), fromRepo) 134 pctx := contextWithAppendPullRepositoryScope(ctx, fromRepo) 135 136 // NOTE: the fromRepo might be private repo and 137 // auth service still can grant token without error. 138 // but the post request will fail because of 401. 139 // 140 // for the private repo, we should remove mount-from 141 // query and send the request again. 142 resp, err = preq.doWithRetries(pctx, nil) 143 if err != nil { 144 return nil, err 145 } 146 147 if resp.StatusCode == http.StatusUnauthorized { 148 log.G(ctx).Debugf("failed to mount from repository %s", fromRepo) 149 150 resp.Body.Close() 151 resp = nil 152 } 153 } 154 155 if resp == nil { 156 resp, err = req.doWithRetries(ctx, nil) 157 if err != nil { 158 return nil, err 159 } 160 } 161 defer resp.Body.Close() 162 163 switch resp.StatusCode { 164 case http.StatusOK, http.StatusAccepted, http.StatusNoContent: 165 case http.StatusCreated: 166 p.tracker.SetStatus(ref, Status{ 167 Status: content.Status{ 168 Ref: ref, 169 }, 170 }) 171 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) 172 default: 173 // TODO: log error 174 return nil, errors.Errorf("unexpected response: %s", resp.Status) 175 } 176 177 var ( 178 location = resp.Header.Get("Location") 179 lurl *url.URL 180 lhost = host 181 ) 182 // Support paths without host in location 183 if strings.HasPrefix(location, "/") { 184 lurl, err = url.Parse(lhost.Scheme + "://" + lhost.Host + location) 185 if err != nil { 186 return nil, errors.Wrapf(err, "unable to parse location %v", location) 187 } 188 } else { 189 if !strings.Contains(location, "://") { 190 location = lhost.Scheme + "://" + location 191 } 192 lurl, err = url.Parse(location) 193 if err != nil { 194 return nil, errors.Wrapf(err, "unable to parse location %v", location) 195 } 196 197 if lurl.Host != lhost.Host || lhost.Scheme != lurl.Scheme { 198 199 lhost.Scheme = lurl.Scheme 200 lhost.Host = lurl.Host 201 log.G(ctx).WithField("host", lhost.Host).WithField("scheme", lhost.Scheme).Debug("upload changed destination") 202 203 // Strip authorizer if change to host or scheme 204 lhost.Authorizer = nil 205 } 206 } 207 q := lurl.Query() 208 q.Add("digest", desc.Digest.String()) 209 210 req = p.request(lhost, http.MethodPut) 211 req.header.Set("Content-Type", "application/octet-stream") 212 req.path = lurl.Path + "?" + q.Encode() 213 } 214 p.tracker.SetStatus(ref, Status{ 215 Status: content.Status{ 216 Ref: ref, 217 Total: desc.Size, 218 Expected: desc.Digest, 219 StartedAt: time.Now(), 220 }, 221 }) 222 223 // TODO: Support chunked upload 224 225 pr, pw := io.Pipe() 226 respC := make(chan *http.Response, 1) 227 body := ioutil.NopCloser(pr) 228 229 req.body = func() (io.ReadCloser, error) { 230 if body == nil { 231 return nil, errors.New("cannot reuse body, request must be retried") 232 } 233 // Only use the body once since pipe cannot be seeked 234 ob := body 235 body = nil 236 return ob, nil 237 } 238 req.size = desc.Size 239 240 go func() { 241 defer close(respC) 242 resp, err := req.doWithRetries(ctx, nil) 243 if err != nil { 244 pr.CloseWithError(err) 245 return 246 } 247 248 switch resp.StatusCode { 249 case http.StatusOK, http.StatusCreated, http.StatusNoContent: 250 default: 251 // TODO: log error 252 pr.CloseWithError(errors.Errorf("unexpected response: %s", resp.Status)) 253 } 254 respC <- resp 255 }() 256 257 return &pushWriter{ 258 base: p.dockerBase, 259 ref: ref, 260 pipe: pw, 261 responseC: respC, 262 isManifest: isManifest, 263 expected: desc.Digest, 264 tracker: p.tracker, 265 }, nil 266 } 267 268 func getManifestPath(object string, dgst digest.Digest) []string { 269 if i := strings.IndexByte(object, '@'); i >= 0 { 270 if object[i+1:] != dgst.String() { 271 // use digest, not tag 272 object = "" 273 } else { 274 // strip @<digest> for registry path to make tag 275 object = object[:i] 276 } 277 278 } 279 280 if object == "" { 281 return []string{"manifests", dgst.String()} 282 } 283 284 return []string{"manifests", object} 285 } 286 287 type pushWriter struct { 288 base *dockerBase 289 ref string 290 291 pipe *io.PipeWriter 292 responseC <-chan *http.Response 293 isManifest bool 294 295 expected digest.Digest 296 tracker StatusTracker 297 } 298 299 func (pw *pushWriter) Write(p []byte) (n int, err error) { 300 status, err := pw.tracker.GetStatus(pw.ref) 301 if err != nil { 302 return n, err 303 } 304 n, err = pw.pipe.Write(p) 305 status.Offset += int64(n) 306 status.UpdatedAt = time.Now() 307 pw.tracker.SetStatus(pw.ref, status) 308 return 309 } 310 311 func (pw *pushWriter) Close() error { 312 return pw.pipe.Close() 313 } 314 315 func (pw *pushWriter) Status() (content.Status, error) { 316 status, err := pw.tracker.GetStatus(pw.ref) 317 if err != nil { 318 return content.Status{}, err 319 } 320 return status.Status, nil 321 322 } 323 324 func (pw *pushWriter) Digest() digest.Digest { 325 // TODO: Get rid of this function? 326 return pw.expected 327 } 328 329 func (pw *pushWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error { 330 // Check whether read has already thrown an error 331 if _, err := pw.pipe.Write([]byte{}); err != nil && err != io.ErrClosedPipe { 332 return errors.Wrap(err, "pipe error before commit") 333 } 334 335 if err := pw.pipe.Close(); err != nil { 336 return err 337 } 338 // TODO: Update status to determine committing 339 340 // TODO: timeout waiting for response 341 resp := <-pw.responseC 342 if resp == nil { 343 return errors.New("no response") 344 } 345 defer resp.Body.Close() 346 347 // 201 is specified return status, some registries return 348 // 200, 202 or 204. 349 switch resp.StatusCode { 350 case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: 351 default: 352 return errors.Errorf("unexpected status: %s", resp.Status) 353 } 354 355 status, err := pw.tracker.GetStatus(pw.ref) 356 if err != nil { 357 return errors.Wrap(err, "failed to get status") 358 } 359 360 if size > 0 && size != status.Offset { 361 return errors.Errorf("unexpected size %d, expected %d", status.Offset, size) 362 } 363 364 if expected == "" { 365 expected = status.Expected 366 } 367 368 actual, err := digest.Parse(resp.Header.Get("Docker-Content-Digest")) 369 if err != nil { 370 return errors.Wrap(err, "invalid content digest in response") 371 } 372 373 if actual != expected { 374 return errors.Errorf("got digest %s, expected %s", actual, expected) 375 } 376 377 return nil 378 } 379 380 func (pw *pushWriter) Truncate(size int64) error { 381 // TODO: if blob close request and start new request at offset 382 // TODO: always error on manifest 383 return errors.New("cannot truncate remote upload") 384 } 385 386 func requestWithMountFrom(req *request, mount, from string) *request { 387 creq := *req 388 389 sep := "?" 390 if strings.Contains(creq.path, sep) { 391 sep = "&" 392 } 393 394 creq.path = creq.path + sep + "mount=" + mount + "&from=" + from 395 396 return &creq 397 }