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  }