Browse code

LCOW: Auto-select OS

Signed-off-by: John Howard <jhoward@microsoft.com>

Addresses https://github.com/moby/moby/pull/35089#issuecomment-367802698.
This change enables the daemon to automatically select an image under LCOW
that can be used if the API doesn't specify an explicit platform.

For example:

FROM supertest2014/nyan
ADD Dockerfile /

And docker build . will download the linux image (not a multi-manifest image)

And similarly docker pull ubuntu will match linux/amd64

John Howard authored on 2018/02/24 08:29:26
Showing 9 changed files
... ...
@@ -228,7 +228,7 @@ func (d *dispatchRequest) getOsFromFlagsAndStage(stageOS string) string {
228 228
 		// multi-arch aware yet, it is guaranteed to only hold the OS part here.
229 229
 		return d.builder.options.Platform
230 230
 	default:
231
-		return runtime.GOOS
231
+		return "" // Auto-select
232 232
 	}
233 233
 }
234 234
 
... ...
@@ -247,9 +247,9 @@ func (d *dispatchRequest) getImageOrStage(name string, stageOS string) (builder.
247 247
 		imageImage.OS = runtime.GOOS
248 248
 		if runtime.GOOS == "windows" {
249 249
 			switch os {
250
-			case "windows", "":
250
+			case "windows":
251 251
 				return nil, errors.New("Windows does not support FROM scratch")
252
-			case "linux":
252
+			case "linux", "":
253 253
 				if !system.LCOWSupported() {
254 254
 					return nil, errors.New("Linux containers are not supported on this system")
255 255
 				}
... ...
@@ -166,7 +166,7 @@ func (i *ImageService) pullForBuilder(ctx context.Context, name string, authConf
166 166
 // Every call to GetImageAndReleasableLayer MUST call releasableLayer.Release() to prevent
167 167
 // leaking of layers.
168 168
 func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error) {
169
-	if refOrID == "" {
169
+	if refOrID == "" { // ie FROM scratch
170 170
 		if !system.IsOSSupported(opts.OS) {
171 171
 			return nil, nil, system.ErrNotSupportedOperatingSystem
172 172
 		}
... ...
@@ -3,7 +3,6 @@ package images // import "github.com/docker/docker/daemon/images"
3 3
 import (
4 4
 	"context"
5 5
 	"io"
6
-	"runtime"
7 6
 	"strings"
8 7
 	"time"
9 8
 
... ...
@@ -65,11 +64,6 @@ func (i *ImageService) pullImageWithReference(ctx context.Context, ref reference
65 65
 		close(writesDone)
66 66
 	}()
67 67
 
68
-	// Default to the host OS platform in case it hasn't been populated with an explicit value.
69
-	if os == "" {
70
-		os = runtime.GOOS
71
-	}
72
-
73 68
 	imagePullConfig := &distribution.ImagePullConfig{
74 69
 		Config: distribution.Config{
75 70
 			MetaHeaders:      metaHeaders,
... ...
@@ -3,7 +3,6 @@ package distribution // import "github.com/docker/docker/distribution"
3 3
 import (
4 4
 	"context"
5 5
 	"fmt"
6
-	"runtime"
7 6
 
8 7
 	"github.com/docker/distribution/reference"
9 8
 	"github.com/docker/docker/api"
... ...
@@ -115,11 +114,6 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
115 115
 			continue
116 116
 		}
117 117
 
118
-		// Make sure we default the OS if it hasn't been supplied
119
-		if imagePullConfig.OS == "" {
120
-			imagePullConfig.OS = runtime.GOOS
121
-		}
122
-
123 118
 		if err := puller.Pull(ctx, ref, imagePullConfig.OS); err != nil {
124 119
 			// Was this pull cancelled? If so, don't try to fall
125 120
 			// back.
... ...
@@ -509,6 +509,14 @@ func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Reference, unv
509 509
 		}
510 510
 	}
511 511
 
512
+	// In the situation that the API call didn't specify an OS explicitly, but
513
+	// we support the operating system, switch to that operating system.
514
+	// eg FROM supertest2014/nyan with no platform specifier, and docker build
515
+	// with no --platform= flag under LCOW.
516
+	if requestedOS == "" && system.IsOSSupported(configOS) {
517
+		requestedOS = configOS
518
+	}
519
+
512 520
 	// Early bath if the requested OS doesn't match that of the configuration.
513 521
 	// This avoids doing the download, only to potentially fail later.
514 522
 	if !strings.EqualFold(configOS, requestedOS) {
... ...
@@ -618,9 +626,10 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
618 618
 
619 619
 		// Early bath if the requested OS doesn't match that of the configuration.
620 620
 		// This avoids doing the download, only to potentially fail later.
621
-		if !strings.EqualFold(configPlatform.OS, requestedOS) {
621
+		if !system.IsOSSupported(configPlatform.OS) {
622 622
 			return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configPlatform.OS, requestedOS)
623 623
 		}
624
+		requestedOS = configPlatform.OS
624 625
 
625 626
 		// Populate diff ids in descriptors to avoid downloading foreign layers
626 627
 		// which have been side loaded
... ...
@@ -629,6 +638,10 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
629 629
 		}
630 630
 	}
631 631
 
632
+	if requestedOS == "" {
633
+		requestedOS = runtime.GOOS
634
+	}
635
+
632 636
 	if p.config.DownloadManager != nil {
633 637
 		go func() {
634 638
 			var (
... ...
@@ -722,18 +735,22 @@ func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan
722 722
 
723 723
 // pullManifestList handles "manifest lists" which point to various
724 724
 // platform-specific manifests.
725
-func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList, os string) (id digest.Digest, manifestListDigest digest.Digest, err error) {
725
+func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList, requestedOS string) (id digest.Digest, manifestListDigest digest.Digest, err error) {
726 726
 	manifestListDigest, err = schema2ManifestDigest(ref, mfstList)
727 727
 	if err != nil {
728 728
 		return "", "", err
729 729
 	}
730 730
 
731
-	logrus.Debugf("%s resolved to a manifestList object with %d entries; looking for a %s/%s match", ref, len(mfstList.Manifests), os, runtime.GOARCH)
731
+	logOS := requestedOS // May be "" indicating any OS
732
+	if logOS == "" {
733
+		logOS = "*"
734
+	}
735
+	logrus.Debugf("%s resolved to a manifestList object with %d entries; looking for a %s/%s match", ref, len(mfstList.Manifests), logOS, runtime.GOARCH)
732 736
 
733
-	manifestMatches := filterManifests(mfstList.Manifests, os)
737
+	manifestMatches := filterManifests(mfstList.Manifests, requestedOS)
734 738
 
735 739
 	if len(manifestMatches) == 0 {
736
-		errMsg := fmt.Sprintf("no matching manifest for %s/%s in the manifest list entries", os, runtime.GOARCH)
740
+		errMsg := fmt.Sprintf("no matching manifest for %s/%s in the manifest list entries", logOS, runtime.GOARCH)
737 741
 		logrus.Debugf(errMsg)
738 742
 		return "", "", errors.New(errMsg)
739 743
 	}
... ...
@@ -764,12 +781,12 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
764 764
 
765 765
 	switch v := manifest.(type) {
766 766
 	case *schema1.SignedManifest:
767
-		id, _, err = p.pullSchema1(ctx, manifestRef, v, os)
767
+		id, _, err = p.pullSchema1(ctx, manifestRef, v, manifestMatches[0].Platform.OS)
768 768
 		if err != nil {
769 769
 			return "", "", err
770 770
 		}
771 771
 	case *schema2.DeserializedManifest:
772
-		id, _, err = p.pullSchema2(ctx, manifestRef, v, os)
772
+		id, _, err = p.pullSchema2(ctx, manifestRef, v, manifestMatches[0].Platform.OS)
773 773
 		if err != nil {
774 774
 			return "", "", err
775 775
 		}
... ...
@@ -16,13 +16,13 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo
16 16
 	return blobs.Open(ctx, ld.digest)
17 17
 }
18 18
 
19
-func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor {
19
+func filterManifests(manifests []manifestlist.ManifestDescriptor, _ string) []manifestlist.ManifestDescriptor {
20 20
 	var matches []manifestlist.ManifestDescriptor
21 21
 	for _, manifestDescriptor := range manifests {
22
-		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os {
22
+		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS {
23 23
 			matches = append(matches, manifestDescriptor)
24 24
 
25
-			logrus.Debugf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
25
+			logrus.Debugf("found match for %s/%s with media type %s, digest %s", runtime.GOOS, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
26 26
 		}
27 27
 	}
28 28
 	return matches
... ...
@@ -62,24 +62,27 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo
62 62
 	return rsc, err
63 63
 }
64 64
 
65
-func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor {
66
-	osVersion := ""
67
-	if os == "windows" {
68
-		version := system.GetOSVersion()
69
-		osVersion = fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build)
70
-		logrus.Debugf("will prefer entries with version %s", osVersion)
71
-	}
65
+func filterManifests(manifests []manifestlist.ManifestDescriptor, requestedOS string) []manifestlist.ManifestDescriptor {
66
+	version := system.GetOSVersion()
67
+	osVersion := fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build)
68
+	logrus.Debugf("will prefer Windows entries with version %s", osVersion)
72 69
 
73 70
 	var matches []manifestlist.ManifestDescriptor
71
+	foundWindowsMatch := false
74 72
 	for _, manifestDescriptor := range manifests {
75
-		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os {
73
+		if (manifestDescriptor.Platform.Architecture == runtime.GOARCH) &&
74
+			((requestedOS != "" && manifestDescriptor.Platform.OS == requestedOS) || // Explicit user request for an OS we know we support
75
+				(requestedOS == "" && system.IsOSSupported(manifestDescriptor.Platform.OS))) { // No user requested OS, but one we can support
76 76
 			matches = append(matches, manifestDescriptor)
77
-			logrus.Debugf("found match for %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
77
+			logrus.Debugf("found match %s/%s %s with media type %s, digest %s", manifestDescriptor.Platform.OS, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
78
+			if strings.EqualFold("windows", manifestDescriptor.Platform.OS) {
79
+				foundWindowsMatch = true
80
+			}
78 81
 		} else {
79
-			logrus.Debugf("ignoring %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
82
+			logrus.Debugf("ignoring %s/%s %s with media type %s, digest %s", manifestDescriptor.Platform.OS, manifestDescriptor.Platform.Architecture, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
80 83
 		}
81 84
 	}
82
-	if os == "windows" {
85
+	if foundWindowsMatch {
83 86
 		sort.Stable(manifestsByVersion{osVersion, matches})
84 87
 	}
85 88
 	return matches
... ...
@@ -76,7 +76,8 @@ func (is *store) restore() error {
76 76
 		var l layer.Layer
77 77
 		if chainID := img.RootFS.ChainID(); chainID != "" {
78 78
 			if !system.IsOSSupported(img.OperatingSystem()) {
79
-				return system.ErrNotSupportedOperatingSystem
79
+				logrus.Errorf("not restoring image with unsupported operating system %v, %v, %s", dgst, chainID, img.OperatingSystem())
80
+				return nil
80 81
 			}
81 82
 			l, err = is.lss[img.OperatingSystem()].Get(chainID)
82 83
 			if err != nil {
... ...
@@ -59,10 +59,10 @@ func ParsePlatform(in string) *specs.Platform {
59 59
 
60 60
 // IsOSSupported determines if an operating system is supported by the host
61 61
 func IsOSSupported(os string) bool {
62
-	if runtime.GOOS == os {
62
+	if strings.EqualFold(runtime.GOOS, os) {
63 63
 		return true
64 64
 	}
65
-	if LCOWSupported() && os == "linux" {
65
+	if LCOWSupported() && strings.EqualFold(os, "linux") {
66 66
 		return true
67 67
 	}
68 68
 	return false