231 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
| package handlers
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 
 | |
| 	"github.com/docker/distribution"
 | |
| 	ctxu "github.com/docker/distribution/context"
 | |
| 	"github.com/docker/distribution/digest"
 | |
| 	"github.com/docker/distribution/registry/api/errcode"
 | |
| 	"github.com/docker/distribution/registry/api/v2"
 | |
| 	"github.com/gorilla/handlers"
 | |
| )
 | |
| 
 | |
| // imageManifestDispatcher takes the request context and builds the
 | |
| // appropriate handler for handling image manifest requests.
 | |
| func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
 | |
| 	imageManifestHandler := &imageManifestHandler{
 | |
| 		Context: ctx,
 | |
| 	}
 | |
| 	reference := getReference(ctx)
 | |
| 	dgst, err := digest.ParseDigest(reference)
 | |
| 	if err != nil {
 | |
| 		// We just have a tag
 | |
| 		imageManifestHandler.Tag = reference
 | |
| 	} else {
 | |
| 		imageManifestHandler.Digest = dgst
 | |
| 	}
 | |
| 
 | |
| 	mhandler := handlers.MethodHandler{
 | |
| 		"GET":  http.HandlerFunc(imageManifestHandler.GetImageManifest),
 | |
| 		"HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest),
 | |
| 	}
 | |
| 
 | |
| 	if !ctx.readOnly {
 | |
| 		mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest)
 | |
| 		mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest)
 | |
| 	}
 | |
| 
 | |
| 	return mhandler
 | |
| }
 | |
| 
 | |
| // imageManifestHandler handles http operations on image manifests.
 | |
| type imageManifestHandler struct {
 | |
| 	*Context
 | |
| 
 | |
| 	// One of tag or digest gets set, depending on what is present in context.
 | |
| 	Tag    string
 | |
| 	Digest digest.Digest
 | |
| }
 | |
| 
 | |
| // GetImageManifest fetches the image manifest from the storage backend, if it exists.
 | |
| // todo(richardscothern): this assumes v2 schema 1 manifests for now but in the future
 | |
| // get the version from the Accept HTTP header
 | |
| func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctxu.GetLogger(imh).Debug("GetImageManifest")
 | |
| 	manifests, err := imh.Repository.Manifests(imh)
 | |
| 	if err != nil {
 | |
| 		imh.Errors = append(imh.Errors, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var manifest distribution.Manifest
 | |
| 	if imh.Tag != "" {
 | |
| 		tags := imh.Repository.Tags(imh)
 | |
| 		desc, err := tags.Get(imh, imh.Tag)
 | |
| 		if err != nil {
 | |
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
 | |
| 			return
 | |
| 		}
 | |
| 		imh.Digest = desc.Digest
 | |
| 	}
 | |
| 
 | |
| 	if etagMatch(r, imh.Digest.String()) {
 | |
| 		w.WriteHeader(http.StatusNotModified)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	manifest, err = manifests.Get(imh, imh.Digest)
 | |
| 	if err != nil {
 | |
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ct, p, err := manifest.Payload()
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	w.Header().Set("Content-Type", ct)
 | |
| 	w.Header().Set("Content-Length", fmt.Sprint(len(p)))
 | |
| 	w.Header().Set("Docker-Content-Digest", imh.Digest.String())
 | |
| 	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
 | |
| 	w.Write(p)
 | |
| }
 | |
| 
 | |
| func etagMatch(r *http.Request, etag string) bool {
 | |
| 	for _, headerVal := range r.Header["If-None-Match"] {
 | |
| 		if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // PutImageManifest validates and stores an image in the registry.
 | |
| func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctxu.GetLogger(imh).Debug("PutImageManifest")
 | |
| 	manifests, err := imh.Repository.Manifests(imh)
 | |
| 	if err != nil {
 | |
| 		imh.Errors = append(imh.Errors, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var jsonBuf bytes.Buffer
 | |
| 	if err := copyFullPayload(w, r, &jsonBuf, imh, "image manifest PUT", &imh.Errors); err != nil {
 | |
| 		// copyFullPayload reports the error if necessary
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	mediaType := r.Header.Get("Content-Type")
 | |
| 	manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes())
 | |
| 	if err != nil {
 | |
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if imh.Digest != "" {
 | |
| 		if desc.Digest != imh.Digest {
 | |
| 			ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest)
 | |
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
 | |
| 			return
 | |
| 		}
 | |
| 	} else if imh.Tag != "" {
 | |
| 		imh.Digest = desc.Digest
 | |
| 	} else {
 | |
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified"))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	_, err = manifests.Put(imh, manifest)
 | |
| 	if err != nil {
 | |
| 		// TODO(stevvooe): These error handling switches really need to be
 | |
| 		// handled by an app global mapper.
 | |
| 		if err == distribution.ErrUnsupported {
 | |
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
 | |
| 			return
 | |
| 		}
 | |
| 		switch err := err.(type) {
 | |
| 		case distribution.ErrManifestVerification:
 | |
| 			for _, verificationError := range err {
 | |
| 				switch verificationError := verificationError.(type) {
 | |
| 				case distribution.ErrManifestBlobUnknown:
 | |
| 					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest))
 | |
| 				case distribution.ErrManifestNameInvalid:
 | |
| 					imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err))
 | |
| 				case distribution.ErrManifestUnverified:
 | |
| 					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified)
 | |
| 				default:
 | |
| 					if verificationError == digest.ErrDigestInvalidFormat {
 | |
| 						imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
 | |
| 					} else {
 | |
| 						imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		default:
 | |
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
 | |
| 		}
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Tag this manifest
 | |
| 	if imh.Tag != "" {
 | |
| 		tags := imh.Repository.Tags(imh)
 | |
| 		err = tags.Tag(imh, imh.Tag, desc)
 | |
| 		if err != nil {
 | |
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| 
 | |
| 	// Construct a canonical url for the uploaded manifest.
 | |
| 	location, err := imh.urlBuilder.BuildManifestURL(imh.Repository.Name(), imh.Digest.String())
 | |
| 	if err != nil {
 | |
| 		// NOTE(stevvooe): Given the behavior above, this absurdly unlikely to
 | |
| 		// happen. We'll log the error here but proceed as if it worked. Worst
 | |
| 		// case, we set an empty location header.
 | |
| 		ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	w.Header().Set("Location", location)
 | |
| 	w.Header().Set("Docker-Content-Digest", imh.Digest.String())
 | |
| 	w.WriteHeader(http.StatusCreated)
 | |
| }
 | |
| 
 | |
| // DeleteImageManifest removes the manifest with the given digest from the registry.
 | |
| func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctxu.GetLogger(imh).Debug("DeleteImageManifest")
 | |
| 
 | |
| 	manifests, err := imh.Repository.Manifests(imh)
 | |
| 	if err != nil {
 | |
| 		imh.Errors = append(imh.Errors, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	err = manifests.Delete(imh, imh.Digest)
 | |
| 	if err != nil {
 | |
| 		switch err {
 | |
| 		case digest.ErrDigestUnsupported:
 | |
| 		case digest.ErrDigestInvalidFormat:
 | |
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
 | |
| 			return
 | |
| 		case distribution.ErrBlobUnknown:
 | |
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
 | |
| 			return
 | |
| 		case distribution.ErrUnsupported:
 | |
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
 | |
| 			return
 | |
| 		default:
 | |
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	w.WriteHeader(http.StatusAccepted)
 | |
| }
 |