Skip to content
This repository was archived by the owner on Feb 8, 2021. It is now read-only.

Commit 2bb8c85

Browse files
committed
Add support for manifest lists ("fat manifests")
A manifest list refers to platform-specific manifests. This allows for images that target more than one architecture to share the same tag. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
1 parent c8d277d commit 2bb8c85

File tree

1 file changed

+91
-20
lines changed

1 file changed

+91
-20
lines changed

distribution/pull_v2.go

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/Sirupsen/logrus"
1313
"github.com/docker/distribution"
1414
"github.com/docker/distribution/digest"
15+
"github.com/docker/distribution/manifest/manifestlist"
1516
"github.com/docker/distribution/manifest/schema1"
1617
"github.com/docker/distribution/manifest/schema2"
1718
"github.com/docker/distribution/registry/api/errcode"
@@ -254,6 +255,11 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat
254255
if err != nil {
255256
return false, err
256257
}
258+
case *manifestlist.DeserializedManifestList:
259+
imageID, manifestDigest, err = p.pullManifestList(ctx, ref, v)
260+
if err != nil {
261+
return false, err
262+
}
257263
default:
258264
return false, errors.New("unsupported manifest format")
259265
}
@@ -357,30 +363,11 @@ func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Named, unverif
357363
}
358364

359365
func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest) (imageID image.ID, manifestDigest digest.Digest, err error) {
360-
_, canonical, err := mfst.Payload()
366+
manifestDigest, err = schema2ManifestDigest(ref, mfst)
361367
if err != nil {
362368
return "", "", err
363369
}
364370

365-
// If pull by digest, then verify the manifest digest.
366-
if digested, isDigested := ref.(reference.Canonical); isDigested {
367-
verifier, err := digest.NewDigestVerifier(digested.Digest())
368-
if err != nil {
369-
return "", "", err
370-
}
371-
if _, err := verifier.Write(canonical); err != nil {
372-
return "", "", err
373-
}
374-
if !verifier.Verified() {
375-
err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
376-
logrus.Error(err)
377-
return "", "", err
378-
}
379-
manifestDigest = digested.Digest()
380-
} else {
381-
manifestDigest = digest.FromBytes(canonical)
382-
}
383-
384371
target := mfst.Target()
385372
imageID = image.ID(target.Digest)
386373
if _, err := p.config.ImageStore.Get(imageID); err == nil {
@@ -470,6 +457,62 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
470457
return imageID, manifestDigest, nil
471458
}
472459

460+
// pullManifestList handles "manifest lists" which point to various
461+
// platform-specifc manifests.
462+
func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList) (imageID image.ID, manifestListDigest digest.Digest, err error) {
463+
manifestListDigest, err = schema2ManifestDigest(ref, mfstList)
464+
if err != nil {
465+
return "", "", err
466+
}
467+
468+
var manifestDigest digest.Digest
469+
for _, manifestDescriptor := range mfstList.Manifests {
470+
// TODO(aaronl): The manifest list spec supports optional
471+
// "features" and "variant" fields. These are not yet used.
472+
// Once they are, their values should be interpreted here.
473+
if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS {
474+
manifestDigest = manifestDescriptor.Digest
475+
break
476+
}
477+
}
478+
479+
if manifestDigest == "" {
480+
return "", "", errors.New("no supported platform found in manifest list")
481+
}
482+
483+
manSvc, err := p.repo.Manifests(ctx)
484+
if err != nil {
485+
return "", "", err
486+
}
487+
488+
manifest, err := manSvc.Get(ctx, manifestDigest)
489+
if err != nil {
490+
return "", "", err
491+
}
492+
493+
manifestRef, err := reference.WithDigest(ref, manifestDigest)
494+
if err != nil {
495+
return "", "", err
496+
}
497+
498+
switch v := manifest.(type) {
499+
case *schema1.SignedManifest:
500+
imageID, _, err = p.pullSchema1(ctx, manifestRef, v)
501+
if err != nil {
502+
return "", "", err
503+
}
504+
case *schema2.DeserializedManifest:
505+
imageID, _, err = p.pullSchema2(ctx, manifestRef, v)
506+
if err != nil {
507+
return "", "", err
508+
}
509+
default:
510+
return "", "", errors.New("unsupported manifest format")
511+
}
512+
513+
return imageID, manifestListDigest, err
514+
}
515+
473516
func (p *v2Puller) pullSchema2ImageConfig(ctx context.Context, dgst digest.Digest) (configJSON []byte, err error) {
474517
blobs := p.repo.Blobs(ctx)
475518
configJSON, err = blobs.Get(ctx, dgst)
@@ -494,6 +537,34 @@ func (p *v2Puller) pullSchema2ImageConfig(ctx context.Context, dgst digest.Diges
494537
return configJSON, nil
495538
}
496539

540+
// schema2ManifestDigest computes the manifest digest, and, if pulling by
541+
// digest, ensures that it matches the requested digest.
542+
func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) {
543+
_, canonical, err := mfst.Payload()
544+
if err != nil {
545+
return "", err
546+
}
547+
548+
// If pull by digest, then verify the manifest digest.
549+
if digested, isDigested := ref.(reference.Canonical); isDigested {
550+
verifier, err := digest.NewDigestVerifier(digested.Digest())
551+
if err != nil {
552+
return "", err
553+
}
554+
if _, err := verifier.Write(canonical); err != nil {
555+
return "", err
556+
}
557+
if !verifier.Verified() {
558+
err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
559+
logrus.Error(err)
560+
return "", err
561+
}
562+
return digested.Digest(), nil
563+
}
564+
565+
return digest.FromBytes(canonical), nil
566+
}
567+
497568
// allowV1Fallback checks if the error is a possible reason to fallback to v1
498569
// (even if confirmedV2 has been set already), and if so, wraps the error in
499570
// a fallbackError with confirmedV2 set to false. Otherwise, it returns the

0 commit comments

Comments
 (0)