github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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 remoteserrors "github.com/containerd/containerd/remotes/errors" 34 digest "github.com/opencontainers/go-digest" 35 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 36 "github.com/pkg/errors" 37 ) 38 39 type dockerPusher struct { 40 *dockerBase 41 object string 42 43 // TODO: namespace tracker 44 tracker StatusTracker 45 } 46 47 func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (content.Writer, error) { 48 ctx, err := contextWithRepositoryScope(ctx, p.refspec, true) 49 if err != nil { 50 return nil, err 51 } 52 ref := remotes.MakeRefKey(ctx, desc) 53 status, err := p.tracker.GetStatus(ref) 54 if err == nil { 55 if status.Offset == status.Total { 56 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "ref %v", ref) 57 } 58 // TODO: Handle incomplete status 59 } else if !errdefs.IsNotFound(err) { 60 return nil, errors.Wrap(err, "failed to get status") 61 } 62 63 hosts := p.filterHosts(HostCapabilityPush) 64 if len(hosts) == 0 { 65 return nil, errors.Wrap(errdefs.ErrNotFound, "no push hosts") 66 } 67 68 var ( 69 isManifest bool 70 existCheck []string 71 host = hosts[0] 72 ) 73 74 switch desc.MediaType { 75 case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList, 76 ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: 77 isManifest = true 78 existCheck = getManifestPath(p.object, desc.Digest) 79 default: 80 existCheck = []string{"blobs", desc.Digest.String()} 81 } 82 83 req := p.request(host, http.MethodHead, existCheck...) 84 req.header.Set("Accept", strings.Join([]string{desc.MediaType, `*/*`}, ", ")) 85 86 log.G(ctx).WithField("url", req.String()).Debugf("checking and pushing to") 87 88 resp, err := req.doWithRetries(ctx, nil) 89 if err != nil { 90 if !errors.Is(err, ErrInvalidAuthorization) { 91 return nil, err 92 } 93 log.G(ctx).WithError(err).Debugf("Unable to check existence, continuing with push") 94 } else { 95 if resp.StatusCode == http.StatusOK { 96 var exists bool 97 if isManifest && existCheck[1] != desc.Digest.String() { 98 dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest")) 99 if dgstHeader == desc.Digest { 100 exists = true 101 } 102 } else { 103 exists = true 104 } 105 106 if exists { 107 p.tracker.SetStatus(ref, Status{ 108 Status: content.Status{ 109 Ref: ref, 110 // TODO: Set updated time? 111 }, 112 }) 113 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) 114 } 115 } else if resp.StatusCode != http.StatusNotFound { 116 err := remoteserrors.NewUnexpectedStatusErr(resp) 117 log.G(ctx).WithField("resp", resp).WithField("body", string(err.(remoteserrors.ErrUnexpectedStatus).Body)).Debug("unexpected response") 118 return nil, err 119 } 120 } 121 122 if isManifest { 123 putPath := getManifestPath(p.object, desc.Digest) 124 req = p.request(host, http.MethodPut, putPath...) 125 req.header.Add("Content-Type", desc.MediaType) 126 } else { 127 // Start upload request 128 req = p.request(host, http.MethodPost, "blobs", "uploads/") 129 130 var resp *http.Response 131 if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" { 132 preq := requestWithMountFrom(req, desc.Digest.String(), fromRepo) 133 pctx := contextWithAppendPullRepositoryScope(ctx, fromRepo) 134 135 // NOTE: the fromRepo might be private repo and 136 // auth service still can grant token without error. 137 // but the post request will fail because of 401. 138 // 139 // for the private repo, we should remove mount-from 140 // query and send the request again. 141 resp, err = preq.do(pctx) 142 if err != nil { 143 return nil, err 144 } 145 146 if resp.StatusCode == http.StatusUnauthorized { 147 log.G(ctx).Debugf("failed to mount from repository %s", fromRepo) 148 149 resp.Body.Close() 150 resp = nil 151 } 152 } 153 154 if resp == nil { 155 resp, err = req.doWithRetries(ctx, nil) 156 if err != nil { 157 return nil, err 158 } 159 } 160 161 switch resp.StatusCode { 162 case http.StatusOK, http.StatusAccepted, http.StatusNoContent: 163 case http.StatusCreated: 164 p.tracker.SetStatus(ref, Status{ 165 Status: content.Status{ 166 Ref: ref, 167 }, 168 }) 169 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) 170 default: 171 err := remoteserrors.NewUnexpectedStatusErr(resp) 172 log.G(ctx).WithField("resp", resp).WithField("body", string(err.(remoteserrors.ErrUnexpectedStatus).Body)).Debug("unexpected response") 173 return nil, err 174 } 175 176 var ( 177 location = resp.Header.Get("Location") 178 lurl *url.URL 179 lhost = host 180 ) 181 // Support paths without host in location 182 if strings.HasPrefix(location, "/") { 183 lurl, err = url.Parse(lhost.Scheme + "://" + lhost.Host + location) 184 if err != nil { 185 return nil, errors.Wrapf(err, "unable to parse location %v", location) 186 } 187 } else { 188 if !strings.Contains(location, "://") { 189 location = lhost.Scheme + "://" + location 190 } 191 lurl, err = url.Parse(location) 192 if err != nil { 193 return nil, errors.Wrapf(err, "unable to parse location %v", location) 194 } 195 196 if lurl.Host != lhost.Host || lhost.Scheme != lurl.Scheme { 197 198 lhost.Scheme = lurl.Scheme 199 lhost.Host = lurl.Host 200 log.G(ctx).WithField("host", lhost.Host).WithField("scheme", lhost.Scheme).Debug("upload changed destination") 201 202 // Strip authorizer if change to host or scheme 203 lhost.Authorizer = nil 204 } 205 } 206 q := lurl.Query() 207 q.Add("digest", desc.Digest.String()) 208 209 req = p.request(lhost, http.MethodPut) 210 req.header.Set("Content-Type", "application/octet-stream") 211 req.path = lurl.Path + "?" + q.Encode() 212 } 213 p.tracker.SetStatus(ref, Status{ 214 Status: content.Status{ 215 Ref: ref, 216 Total: desc.Size, 217 Expected: desc.Digest, 218 StartedAt: time.Now(), 219 }, 220 }) 221 222 // TODO: Support chunked upload 223 224 pr, pw := io.Pipe() 225 respC := make(chan *http.Response, 1) 226 body := ioutil.NopCloser(pr) 227 228 req.body = func() (io.ReadCloser, error) { 229 if body == nil { 230 return nil, errors.New("cannot reuse body, request must be retried") 231 } 232 // Only use the body once since pipe cannot be seeked 233 ob := body 234 body = nil 235 return ob, nil 236 } 237 req.size = desc.Size 238 239 go func() { 240 defer close(respC) 241 resp, err := req.do(ctx) 242 if err != nil { 243 pr.CloseWithError(err) 244 return 245 } 246 247 switch resp.StatusCode { 248 case http.StatusOK, http.StatusCreated, http.StatusNoContent: 249 default: 250 err := remoteserrors.NewUnexpectedStatusErr(resp) 251 log.G(ctx).WithField("resp", resp).WithField("body", string(err.(remoteserrors.ErrUnexpectedStatus).Body)).Debug("unexpected response") 252 pr.CloseWithError(err) 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 346 // 201 is specified return status, some registries return 347 // 200, 202 or 204. 348 switch resp.StatusCode { 349 case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: 350 default: 351 return errors.Errorf("unexpected status: %s", resp.Status) 352 } 353 354 status, err := pw.tracker.GetStatus(pw.ref) 355 if err != nil { 356 return errors.Wrap(err, "failed to get status") 357 } 358 359 if size > 0 && size != status.Offset { 360 return errors.Errorf("unexpected size %d, expected %d", status.Offset, size) 361 } 362 363 if expected == "" { 364 expected = status.Expected 365 } 366 367 actual, err := digest.Parse(resp.Header.Get("Docker-Content-Digest")) 368 if err != nil { 369 return errors.Wrap(err, "invalid content digest in response") 370 } 371 372 if actual != expected { 373 return errors.Errorf("got digest %s, expected %s", actual, expected) 374 } 375 376 return nil 377 } 378 379 func (pw *pushWriter) Truncate(size int64) error { 380 // TODO: if blob close request and start new request at offset 381 // TODO: always error on manifest 382 return errors.New("cannot truncate remote upload") 383 } 384 385 func requestWithMountFrom(req *request, mount, from string) *request { 386 creq := *req 387 388 sep := "?" 389 if strings.Contains(creq.path, sep) { 390 sep = "&" 391 } 392 393 creq.path = creq.path + sep + "mount=" + mount + "&from=" + from 394 395 return &creq 396 }