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
							
								
									9c416f0e94
								
							
						
					
					
						commit
						697af09566
					
				|  | @ -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