github.com/lalkh/containerd@v1.4.3/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 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) 113 } 114 } else if resp.StatusCode != http.StatusNotFound { 115 // TODO: log error 116 return nil, errors.Errorf("unexpected response: %s", resp.Status) 117 } 118 } 119 120 if isManifest { 121 putPath := getManifestPath(p.object, desc.Digest) 122 req = p.request(host, http.MethodPut, putPath...) 123 req.header.Add("Content-Type", desc.MediaType) 124 } else { 125 // Start upload request 126 req = p.request(host, http.MethodPost, "blobs", "uploads/") 127 128 var resp *http.Response 129 if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" { 130 preq := requestWithMountFrom(req, desc.Digest.String(), fromRepo) 131 pctx := contextWithAppendPullRepositoryScope(ctx, fromRepo) 132 133 // NOTE: the fromRepo might be private repo and 134 // auth service still can grant token without error. 135 // but the post request will fail because of 401. 136 // 137 // for the private repo, we should remove mount-from 138 // query and send the request again. 139 resp, err = preq.do(pctx) 140 if err != nil { 141 return nil, err 142 } 143 144 if resp.StatusCode == http.StatusUnauthorized { 145 log.G(ctx).Debugf("failed to mount from repository %s", fromRepo) 146 147 resp.Body.Close() 148 resp = nil 149 } 150 } 151 152 if resp == nil { 153 resp, err = req.doWithRetries(ctx, nil) 154 if err != nil { 155 return nil, err 156 } 157 } 158 159 switch resp.StatusCode { 160 case http.StatusOK, http.StatusAccepted, http.StatusNoContent: 161 case http.StatusCreated: 162 p.tracker.SetStatus(ref, Status{ 163 Status: content.Status{ 164 Ref: ref, 165 }, 166 }) 167 return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) 168 default: 169 // TODO: log error 170 return nil, errors.Errorf("unexpected response: %s", resp.Status) 171 } 172 173 var ( 174 location = resp.Header.Get("Location") 175 lurl *url.URL 176 lhost = host 177 ) 178 // Support paths without host in location 179 if strings.HasPrefix(location, "/") { 180 lurl, err = url.Parse(lhost.Scheme + "://" + lhost.Host + location) 181 if err != nil { 182 return nil, errors.Wrapf(err, "unable to parse location %v", location) 183 } 184 } else { 185 if !strings.Contains(location, "://") { 186 location = lhost.Scheme + "://" + location 187 } 188 lurl, err = url.Parse(location) 189 if err != nil { 190 return nil, errors.Wrapf(err, "unable to parse location %v", location) 191 } 192 193 if lurl.Host != lhost.Host || lhost.Scheme != lurl.Scheme { 194 195 lhost.Scheme = lurl.Scheme 196 lhost.Host = lurl.Host 197 log.G(ctx).WithField("host", lhost.Host).WithField("scheme", lhost.Scheme).Debug("upload changed destination") 198 199 // Strip authorizer if change to host or scheme 200 lhost.Authorizer = nil 201 } 202 } 203 q := lurl.Query() 204 q.Add("digest", desc.Digest.String()) 205 206 req = p.request(lhost, http.MethodPut) 207 req.header.Set("Content-Type", "application/octet-stream") 208 req.path = lurl.Path + "?" + q.Encode() 209 } 210 p.tracker.SetStatus(ref, Status{ 211 Status: content.Status{ 212 Ref: ref, 213 Total: desc.Size, 214 Expected: desc.Digest, 215 StartedAt: time.Now(), 216 }, 217 }) 218 219 // TODO: Support chunked upload 220 221 pr, pw := io.Pipe() 222 respC := make(chan *http.Response, 1) 223 body := ioutil.NopCloser(pr) 224 225 req.body = func() (io.ReadCloser, error) { 226 if body == nil { 227 return nil, errors.New("cannot reuse body, request must be retried") 228 } 229 // Only use the body once since pipe cannot be seeked 230 ob := body 231 body = nil 232 return ob, nil 233 } 234 req.size = desc.Size 235 236 go func() { 237 defer close(respC) 238 resp, err := req.do(ctx) 239 if err != nil { 240 pr.CloseWithError(err) 241 return 242 } 243 244 switch resp.StatusCode { 245 case http.StatusOK, http.StatusCreated, http.StatusNoContent: 246 default: 247 // TODO: log error 248 pr.CloseWithError(errors.Errorf("unexpected response: %s", resp.Status)) 249 } 250 respC <- resp 251 }() 252 253 return &pushWriter{ 254 base: p.dockerBase, 255 ref: ref, 256 pipe: pw, 257 responseC: respC, 258 isManifest: isManifest, 259 expected: desc.Digest, 260 tracker: p.tracker, 261 }, nil 262 } 263 264 func getManifestPath(object string, dgst digest.Digest) []string { 265 if i := strings.IndexByte(object, '@'); i >= 0 { 266 if object[i+1:] != dgst.String() { 267 // use digest, not tag 268 object = "" 269 } else { 270 // strip @<digest> for registry path to make tag 271 object = object[:i] 272 } 273 274 } 275 276 if object == "" { 277 return []string{"manifests", dgst.String()} 278 } 279 280 return []string{"manifests", object} 281 } 282 283 type pushWriter struct { 284 base *dockerBase 285 ref string 286 287 pipe *io.PipeWriter 288 responseC <-chan *http.Response 289 isManifest bool 290 291 expected digest.Digest 292 tracker StatusTracker 293 } 294 295 func (pw *pushWriter) Write(p []byte) (n int, err error) { 296 status, err := pw.tracker.GetStatus(pw.ref) 297 if err != nil { 298 return n, err 299 } 300 n, err = pw.pipe.Write(p) 301 status.Offset += int64(n) 302 status.UpdatedAt = time.Now() 303 pw.tracker.SetStatus(pw.ref, status) 304 return 305 } 306 307 func (pw *pushWriter) Close() error { 308 return pw.pipe.Close() 309 } 310 311 func (pw *pushWriter) Status() (content.Status, error) { 312 status, err := pw.tracker.GetStatus(pw.ref) 313 if err != nil { 314 return content.Status{}, err 315 } 316 return status.Status, nil 317 318 } 319 320 func (pw *pushWriter) Digest() digest.Digest { 321 // TODO: Get rid of this function? 322 return pw.expected 323 } 324 325 func (pw *pushWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error { 326 // Check whether read has already thrown an error 327 if _, err := pw.pipe.Write([]byte{}); err != nil && err != io.ErrClosedPipe { 328 return errors.Wrap(err, "pipe error before commit") 329 } 330 331 if err := pw.pipe.Close(); err != nil { 332 return err 333 } 334 // TODO: Update status to determine committing 335 336 // TODO: timeout waiting for response 337 resp := <-pw.responseC 338 if resp == nil { 339 return errors.New("no response") 340 } 341 342 // 201 is specified return status, some registries return 343 // 200, 202 or 204. 344 switch resp.StatusCode { 345 case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: 346 default: 347 return errors.Errorf("unexpected status: %s", resp.Status) 348 } 349 350 status, err := pw.tracker.GetStatus(pw.ref) 351 if err != nil { 352 return errors.Wrap(err, "failed to get status") 353 } 354 355 if size > 0 && size != status.Offset { 356 return errors.Errorf("unexpected size %d, expected %d", status.Offset, size) 357 } 358 359 if expected == "" { 360 expected = status.Expected 361 } 362 363 actual, err := digest.Parse(resp.Header.Get("Docker-Content-Digest")) 364 if err != nil { 365 return errors.Wrap(err, "invalid content digest in response") 366 } 367 368 if actual != expected { 369 return errors.Errorf("got digest %s, expected %s", actual, expected) 370 } 371 372 return nil 373 } 374 375 func (pw *pushWriter) Truncate(size int64) error { 376 // TODO: if blob close request and start new request at offset 377 // TODO: always error on manifest 378 return errors.New("cannot truncate remote upload") 379 } 380 381 func requestWithMountFrom(req *request, mount, from string) *request { 382 creq := *req 383 384 sep := "?" 385 if strings.Contains(creq.path, sep) { 386 sep = "&" 387 } 388 389 creq.path = creq.path + sep + "mount=" + mount + "&from=" + from 390 391 return &creq 392 }