...

Source file src/github.com/docker/distribution/registry/handlers/manifests.go

Documentation: github.com/docker/distribution/registry/handlers

     1  package handlers
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/distribution/reference"
    10  	"github.com/docker/distribution"
    11  	dcontext "github.com/docker/distribution/context"
    12  	"github.com/docker/distribution/manifest/manifestlist"
    13  	"github.com/docker/distribution/manifest/ocischema"
    14  	"github.com/docker/distribution/manifest/schema1"
    15  	"github.com/docker/distribution/manifest/schema2"
    16  	"github.com/docker/distribution/registry/api/errcode"
    17  	v2 "github.com/docker/distribution/registry/api/v2"
    18  	"github.com/docker/distribution/registry/auth"
    19  	"github.com/gorilla/handlers"
    20  	"github.com/opencontainers/go-digest"
    21  	v1 "github.com/opencontainers/image-spec/specs-go/v1"
    22  )
    23  
    24  // These constants determine which architecture and OS to choose from a
    25  // manifest list when downconverting it to a schema1 manifest.
    26  const (
    27  	defaultArch         = "amd64"
    28  	defaultOS           = "linux"
    29  	maxManifestBodySize = 4 << 20
    30  	imageClass          = "image"
    31  )
    32  
    33  type storageType int
    34  
    35  const (
    36  	manifestSchema1     storageType = iota // 0
    37  	manifestSchema2                        // 1
    38  	manifestlistSchema                     // 2
    39  	ociSchema                              // 3
    40  	ociImageIndexSchema                    // 4
    41  	numStorageTypes                        // 5
    42  )
    43  
    44  // manifestDispatcher takes the request context and builds the
    45  // appropriate handler for handling manifest requests.
    46  func manifestDispatcher(ctx *Context, r *http.Request) http.Handler {
    47  	manifestHandler := &manifestHandler{
    48  		Context: ctx,
    49  	}
    50  	reference := getReference(ctx)
    51  	dgst, err := digest.Parse(reference)
    52  	if err != nil {
    53  		// We just have a tag
    54  		manifestHandler.Tag = reference
    55  	} else {
    56  		manifestHandler.Digest = dgst
    57  	}
    58  
    59  	mhandler := handlers.MethodHandler{
    60  		"GET":  http.HandlerFunc(manifestHandler.GetManifest),
    61  		"HEAD": http.HandlerFunc(manifestHandler.GetManifest),
    62  	}
    63  
    64  	if !ctx.readOnly {
    65  		mhandler["PUT"] = http.HandlerFunc(manifestHandler.PutManifest)
    66  		mhandler["DELETE"] = http.HandlerFunc(manifestHandler.DeleteManifest)
    67  	}
    68  
    69  	return mhandler
    70  }
    71  
    72  // manifestHandler handles http operations on image manifests.
    73  type manifestHandler struct {
    74  	*Context
    75  
    76  	// One of tag or digest gets set, depending on what is present in context.
    77  	Tag    string
    78  	Digest digest.Digest
    79  }
    80  
    81  // GetManifest fetches the image manifest from the storage backend, if it exists.
    82  func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) {
    83  	dcontext.GetLogger(imh).Debug("GetImageManifest")
    84  	manifests, err := imh.Repository.Manifests(imh)
    85  	if err != nil {
    86  		imh.Errors = append(imh.Errors, err)
    87  		return
    88  	}
    89  	var supports [numStorageTypes]bool
    90  
    91  	// this parsing of Accept headers is not quite as full-featured as godoc.org's parser, but we don't care about "q=" values
    92  	// https://github.com/golang/gddo/blob/e91d4165076d7474d20abda83f92d15c7ebc3e81/httputil/header/header.go#L165-L202
    93  	for _, acceptHeader := range r.Header["Accept"] {
    94  		// r.Header[...] is a slice in case the request contains the same header more than once
    95  		// if the header isn't set, we'll get the zero value, which "range" will handle gracefully
    96  
    97  		// we need to split each header value on "," to get the full list of "Accept" values (per RFC 2616)
    98  		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
    99  		for _, mediaType := range strings.Split(acceptHeader, ",") {
   100  			// remove "; q=..." if present
   101  			if i := strings.Index(mediaType, ";"); i >= 0 {
   102  				mediaType = mediaType[:i]
   103  			}
   104  
   105  			// it's common (but not required) for Accept values to be space separated ("a/b, c/d, e/f")
   106  			mediaType = strings.TrimSpace(mediaType)
   107  
   108  			if mediaType == schema2.MediaTypeManifest {
   109  				supports[manifestSchema2] = true
   110  			}
   111  			if mediaType == manifestlist.MediaTypeManifestList {
   112  				supports[manifestlistSchema] = true
   113  			}
   114  			if mediaType == v1.MediaTypeImageManifest {
   115  				supports[ociSchema] = true
   116  			}
   117  			if mediaType == v1.MediaTypeImageIndex {
   118  				supports[ociImageIndexSchema] = true
   119  			}
   120  		}
   121  	}
   122  
   123  	if imh.Tag != "" {
   124  		tags := imh.Repository.Tags(imh)
   125  		desc, err := tags.Get(imh, imh.Tag)
   126  		if err != nil {
   127  			if _, ok := err.(distribution.ErrTagUnknown); ok {
   128  				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
   129  			} else {
   130  				imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   131  			}
   132  			return
   133  		}
   134  		imh.Digest = desc.Digest
   135  	}
   136  
   137  	if etagMatch(r, imh.Digest.String()) {
   138  		w.WriteHeader(http.StatusNotModified)
   139  		return
   140  	}
   141  
   142  	var options []distribution.ManifestServiceOption
   143  	if imh.Tag != "" {
   144  		options = append(options, distribution.WithTag(imh.Tag))
   145  	}
   146  	manifest, err := manifests.Get(imh, imh.Digest, options...)
   147  	if err != nil {
   148  		if _, ok := err.(distribution.ErrManifestUnknownRevision); ok {
   149  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
   150  		} else {
   151  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   152  		}
   153  		return
   154  	}
   155  	// determine the type of the returned manifest
   156  	manifestType := manifestSchema1
   157  	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
   158  	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
   159  	if isSchema2 {
   160  		manifestType = manifestSchema2
   161  	} else if _, isOCImanifest := manifest.(*ocischema.DeserializedManifest); isOCImanifest {
   162  		manifestType = ociSchema
   163  	} else if isManifestList {
   164  		if manifestList.MediaType == manifestlist.MediaTypeManifestList {
   165  			manifestType = manifestlistSchema
   166  		} else if manifestList.MediaType == v1.MediaTypeImageIndex {
   167  			manifestType = ociImageIndexSchema
   168  		}
   169  	}
   170  
   171  	if manifestType == ociSchema && !supports[ociSchema] {
   172  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithMessage("OCI manifest found, but accept header does not support OCI manifests"))
   173  		return
   174  	}
   175  	if manifestType == ociImageIndexSchema && !supports[ociImageIndexSchema] {
   176  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithMessage("OCI index found, but accept header does not support OCI indexes"))
   177  		return
   178  	}
   179  	// Only rewrite schema2 manifests when they are being fetched by tag.
   180  	// If they are being fetched by digest, we can't return something not
   181  	// matching the digest.
   182  	if imh.Tag != "" && manifestType == manifestSchema2 && !supports[manifestSchema2] {
   183  		// Rewrite manifest in schema1 format
   184  		dcontext.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
   185  
   186  		manifest, err = imh.convertSchema2Manifest(schema2Manifest)
   187  		if err != nil {
   188  			return
   189  		}
   190  	} else if imh.Tag != "" && manifestType == manifestlistSchema && !supports[manifestlistSchema] {
   191  		// Rewrite manifest in schema1 format
   192  		dcontext.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String())
   193  
   194  		// Find the image manifest corresponding to the default
   195  		// platform
   196  		var manifestDigest digest.Digest
   197  		for _, manifestDescriptor := range manifestList.Manifests {
   198  			if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS {
   199  				manifestDigest = manifestDescriptor.Digest
   200  				break
   201  			}
   202  		}
   203  
   204  		if manifestDigest == "" {
   205  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
   206  			return
   207  		}
   208  
   209  		manifest, err = manifests.Get(imh, manifestDigest)
   210  		if err != nil {
   211  			if _, ok := err.(distribution.ErrManifestUnknownRevision); ok {
   212  				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
   213  			} else {
   214  				imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   215  			}
   216  			return
   217  		}
   218  
   219  		// If necessary, convert the image manifest
   220  		if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supports[manifestSchema2] {
   221  			manifest, err = imh.convertSchema2Manifest(schema2Manifest)
   222  			if err != nil {
   223  				return
   224  			}
   225  		} else {
   226  			imh.Digest = manifestDigest
   227  		}
   228  	}
   229  
   230  	ct, p, err := manifest.Payload()
   231  	if err != nil {
   232  		return
   233  	}
   234  
   235  	w.Header().Set("Content-Type", ct)
   236  	w.Header().Set("Content-Length", fmt.Sprint(len(p)))
   237  	w.Header().Set("Docker-Content-Digest", imh.Digest.String())
   238  	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
   239  	w.Write(p)
   240  }
   241  
   242  func (imh *manifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) {
   243  	targetDescriptor := schema2Manifest.Target()
   244  	blobs := imh.Repository.Blobs(imh)
   245  	configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
   246  	if err != nil {
   247  		if err == distribution.ErrBlobUnknown {
   248  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   249  		} else {
   250  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   251  		}
   252  		return nil, err
   253  	}
   254  
   255  	ref := imh.Repository.Named()
   256  
   257  	if imh.Tag != "" {
   258  		ref, err = reference.WithTag(ref, imh.Tag)
   259  		if err != nil {
   260  			imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail(err))
   261  			return nil, err
   262  		}
   263  	}
   264  
   265  	builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, ref, configJSON)
   266  	for _, d := range schema2Manifest.Layers {
   267  		if err := builder.AppendReference(d); err != nil {
   268  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   269  			return nil, err
   270  		}
   271  	}
   272  	manifest, err := builder.Build(imh)
   273  	if err != nil {
   274  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   275  		return nil, err
   276  	}
   277  	imh.Digest = digest.FromBytes(manifest.(*schema1.SignedManifest).Canonical)
   278  
   279  	return manifest, nil
   280  }
   281  
   282  func etagMatch(r *http.Request, etag string) bool {
   283  	for _, headerVal := range r.Header["If-None-Match"] {
   284  		if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
   285  			return true
   286  		}
   287  	}
   288  	return false
   289  }
   290  
   291  // PutManifest validates and stores a manifest in the registry.
   292  func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) {
   293  	dcontext.GetLogger(imh).Debug("PutImageManifest")
   294  	manifests, err := imh.Repository.Manifests(imh)
   295  	if err != nil {
   296  		imh.Errors = append(imh.Errors, err)
   297  		return
   298  	}
   299  
   300  	var jsonBuf bytes.Buffer
   301  	if err := copyFullPayload(imh, w, r, &jsonBuf, maxManifestBodySize, "image manifest PUT"); err != nil {
   302  		// copyFullPayload reports the error if necessary
   303  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err.Error()))
   304  		return
   305  	}
   306  
   307  	mediaType := r.Header.Get("Content-Type")
   308  	manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes())
   309  	if err != nil {
   310  		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
   311  		return
   312  	}
   313  
   314  	if imh.Digest != "" {
   315  		if desc.Digest != imh.Digest {
   316  			dcontext.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest)
   317  			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
   318  			return
   319  		}
   320  	} else if imh.Tag != "" {
   321  		imh.Digest = desc.Digest
   322  	} else {
   323  		imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified"))
   324  		return
   325  	}
   326  
   327  	isAnOCIManifest := mediaType == v1.MediaTypeImageManifest || mediaType == v1.MediaTypeImageIndex
   328  
   329  	if isAnOCIManifest {
   330  		dcontext.GetLogger(imh).Debug("Putting an OCI Manifest!")
   331  	} else {
   332  		dcontext.GetLogger(imh).Debug("Putting a Docker Manifest!")
   333  	}
   334  
   335  	var options []distribution.ManifestServiceOption
   336  	if imh.Tag != "" {
   337  		options = append(options, distribution.WithTag(imh.Tag))
   338  	}
   339  
   340  	if err := imh.applyResourcePolicy(manifest); err != nil {
   341  		imh.Errors = append(imh.Errors, err)
   342  		return
   343  	}
   344  
   345  	_, err = manifests.Put(imh, manifest, options...)
   346  	if err != nil {
   347  		// TODO(stevvooe): These error handling switches really need to be
   348  		// handled by an app global mapper.
   349  		if err == distribution.ErrUnsupported {
   350  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
   351  			return
   352  		}
   353  		if err == distribution.ErrAccessDenied {
   354  			imh.Errors = append(imh.Errors, errcode.ErrorCodeDenied)
   355  			return
   356  		}
   357  		switch err := err.(type) {
   358  		case distribution.ErrManifestVerification:
   359  			for _, verificationError := range err {
   360  				switch verificationError := verificationError.(type) {
   361  				case distribution.ErrManifestBlobUnknown:
   362  					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest))
   363  				case distribution.ErrManifestNameInvalid:
   364  					imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err))
   365  				case distribution.ErrManifestUnverified:
   366  					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified)
   367  				default:
   368  					if verificationError == digest.ErrDigestInvalidFormat {
   369  						imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
   370  					} else {
   371  						imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError)
   372  					}
   373  				}
   374  			}
   375  		case errcode.Error:
   376  			imh.Errors = append(imh.Errors, err)
   377  		default:
   378  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   379  		}
   380  		return
   381  	}
   382  
   383  	// Tag this manifest
   384  	if imh.Tag != "" {
   385  		tags := imh.Repository.Tags(imh)
   386  		err = tags.Tag(imh, imh.Tag, desc)
   387  		if err != nil {
   388  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   389  			return
   390  		}
   391  
   392  	}
   393  
   394  	// Construct a canonical url for the uploaded manifest.
   395  	ref, err := reference.WithDigest(imh.Repository.Named(), imh.Digest)
   396  	if err != nil {
   397  		imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
   398  		return
   399  	}
   400  
   401  	location, err := imh.urlBuilder.BuildManifestURL(ref)
   402  	if err != nil {
   403  		// NOTE(stevvooe): Given the behavior above, this absurdly unlikely to
   404  		// happen. We'll log the error here but proceed as if it worked. Worst
   405  		// case, we set an empty location header.
   406  		dcontext.GetLogger(imh).Errorf("error building manifest url from digest: %v", err)
   407  	}
   408  
   409  	w.Header().Set("Location", location)
   410  	w.Header().Set("Docker-Content-Digest", imh.Digest.String())
   411  	w.WriteHeader(http.StatusCreated)
   412  
   413  	dcontext.GetLogger(imh).Debug("Succeeded in putting manifest!")
   414  }
   415  
   416  // applyResourcePolicy checks whether the resource class matches what has
   417  // been authorized and allowed by the policy configuration.
   418  func (imh *manifestHandler) applyResourcePolicy(manifest distribution.Manifest) error {
   419  	allowedClasses := imh.App.Config.Policy.Repository.Classes
   420  	if len(allowedClasses) == 0 {
   421  		return nil
   422  	}
   423  
   424  	var class string
   425  	switch m := manifest.(type) {
   426  	case *schema1.SignedManifest:
   427  		class = imageClass
   428  	case *schema2.DeserializedManifest:
   429  		switch m.Config.MediaType {
   430  		case schema2.MediaTypeImageConfig:
   431  			class = imageClass
   432  		case schema2.MediaTypePluginConfig:
   433  			class = "plugin"
   434  		default:
   435  			return errcode.ErrorCodeDenied.WithMessage("unknown manifest class for " + m.Config.MediaType)
   436  		}
   437  	case *ocischema.DeserializedManifest:
   438  		switch m.Config.MediaType {
   439  		case v1.MediaTypeImageConfig:
   440  			class = imageClass
   441  		default:
   442  			return errcode.ErrorCodeDenied.WithMessage("unknown manifest class for " + m.Config.MediaType)
   443  		}
   444  	}
   445  
   446  	if class == "" {
   447  		return nil
   448  	}
   449  
   450  	// Check to see if class is allowed in registry
   451  	var allowedClass bool
   452  	for _, c := range allowedClasses {
   453  		if class == c {
   454  			allowedClass = true
   455  			break
   456  		}
   457  	}
   458  	if !allowedClass {
   459  		return errcode.ErrorCodeDenied.WithMessage(fmt.Sprintf("registry does not allow %s manifest", class))
   460  	}
   461  
   462  	resources := auth.AuthorizedResources(imh)
   463  	n := imh.Repository.Named().Name()
   464  
   465  	var foundResource bool
   466  	for _, r := range resources {
   467  		if r.Name == n {
   468  			if r.Class == "" {
   469  				r.Class = imageClass
   470  			}
   471  			if r.Class == class {
   472  				return nil
   473  			}
   474  			foundResource = true
   475  		}
   476  	}
   477  
   478  	// resource was found but no matching class was found
   479  	if foundResource {
   480  		return errcode.ErrorCodeDenied.WithMessage(fmt.Sprintf("repository not authorized for %s manifest", class))
   481  	}
   482  
   483  	return nil
   484  
   485  }
   486  
   487  // DeleteManifest removes the manifest with the given digest from the registry.
   488  func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
   489  	dcontext.GetLogger(imh).Debug("DeleteImageManifest")
   490  
   491  	manifests, err := imh.Repository.Manifests(imh)
   492  	if err != nil {
   493  		imh.Errors = append(imh.Errors, err)
   494  		return
   495  	}
   496  
   497  	err = manifests.Delete(imh, imh.Digest)
   498  	if err != nil {
   499  		switch err {
   500  		case digest.ErrDigestUnsupported:
   501  		case digest.ErrDigestInvalidFormat:
   502  			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
   503  			return
   504  		case distribution.ErrBlobUnknown:
   505  			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
   506  			return
   507  		case distribution.ErrUnsupported:
   508  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
   509  			return
   510  		default:
   511  			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown)
   512  			return
   513  		}
   514  	}
   515  
   516  	tagService := imh.Repository.Tags(imh)
   517  	referencedTags, err := tagService.Lookup(imh, distribution.Descriptor{Digest: imh.Digest})
   518  	if err != nil {
   519  		imh.Errors = append(imh.Errors, err)
   520  		return
   521  	}
   522  
   523  	for _, tag := range referencedTags {
   524  		if err := tagService.Untag(imh, tag); err != nil {
   525  			imh.Errors = append(imh.Errors, err)
   526  			return
   527  		}
   528  	}
   529  
   530  	w.WriteHeader(http.StatusAccepted)
   531  }
   532  

View as plain text