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  }