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

     1  package handlers
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"github.com/docker/distribution"
     9  	ctxu "github.com/docker/distribution/context"
    10  	"github.com/docker/distribution/digest"
    11  	"github.com/docker/distribution/manifest/manifestlist"
    12  	"github.com/docker/distribution/manifest/schema1"
    13  	"github.com/docker/distribution/manifest/schema2"
    14  	"github.com/docker/distribution/reference"
    15  	"github.com/docker/distribution/registry/api/errcode"
    16  	"github.com/docker/distribution/registry/api/v2"
    17  	"github.com/gorilla/handlers"
    18  )
    19  
    20  // These constants determine which architecture and OS to choose from a
    21  // manifest list when downconverting it to a schema1 manifest.
    22  const (
    23  	defaultArch = "amd64"
    24  	defaultOS   = "linux"
    25  )
    26  
    27  // imageManifestDispatcher takes the request context and builds the
    28  // appropriate handler for handling image manifest requests.
    29  func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
    30  	imageManifestHandler := &imageManifestHandler{
    31  		Context: ctx,
    32  	}
    33  	reference := getReference(ctx)
    34  	dgst, err := digest.ParseDigest(reference)
    35  	if err != nil {
    36  		// We just have a tag
    37  		imageManifestHandler.Tag = reference
    38  	} else {
    39  		imageManifestHandler.Digest = dgst
    40  	}
    41  
    42  	mhandler := handlers.MethodHandler{
    43  		"GET":  http.HandlerFunc(imageManifestHandler.GetImageManifest),
    44  		"HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest),
    45  	}
    46  
    47  	if !ctx.readOnly {
    48  		mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest)
    49  		mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest)
    50  	}
    51  
    52  	return mhandler
    53  }
    54  
    55  // imageManifestHandler handles http operations on image manifests.
    56  type imageManifestHandler struct {
    57  	*Context
    58  
    59  	// One of tag or digest gets set, depending on what is present in context.
    60  	Tag    string
    61  	Digest digest.Digest
    62  }
    63  
    64  // GetImageManifest fetches the image manifest from the storage backend, if it exists.
    65  func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
    66  	ctxu.GetLogger(imh).Debug("GetImageManifest")
    67  	manifests, err := imh.Repository.Manifests(imh)
    68  	if err != nil {
    69  		imh.Errors = append(imh.Errors, err)
    70  		return
    71  	}
    72  
    73  	var manifest distribution.Manifest
    74  	if imh.Tag != "" {
    75  		tags := imh.Repository.Tags(imh)
    76  		desc, err := tags.Get(imh, imh.Tag)
    77  		if err != nil {
    78  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
    79  			return
    80  		}
    81  		imh.Digest = desc.Digest
    82  	}
    83  
    84  	if etagMatch(r, imh.Digest.String()) {
    85  		w.WriteHeader(http.StatusNotModified)
    86  		return
    87  	}
    88  
    89  	manifest, err = manifests.Get(imh, imh.Digest)
    90  	if err != nil {
    91  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
    92  		return
    93  	}
    94  
    95  	supportsSchema2 := false
    96  	supportsManifestList := false
    97  	if acceptHeaders, ok := r.Header["Accept"]; ok {
    98  		for _, mediaType := range acceptHeaders {
    99  			if mediaType == schema2.MediaTypeManifest {
   100  				supportsSchema2 = true
   101  			}
   102  			if mediaType == manifestlist.MediaTypeManifestList {
   103  				supportsManifestList = true
   104  			}
   105  		}
   106  	}
   107  
   108  	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
   109  	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
   110  
   111  	// Only rewrite schema2 manifests when they are being fetched by tag.
   112  	// If they are being fetched by digest, we can't return something not
   113  	// matching the digest.
   114  	if imh.Tag != "" && isSchema2 && !supportsSchema2 {
   115  		// Rewrite manifest in schema1 format
   116  		ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
   117  
   118  		manifest, err = imh.convertSchema2Manifest(schema2Manifest)
   119  		if err != nil {
   120  			return
   121  		}
   122  	} else if imh.Tag != "" && isManifestList && !supportsManifestList {
   123  		// Rewrite manifest in schema1 format
   124  		ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String())
   125  
   126  		// Find the image manifest corresponding to the default
   127  		// platform
   128  		var manifestDigest digest.Digest
   129  		for _, manifestDescriptor := range manifestList.Manifests {
   130  			if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS {
   131  				manifestDigest = manifestDescriptor.Digest
   132  				break
   133  			}
   134  		}
   135  
   136  		if manifestDigest == "" {
   137  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
   138  			return
   139  		}
   140  
   141  		manifest, err = manifests.Get(imh, manifestDigest)
   142  		if err != nil {
   143  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
   144  			return
   145  		}
   146  
   147  		// If necessary, convert the image manifest
   148  		if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
   149  			manifest, err = imh.convertSchema2Manifest(schema2Manifest)
   150  			if err != nil {
   151  				return
   152  			}
   153  		}
   154  	}
   155  
   156  	ct, p, err := manifest.Payload()
   157  	if err != nil {
   158  		return
   159  	}
   160  
   161  	w.Header().Set("Content-Type", ct)
   162  	w.Header().Set("Content-Length", fmt.Sprint(len(p)))
   163  	w.Header().Set("Docker-Content-Digest", imh.Digest.String())
   164  	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
   165  	w.Write(p)
   166  }
   167  
   168  func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) {
   169  	targetDescriptor := schema2Manifest.Target()
   170  	blobs := imh.Repository.Blobs(imh)
   171  	configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
   172  	if err != nil {
   173  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   174  		return nil, err
   175  	}
   176  
   177  	ref := imh.Repository.Name()
   178  
   179  	if imh.Tag != "" {
   180  		ref, err = reference.WithTag(imh.Repository.Name(), imh.Tag)
   181  		if err != nil {
   182  			imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail(err))
   183  			return nil, err
   184  		}
   185  	}
   186  
   187  	builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, ref, configJSON)
   188  	for _, d := range schema2Manifest.References() {
   189  		if err := builder.AppendReference(d); err != nil {
   190  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   191  			return nil, err
   192  		}
   193  	}
   194  	manifest, err := builder.Build(imh)
   195  	if err != nil {
   196  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   197  		return nil, err
   198  	}
   199  	imh.Digest = digest.FromBytes(manifest.(*schema1.SignedManifest).Canonical)
   200  
   201  	return manifest, nil
   202  }
   203  
   204  func etagMatch(r *http.Request, etag string) bool {
   205  	for _, headerVal := range r.Header["If-None-Match"] {
   206  		if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
   207  			return true
   208  		}
   209  	}
   210  	return false
   211  }
   212  
   213  // PutImageManifest validates and stores an image in the registry.
   214  func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) {
   215  	ctxu.GetLogger(imh).Debug("PutImageManifest")
   216  	manifests, err := imh.Repository.Manifests(imh)
   217  	if err != nil {
   218  		imh.Errors = append(imh.Errors, err)
   219  		return
   220  	}
   221  
   222  	var jsonBuf bytes.Buffer
   223  	if err := copyFullPayload(w, r, &jsonBuf, imh, "image manifest PUT", &imh.Errors); err != nil {
   224  		// copyFullPayload reports the error if necessary
   225  		return
   226  	}
   227  
   228  	mediaType := r.Header.Get("Content-Type")
   229  	manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes())
   230  	if err != nil {
   231  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   232  		return
   233  	}
   234  
   235  	if imh.Digest != "" {
   236  		if desc.Digest != imh.Digest {
   237  			ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest)
   238  			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
   239  			return
   240  		}
   241  	} else if imh.Tag != "" {
   242  		imh.Digest = desc.Digest
   243  	} else {
   244  		imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified"))
   245  		return
   246  	}
   247  
   248  	_, err = manifests.Put(imh, manifest)
   249  	if err != nil {
   250  		// TODO(stevvooe): These error handling switches really need to be
   251  		// handled by an app global mapper.
   252  		if err == distribution.ErrUnsupported {
   253  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
   254  			return
   255  		}
   256  		switch err := err.(type) {
   257  		case distribution.ErrManifestVerification:
   258  			for _, verificationError := range err {
   259  				switch verificationError := verificationError.(type) {
   260  				case distribution.ErrManifestBlobUnknown:
   261  					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest))
   262  				case distribution.ErrManifestNameInvalid:
   263  					imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err))
   264  				case distribution.ErrManifestUnverified:
   265  					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified)
   266  				default:
   267  					if verificationError == digest.ErrDigestInvalidFormat {
   268  						imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
   269  					} else {
   270  						imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError)
   271  					}
   272  				}
   273  			}
   274  		default:
   275  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   276  		}
   277  
   278  		return
   279  	}
   280  
   281  	// Tag this manifest
   282  	if imh.Tag != "" {
   283  		tags := imh.Repository.Tags(imh)
   284  		err = tags.Tag(imh, imh.Tag, desc)
   285  		if err != nil {
   286  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   287  			return
   288  		}
   289  
   290  	}
   291  
   292  	// Construct a canonical url for the uploaded manifest.
   293  	ref, err := reference.WithDigest(imh.Repository.Name(), imh.Digest)
   294  	if err != nil {
   295  		imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   296  		return
   297  	}
   298  
   299  	location, err := imh.urlBuilder.BuildManifestURL(ref)
   300  	if err != nil {
   301  		// NOTE(stevvooe): Given the behavior above, this absurdly unlikely to
   302  		// happen. We'll log the error here but proceed as if it worked. Worst
   303  		// case, we set an empty location header.
   304  		ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err)
   305  	}
   306  
   307  	w.Header().Set("Location", location)
   308  	w.Header().Set("Docker-Content-Digest", imh.Digest.String())
   309  	w.WriteHeader(http.StatusCreated)
   310  }
   311  
   312  // DeleteImageManifest removes the manifest with the given digest from the registry.
   313  func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) {
   314  	ctxu.GetLogger(imh).Debug("DeleteImageManifest")
   315  
   316  	manifests, err := imh.Repository.Manifests(imh)
   317  	if err != nil {
   318  		imh.Errors = append(imh.Errors, err)
   319  		return
   320  	}
   321  
   322  	err = manifests.Delete(imh, imh.Digest)
   323  	if err != nil {
   324  		switch err {
   325  		case digest.ErrDigestUnsupported:
   326  		case digest.ErrDigestInvalidFormat:
   327  			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
   328  			return
   329  		case distribution.ErrBlobUnknown:
   330  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
   331  			return
   332  		case distribution.ErrUnsupported:
   333  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
   334  			return
   335  		default:
   336  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown)
   337  			return
   338  		}
   339  	}
   340  
   341  	tagService := imh.Repository.Tags(imh)
   342  	referencedTags, err := tagService.Lookup(imh, distribution.Descriptor{Digest: imh.Digest})
   343  	if err != nil {
   344  		imh.Errors = append(imh.Errors, err)
   345  		return
   346  	}
   347  
   348  	for _, tag := range referencedTags {
   349  		if err := tagService.Untag(imh, tag); err != nil {
   350  			imh.Errors = append(imh.Errors, err)
   351  			return
   352  		}
   353  	}
   354  
   355  	w.WriteHeader(http.StatusAccepted)
   356  }