Browse code

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>

Aaron Lehmann authored on 2015/12/17 12:19:22
Showing 1 changed files
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"github.com/Sirupsen/logrus"
13 13
 	"github.com/docker/distribution"
14 14
 	"github.com/docker/distribution/digest"
15
+	"github.com/docker/distribution/manifest/manifestlist"
15 16
 	"github.com/docker/distribution/manifest/schema1"
16 17
 	"github.com/docker/distribution/manifest/schema2"
17 18
 	"github.com/docker/distribution/registry/api/errcode"
... ...
@@ -254,6 +255,11 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat
254 254
 		if err != nil {
255 255
 			return false, err
256 256
 		}
257
+	case *manifestlist.DeserializedManifestList:
258
+		imageID, manifestDigest, err = p.pullManifestList(ctx, ref, v)
259
+		if err != nil {
260
+			return false, err
261
+		}
257 262
 	default:
258 263
 		return false, errors.New("unsupported manifest format")
259 264
 	}
... ...
@@ -357,30 +363,11 @@ func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Named, unverif
357 357
 }
358 358
 
359 359
 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()
360
+	manifestDigest, err = schema2ManifestDigest(ref, mfst)
361 361
 	if err != nil {
362 362
 		return "", "", err
363 363
 	}
364 364
 
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
-
384 365
 	target := mfst.Target()
385 366
 	imageID = image.ID(target.Digest)
386 367
 	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
470 470
 	return imageID, manifestDigest, nil
471 471
 }
472 472
 
473
+// pullManifestList handles "manifest lists" which point to various
474
+// platform-specifc manifests.
475
+func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList) (imageID image.ID, manifestListDigest digest.Digest, err error) {
476
+	manifestListDigest, err = schema2ManifestDigest(ref, mfstList)
477
+	if err != nil {
478
+		return "", "", err
479
+	}
480
+
481
+	var manifestDigest digest.Digest
482
+	for _, manifestDescriptor := range mfstList.Manifests {
483
+		// TODO(aaronl): The manifest list spec supports optional
484
+		// "features" and "variant" fields. These are not yet used.
485
+		// Once they are, their values should be interpreted here.
486
+		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS {
487
+			manifestDigest = manifestDescriptor.Digest
488
+			break
489
+		}
490
+	}
491
+
492
+	if manifestDigest == "" {
493
+		return "", "", errors.New("no supported platform found in manifest list")
494
+	}
495
+
496
+	manSvc, err := p.repo.Manifests(ctx)
497
+	if err != nil {
498
+		return "", "", err
499
+	}
500
+
501
+	manifest, err := manSvc.Get(ctx, manifestDigest)
502
+	if err != nil {
503
+		return "", "", err
504
+	}
505
+
506
+	manifestRef, err := reference.WithDigest(ref, manifestDigest)
507
+	if err != nil {
508
+		return "", "", err
509
+	}
510
+
511
+	switch v := manifest.(type) {
512
+	case *schema1.SignedManifest:
513
+		imageID, _, err = p.pullSchema1(ctx, manifestRef, v)
514
+		if err != nil {
515
+			return "", "", err
516
+		}
517
+	case *schema2.DeserializedManifest:
518
+		imageID, _, err = p.pullSchema2(ctx, manifestRef, v)
519
+		if err != nil {
520
+			return "", "", err
521
+		}
522
+	default:
523
+		return "", "", errors.New("unsupported manifest format")
524
+	}
525
+
526
+	return imageID, manifestListDigest, err
527
+}
528
+
473 529
 func (p *v2Puller) pullSchema2ImageConfig(ctx context.Context, dgst digest.Digest) (configJSON []byte, err error) {
474 530
 	blobs := p.repo.Blobs(ctx)
475 531
 	configJSON, err = blobs.Get(ctx, dgst)
... ...
@@ -494,6 +537,34 @@ func (p *v2Puller) pullSchema2ImageConfig(ctx context.Context, dgst digest.Diges
494 494
 	return configJSON, nil
495 495
 }
496 496
 
497
+// schema2ManifestDigest computes the manifest digest, and, if pulling by
498
+// digest, ensures that it matches the requested digest.
499
+func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) {
500
+	_, canonical, err := mfst.Payload()
501
+	if err != nil {
502
+		return "", err
503
+	}
504
+
505
+	// If pull by digest, then verify the manifest digest.
506
+	if digested, isDigested := ref.(reference.Canonical); isDigested {
507
+		verifier, err := digest.NewDigestVerifier(digested.Digest())
508
+		if err != nil {
509
+			return "", err
510
+		}
511
+		if _, err := verifier.Write(canonical); err != nil {
512
+			return "", err
513
+		}
514
+		if !verifier.Verified() {
515
+			err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
516
+			logrus.Error(err)
517
+			return "", err
518
+		}
519
+		return digested.Digest(), nil
520
+	}
521
+
522
+	return digest.FromBytes(canonical), nil
523
+}
524
+
497 525
 // allowV1Fallback checks if the error is a possible reason to fallback to v1
498 526
 // (even if confirmedV2 has been set already), and if so, wraps the error in
499 527
 // a fallbackError with confirmedV2 set to false. Otherwise, it returns the