github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/handlers/blobupload.go (about)

     1  package handlers
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"os"
     8  
     9  	"github.com/docker/distribution"
    10  	ctxu "github.com/docker/distribution/context"
    11  	"github.com/docker/distribution/digest"
    12  	"github.com/docker/distribution/reference"
    13  	"github.com/docker/distribution/registry/api/errcode"
    14  	"github.com/docker/distribution/registry/api/v2"
    15  	"github.com/docker/distribution/registry/storage"
    16  	"github.com/gorilla/handlers"
    17  )
    18  
    19  // blobUploadDispatcher constructs and returns the blob upload handler for the
    20  // given request context.
    21  func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
    22  	buh := &blobUploadHandler{
    23  		Context: ctx,
    24  		UUID:    getUploadUUID(ctx),
    25  	}
    26  
    27  	handler := handlers.MethodHandler{
    28  		"GET":  http.HandlerFunc(buh.GetUploadStatus),
    29  		"HEAD": http.HandlerFunc(buh.GetUploadStatus),
    30  	}
    31  
    32  	if !ctx.readOnly {
    33  		handler["POST"] = http.HandlerFunc(buh.StartBlobUpload)
    34  		handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData)
    35  		handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete)
    36  		handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload)
    37  	}
    38  
    39  	if buh.UUID != "" {
    40  		state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
    41  		if err != nil {
    42  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    43  				ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err)
    44  				buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
    45  			})
    46  		}
    47  		buh.State = state
    48  
    49  		if state.Name != ctx.Repository.Name().Name() {
    50  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    51  				ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Name())
    52  				buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
    53  			})
    54  		}
    55  
    56  		if state.UUID != buh.UUID {
    57  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    58  				ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID)
    59  				buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
    60  			})
    61  		}
    62  
    63  		blobs := ctx.Repository.Blobs(buh)
    64  		upload, err := blobs.Resume(buh, buh.UUID)
    65  		if err != nil {
    66  			ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err)
    67  			if err == distribution.ErrBlobUploadUnknown {
    68  				return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    69  					buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err))
    70  				})
    71  			}
    72  
    73  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    74  				buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
    75  			})
    76  		}
    77  		buh.Upload = upload
    78  
    79  		if state.Offset > 0 {
    80  			// Seek the blob upload to the correct spot if it's non-zero.
    81  			// These error conditions should be rare and demonstrate really
    82  			// problems. We basically cancel the upload and tell the client to
    83  			// start over.
    84  			if nn, err := upload.Seek(buh.State.Offset, os.SEEK_SET); err != nil {
    85  				defer upload.Close()
    86  				ctxu.GetLogger(ctx).Infof("error seeking blob upload: %v", err)
    87  				return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    88  					buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
    89  					upload.Cancel(buh)
    90  				})
    91  			} else if nn != buh.State.Offset {
    92  				defer upload.Close()
    93  				ctxu.GetLogger(ctx).Infof("seek to wrong offest: %d != %d", nn, buh.State.Offset)
    94  				return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    95  					buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
    96  					upload.Cancel(buh)
    97  				})
    98  			}
    99  		}
   100  
   101  		return closeResources(handler, buh.Upload)
   102  	}
   103  
   104  	return handler
   105  }
   106  
   107  // blobUploadHandler handles the http blob upload process.
   108  type blobUploadHandler struct {
   109  	*Context
   110  
   111  	// UUID identifies the upload instance for the current request. Using UUID
   112  	// to key blob writers since this implementation uses UUIDs.
   113  	UUID string
   114  
   115  	Upload distribution.BlobWriter
   116  
   117  	State blobUploadState
   118  }
   119  
   120  // StartBlobUpload begins the blob upload process and allocates a server-side
   121  // blob writer session, optionally mounting the blob from a separate repository.
   122  func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) {
   123  	var options []distribution.BlobCreateOption
   124  
   125  	fromRepo := r.FormValue("from")
   126  	mountDigest := r.FormValue("mount")
   127  
   128  	if mountDigest != "" && fromRepo != "" {
   129  		opt, err := buh.createBlobMountOption(fromRepo, mountDigest)
   130  		if opt != nil && err == nil {
   131  			options = append(options, opt)
   132  		}
   133  	}
   134  
   135  	blobs := buh.Repository.Blobs(buh)
   136  	upload, err := blobs.Create(buh, options...)
   137  
   138  	if err != nil {
   139  		if ebm, ok := err.(distribution.ErrBlobMounted); ok {
   140  			if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil {
   141  				buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   142  			}
   143  		} else if err == distribution.ErrUnsupported {
   144  			buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported)
   145  		} else {
   146  			buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   147  		}
   148  		return
   149  	}
   150  
   151  	buh.Upload = upload
   152  	defer buh.Upload.Close()
   153  
   154  	if err := buh.blobUploadResponse(w, r, true); err != nil {
   155  		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   156  		return
   157  	}
   158  
   159  	w.Header().Set("Docker-Upload-UUID", buh.Upload.ID())
   160  	w.WriteHeader(http.StatusAccepted)
   161  }
   162  
   163  // GetUploadStatus returns the status of a given upload, identified by id.
   164  func (buh *blobUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
   165  	if buh.Upload == nil {
   166  		buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
   167  		return
   168  	}
   169  
   170  	// TODO(dmcgowan): Set last argument to false in blobUploadResponse when
   171  	// resumable upload is supported. This will enable returning a non-zero
   172  	// range for clients to begin uploading at an offset.
   173  	if err := buh.blobUploadResponse(w, r, true); err != nil {
   174  		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   175  		return
   176  	}
   177  
   178  	w.Header().Set("Docker-Upload-UUID", buh.UUID)
   179  	w.WriteHeader(http.StatusNoContent)
   180  }
   181  
   182  // PatchBlobData writes data to an upload.
   183  func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Request) {
   184  	if buh.Upload == nil {
   185  		buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
   186  		return
   187  	}
   188  
   189  	ct := r.Header.Get("Content-Type")
   190  	if ct != "" && ct != "application/octet-stream" {
   191  		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("Bad Content-Type")))
   192  		// TODO(dmcgowan): encode error
   193  		return
   194  	}
   195  
   196  	// TODO(dmcgowan): support Content-Range header to seek and write range
   197  
   198  	if err := copyFullPayload(w, r, buh.Upload, buh, "blob PATCH", &buh.Errors); err != nil {
   199  		// copyFullPayload reports the error if necessary
   200  		return
   201  	}
   202  
   203  	if err := buh.blobUploadResponse(w, r, false); err != nil {
   204  		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   205  		return
   206  	}
   207  
   208  	w.WriteHeader(http.StatusAccepted)
   209  }
   210  
   211  // PutBlobUploadComplete takes the final request of a blob upload. The
   212  // request may include all the blob data or no blob data. Any data
   213  // provided is received and verified. If successful, the blob is linked
   214  // into the blob store and 201 Created is returned with the canonical
   215  // url of the blob.
   216  func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *http.Request) {
   217  	if buh.Upload == nil {
   218  		buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
   219  		return
   220  	}
   221  
   222  	dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
   223  
   224  	if dgstStr == "" {
   225  		// no digest? return error, but allow retry.
   226  		buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest missing"))
   227  		return
   228  	}
   229  
   230  	dgst, err := digest.ParseDigest(dgstStr)
   231  	if err != nil {
   232  		// no digest? return error, but allow retry.
   233  		buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed"))
   234  		return
   235  	}
   236  
   237  	if err := copyFullPayload(w, r, buh.Upload, buh, "blob PUT", &buh.Errors); err != nil {
   238  		// copyFullPayload reports the error if necessary
   239  		return
   240  	}
   241  
   242  	desc, err := buh.Upload.Commit(buh, distribution.Descriptor{
   243  		Digest: dgst,
   244  
   245  		// TODO(stevvooe): This isn't wildly important yet, but we should
   246  		// really set the length and mediatype. For now, we can let the
   247  		// backend take care of this.
   248  	})
   249  
   250  	if err != nil {
   251  		switch err := err.(type) {
   252  		case distribution.ErrBlobInvalidDigest:
   253  			buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
   254  		default:
   255  			switch err {
   256  			case distribution.ErrUnsupported:
   257  				buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported)
   258  			case distribution.ErrBlobInvalidLength, distribution.ErrBlobDigestUnsupported:
   259  				buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
   260  			default:
   261  				ctxu.GetLogger(buh).Errorf("unknown error completing upload: %#v", err)
   262  				buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   263  			}
   264  
   265  		}
   266  
   267  		// Clean up the backend blob data if there was an error.
   268  		if err := buh.Upload.Cancel(buh); err != nil {
   269  			// If the cleanup fails, all we can do is observe and report.
   270  			ctxu.GetLogger(buh).Errorf("error canceling upload after error: %v", err)
   271  		}
   272  
   273  		return
   274  	}
   275  	if err := buh.writeBlobCreatedHeaders(w, desc); err != nil {
   276  		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   277  		return
   278  	}
   279  }
   280  
   281  // CancelBlobUpload cancels an in-progress upload of a blob.
   282  func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Request) {
   283  	if buh.Upload == nil {
   284  		buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
   285  		return
   286  	}
   287  
   288  	w.Header().Set("Docker-Upload-UUID", buh.UUID)
   289  	if err := buh.Upload.Cancel(buh); err != nil {
   290  		ctxu.GetLogger(buh).Errorf("error encountered canceling upload: %v", err)
   291  		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   292  	}
   293  
   294  	w.WriteHeader(http.StatusNoContent)
   295  }
   296  
   297  // blobUploadResponse provides a standard request for uploading blobs and
   298  // chunk responses. This sets the correct headers but the response status is
   299  // left to the caller. The fresh argument is used to ensure that new blob
   300  // uploads always start at a 0 offset. This allows disabling resumable push by
   301  // always returning a 0 offset on check status.
   302  func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error {
   303  
   304  	var offset int64
   305  	if !fresh {
   306  		var err error
   307  		offset, err = buh.Upload.Seek(0, os.SEEK_CUR)
   308  		if err != nil {
   309  			ctxu.GetLogger(buh).Errorf("unable get current offset of blob upload: %v", err)
   310  			return err
   311  		}
   312  	}
   313  
   314  	// TODO(stevvooe): Need a better way to manage the upload state automatically.
   315  	buh.State.Name = buh.Repository.Name().Name()
   316  	buh.State.UUID = buh.Upload.ID()
   317  	buh.State.Offset = offset
   318  	buh.State.StartedAt = buh.Upload.StartedAt()
   319  
   320  	token, err := hmacKey(buh.Config.HTTP.Secret).packUploadState(buh.State)
   321  	if err != nil {
   322  		ctxu.GetLogger(buh).Infof("error building upload state token: %s", err)
   323  		return err
   324  	}
   325  
   326  	uploadURL, err := buh.urlBuilder.BuildBlobUploadChunkURL(
   327  		buh.Repository.Name(), buh.Upload.ID(),
   328  		url.Values{
   329  			"_state": []string{token},
   330  		})
   331  	if err != nil {
   332  		ctxu.GetLogger(buh).Infof("error building upload url: %s", err)
   333  		return err
   334  	}
   335  
   336  	endRange := offset
   337  	if endRange > 0 {
   338  		endRange = endRange - 1
   339  	}
   340  
   341  	w.Header().Set("Docker-Upload-UUID", buh.UUID)
   342  	w.Header().Set("Location", uploadURL)
   343  	w.Header().Set("Content-Length", "0")
   344  	w.Header().Set("Range", fmt.Sprintf("0-%d", endRange))
   345  
   346  	return nil
   347  }
   348  
   349  // mountBlob attempts to mount a blob from another repository by its digest. If
   350  // successful, the blob is linked into the blob store and 201 Created is
   351  // returned with the canonical url of the blob.
   352  func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) {
   353  	dgst, err := digest.ParseDigest(mountDigest)
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	ref, err := reference.ParseNamed(fromRepo)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	canonical, err := reference.WithDigest(ref, dgst)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	return storage.WithMountFrom(canonical), nil
   369  }
   370  
   371  // writeBlobCreatedHeaders writes the standard headers describing a newly
   372  // created blob. A 201 Created is written as well as the canonical URL and
   373  // blob digest.
   374  func (buh *blobUploadHandler) writeBlobCreatedHeaders(w http.ResponseWriter, desc distribution.Descriptor) error {
   375  	ref, err := reference.WithDigest(buh.Repository.Name(), desc.Digest)
   376  	if err != nil {
   377  		return err
   378  	}
   379  	blobURL, err := buh.urlBuilder.BuildBlobURL(ref)
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	w.Header().Set("Location", blobURL)
   385  	w.Header().Set("Content-Length", "0")
   386  	w.Header().Set("Docker-Content-Digest", desc.Digest.String())
   387  	w.WriteHeader(http.StatusCreated)
   388  	return nil
   389  }