github.com/demonoid81/containerd@v1.3.4/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.Cause(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 or 204.
   344  	switch resp.StatusCode {
   345  	case http.StatusOK, http.StatusCreated, http.StatusNoContent:
   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  }