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>
This commit is contained in:
parent
7ef71988a8
commit
fce65b72b3
2 changed files with 129 additions and 27 deletions
|
@ -1567,6 +1567,55 @@ func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified)
|
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) {
|
func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
ctxu "github.com/docker/distribution/context"
|
ctxu "github.com/docker/distribution/context"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/distribution/manifest/manifestlist"
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
"github.com/docker/distribution/registry/api/errcode"
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
|
@ -15,6 +16,13 @@ import (
|
||||||
"github.com/gorilla/handlers"
|
"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
|
// imageManifestDispatcher takes the request context and builds the
|
||||||
// appropriate handler for handling image manifest requests.
|
// appropriate handler for handling image manifest requests.
|
||||||
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
|
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||||
|
@ -83,42 +91,62 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
|
||||||
return
|
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.
|
// Only rewrite schema2 manifests when they are being fetched by tag.
|
||||||
// If they are being fetched by digest, we can't return something not
|
// If they are being fetched by digest, we can't return something not
|
||||||
// matching the digest.
|
// matching the digest.
|
||||||
if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); imh.Tag != "" && isSchema2 {
|
if imh.Tag != "" && isSchema2 && !supportsSchema2 {
|
||||||
supportsSchema2 := false
|
// Rewrite manifest in schema1 format
|
||||||
if acceptHeaders, ok := r.Header["Accept"]; ok {
|
ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
|
||||||
for _, mediaType := range acceptHeaders {
|
|
||||||
if mediaType == schema2.MediaTypeManifest {
|
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
|
||||||
supportsSchema2 = true
|
if err != nil {
|
||||||
break
|
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 {
|
if manifestDigest == "" {
|
||||||
// Rewrite manifest in schema1 format
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
|
||||||
ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
|
return
|
||||||
|
}
|
||||||
|
|
||||||
targetDescriptor := schema2Manifest.Target()
|
manifest, err = manifests.Get(imh, manifestDigest)
|
||||||
blobs := imh.Repository.Blobs(imh)
|
if err != nil {
|
||||||
configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
|
||||||
if err != nil {
|
return
|
||||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, imh.Repository.Name(), imh.Tag, configJSON)
|
// If necessary, convert the image manifest
|
||||||
for _, d := range manifest.References() {
|
if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
|
||||||
if err := builder.AppendReference(d); err != nil {
|
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
|
||||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manifest, err = builder.Build(imh)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,6 +164,31 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
|
||||||
w.Write(p)
|
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 {
|
func etagMatch(r *http.Request, etag string) bool {
|
||||||
for _, headerVal := range r.Header["If-None-Match"] {
|
for _, headerVal := range r.Header["If-None-Match"] {
|
||||||
if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
|
if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
|
||||||
|
|
Loading…
Reference in a new issue