Recognize clients that don't support manifest lists
Convert a default platform's manifest to schema1 on the fly. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>master
							parent
							
								
									7ef71988a8
								
							
						
					
					
						commit
						fce65b72b3
					
				| 
						 | 
				
			
			@ -1567,6 +1567,55 @@ func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs)
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified)
 | 
			
		||||
 | 
			
		||||
	// ------------------
 | 
			
		||||
	// Fetch as a schema1 manifest
 | 
			
		||||
	resp, err = http.Get(manifestURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error fetching manifest list as schema1: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	checkResponse(t, "fetching uploaded manifest list as schema1", resp, http.StatusOK)
 | 
			
		||||
	checkHeaders(t, resp, http.Header{
 | 
			
		||||
		"Docker-Content-Digest": []string{dgst.String()},
 | 
			
		||||
		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	var fetchedSchema1Manifest schema1.SignedManifest
 | 
			
		||||
	dec = json.NewDecoder(resp.Body)
 | 
			
		||||
 | 
			
		||||
	if err := dec.Decode(&fetchedSchema1Manifest); err != nil {
 | 
			
		||||
		t.Fatalf("error decoding fetched schema1 manifest: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 {
 | 
			
		||||
		t.Fatal("wrong schema version")
 | 
			
		||||
	}
 | 
			
		||||
	if fetchedSchema1Manifest.Architecture != "amd64" {
 | 
			
		||||
		t.Fatal("wrong architecture")
 | 
			
		||||
	}
 | 
			
		||||
	if fetchedSchema1Manifest.Name != imageName {
 | 
			
		||||
		t.Fatal("wrong image name")
 | 
			
		||||
	}
 | 
			
		||||
	if fetchedSchema1Manifest.Tag != tag {
 | 
			
		||||
		t.Fatal("wrong tag")
 | 
			
		||||
	}
 | 
			
		||||
	if len(fetchedSchema1Manifest.FSLayers) != 2 {
 | 
			
		||||
		t.Fatal("wrong number of FSLayers")
 | 
			
		||||
	}
 | 
			
		||||
	layers := args.manifest.(*schema2.DeserializedManifest).Layers
 | 
			
		||||
	for i := range layers {
 | 
			
		||||
		if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(layers)-i-1].Digest {
 | 
			
		||||
			t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(fetchedSchema1Manifest.History) != 2 {
 | 
			
		||||
		t.Fatal("wrong number of History entries")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Don't check V1Compatibility fields becuase we're using randomly-generated
 | 
			
		||||
	// layers.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import (
 | 
			
		|||
	"github.com/docker/distribution"
 | 
			
		||||
	ctxu "github.com/docker/distribution/context"
 | 
			
		||||
	"github.com/docker/distribution/digest"
 | 
			
		||||
	"github.com/docker/distribution/manifest/manifestlist"
 | 
			
		||||
	"github.com/docker/distribution/manifest/schema1"
 | 
			
		||||
	"github.com/docker/distribution/manifest/schema2"
 | 
			
		||||
	"github.com/docker/distribution/registry/api/errcode"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,13 @@ import (
 | 
			
		|||
	"github.com/gorilla/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// These constants determine which architecture and OS to choose from a
 | 
			
		||||
// manifest list when downconverting it to a schema1 manifest.
 | 
			
		||||
const (
 | 
			
		||||
	defaultArch = "amd64"
 | 
			
		||||
	defaultOS   = "linux"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// imageManifestDispatcher takes the request context and builds the
 | 
			
		||||
// appropriate handler for handling image manifest requests.
 | 
			
		||||
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
 | 
			
		||||
| 
						 | 
				
			
			@ -83,42 +91,62 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
 | 
			
		|||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	supportsSchema2 := false
 | 
			
		||||
	supportsManifestList := false
 | 
			
		||||
	if acceptHeaders, ok := r.Header["Accept"]; ok {
 | 
			
		||||
		for _, mediaType := range acceptHeaders {
 | 
			
		||||
			if mediaType == schema2.MediaTypeManifest {
 | 
			
		||||
				supportsSchema2 = true
 | 
			
		||||
			}
 | 
			
		||||
			if mediaType == manifestlist.MediaTypeManifestList {
 | 
			
		||||
				supportsManifestList = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
 | 
			
		||||
	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
 | 
			
		||||
 | 
			
		||||
	// Only rewrite schema2 manifests when they are being fetched by tag.
 | 
			
		||||
	// If they are being fetched by digest, we can't return something not
 | 
			
		||||
	// matching the digest.
 | 
			
		||||
	if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); imh.Tag != "" && isSchema2 {
 | 
			
		||||
		supportsSchema2 := false
 | 
			
		||||
		if acceptHeaders, ok := r.Header["Accept"]; ok {
 | 
			
		||||
			for _, mediaType := range acceptHeaders {
 | 
			
		||||
				if mediaType == schema2.MediaTypeManifest {
 | 
			
		||||
					supportsSchema2 = true
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
	if imh.Tag != "" && isSchema2 && !supportsSchema2 {
 | 
			
		||||
		// Rewrite manifest in schema1 format
 | 
			
		||||
		ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
 | 
			
		||||
 | 
			
		||||
		manifest, err = imh.convertSchema2Manifest(schema2Manifest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	} else if imh.Tag != "" && isManifestList && !supportsManifestList {
 | 
			
		||||
		// Rewrite manifest in schema1 format
 | 
			
		||||
		ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String())
 | 
			
		||||
 | 
			
		||||
		// Find the image manifest corresponding to the default
 | 
			
		||||
		// platform
 | 
			
		||||
		var manifestDigest digest.Digest
 | 
			
		||||
		for _, manifestDescriptor := range manifestList.Manifests {
 | 
			
		||||
			if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS {
 | 
			
		||||
				manifestDigest = manifestDescriptor.Digest
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !supportsSchema2 {
 | 
			
		||||
			// Rewrite manifest in schema1 format
 | 
			
		||||
			ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
 | 
			
		||||
		if manifestDigest == "" {
 | 
			
		||||
			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			targetDescriptor := schema2Manifest.Target()
 | 
			
		||||
			blobs := imh.Repository.Blobs(imh)
 | 
			
		||||
			configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		manifest, err = manifests.Get(imh, manifestDigest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, imh.Repository.Name(), imh.Tag, configJSON)
 | 
			
		||||
			for _, d := range manifest.References() {
 | 
			
		||||
				if err := builder.AppendReference(d); err != nil {
 | 
			
		||||
					imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			manifest, err = builder.Build(imh)
 | 
			
		||||
		// If necessary, convert the image manifest
 | 
			
		||||
		if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
 | 
			
		||||
			manifest, err = imh.convertSchema2Manifest(schema2Manifest)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +164,31 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
 | 
			
		|||
	w.Write(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) {
 | 
			
		||||
	targetDescriptor := schema2Manifest.Target()
 | 
			
		||||
	blobs := imh.Repository.Blobs(imh)
 | 
			
		||||
	configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, imh.Repository.Name(), imh.Tag, configJSON)
 | 
			
		||||
	for _, d := range schema2Manifest.References() {
 | 
			
		||||
		if err := builder.AppendReference(d); err != nil {
 | 
			
		||||
			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	manifest, err := builder.Build(imh)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return manifest, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue