Browse code

Add os_version and os_features to Image

These fields are needed to specify the exact version of Windows that an
image can run on. They may be useful for other platforms in the future.

This also changes image.store.Create to validate that the loaded image is
supported on the current machine. This change affects Linux as well, since
it now validates the architecture and OS fields.

Signed-off-by: John Starks <jostarks@microsoft.com>

John Starks authored on 2016/03/17 10:45:40
Showing 15 changed files
... ...
@@ -138,6 +138,8 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str
138 138
 
139 139
 	var history []image.History
140 140
 	rootFS := image.NewRootFS()
141
+	osVersion := ""
142
+	var osFeatures []string
141 143
 
142 144
 	if container.ImageID != "" {
143 145
 		img, err := daemon.imageStore.Get(container.ImageID)
... ...
@@ -146,6 +148,8 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str
146 146
 		}
147 147
 		history = img.History
148 148
 		rootFS = img.RootFS
149
+		osVersion = img.OSVersion
150
+		osFeatures = img.OSFeatures
149 151
 	}
150 152
 
151 153
 	l, err := daemon.layerStore.Register(rwTar, rootFS.ChainID())
... ...
@@ -180,8 +184,10 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str
180 180
 			Author:          c.Author,
181 181
 			Created:         h.Created,
182 182
 		},
183
-		RootFS:  rootFS,
184
-		History: history,
183
+		RootFS:     rootFS,
184
+		History:    history,
185
+		OSFeatures: osFeatures,
186
+		OSVersion:  osVersion,
185 187
 	})
186 188
 
187 189
 	if err != nil {
... ...
@@ -110,10 +110,7 @@ func verifyDaemonSettings(config *Config) error {
110 110
 func checkSystem() error {
111 111
 	// Validate the OS version. Note that docker.exe must be manifested for this
112 112
 	// call to return the correct version.
113
-	osv, err := system.GetOSVersion()
114
-	if err != nil {
115
-		return err
116
-	}
113
+	osv := system.GetOSVersion()
117 114
 	if osv.MajorVersion < 10 {
118 115
 		return fmt.Errorf("This version of Windows does not support the docker daemon")
119 116
 	}
... ...
@@ -135,10 +132,7 @@ func configureMaxThreads(config *Config) error {
135 135
 
136 136
 func (daemon *Daemon) initNetworkController(config *Config) (libnetwork.NetworkController, error) {
137 137
 	// TODO Windows: Remove this check once TP4 is no longer supported
138
-	osv, err := system.GetOSVersion()
139
-	if err != nil {
140
-		return nil, err
141
-	}
138
+	osv := system.GetOSVersion()
142 139
 
143 140
 	if osv.Build < 14260 {
144 141
 		// Set the name of the virtual switch if not specified by -b on daemon start
... ...
@@ -364,8 +358,8 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro
364 364
 	}
365 365
 
366 366
 	// Convert imageData to valid image configuration
367
-	for i := range imageInfos {
368
-		name := strings.ToLower(imageInfos[i].Name)
367
+	for _, info := range imageInfos {
368
+		name := strings.ToLower(info.Name)
369 369
 
370 370
 		type registrar interface {
371 371
 			RegisterDiffID(graphID string, size int64) (layer.Layer, error)
... ...
@@ -374,13 +368,13 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro
374 374
 		if !ok {
375 375
 			return errors.New("Layerstore doesn't support RegisterDiffID")
376 376
 		}
377
-		if _, err := r.RegisterDiffID(imageInfos[i].ID, imageInfos[i].Size); err != nil {
377
+		if _, err := r.RegisterDiffID(info.ID, info.Size); err != nil {
378 378
 			return err
379 379
 		}
380 380
 		// layer is intentionally not released
381 381
 
382 382
 		rootFS := image.NewRootFS()
383
-		rootFS.BaseLayer = filepath.Base(imageInfos[i].Path)
383
+		rootFS.BaseLayer = filepath.Base(info.Path)
384 384
 
385 385
 		// Create history for base layer
386 386
 		config, err := json.Marshal(&image.Image{
... ...
@@ -388,10 +382,12 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro
388 388
 				DockerVersion: dockerversion.Version,
389 389
 				Architecture:  runtime.GOARCH,
390 390
 				OS:            runtime.GOOS,
391
-				Created:       imageInfos[i].CreatedTime,
391
+				Created:       info.CreatedTime,
392 392
 			},
393
-			RootFS:  rootFS,
394
-			History: []image.History{},
393
+			RootFS:     rootFS,
394
+			History:    []image.History{},
395
+			OSVersion:  info.OSVersion,
396
+			OSFeatures: info.OSFeatures,
395 397
 		})
396 398
 
397 399
 		named, err := reference.ParseNamed(name)
... ...
@@ -399,7 +395,7 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro
399 399
 			return err
400 400
 		}
401 401
 
402
-		ref, err := reference.WithTag(named, imageInfos[i].Version)
402
+		ref, err := reference.WithTag(named, info.Version)
403 403
 		if err != nil {
404 404
 			return err
405 405
 		}
... ...
@@ -401,6 +401,8 @@ type CustomImageInfo struct {
401 401
 	Path        string
402 402
 	Size        int64
403 403
 	CreatedTime time.Time
404
+	OSVersion   string   `json:"-"`
405
+	OSFeatures  []string `json:"-"`
404 406
 }
405 407
 
406 408
 // GetCustomImageInfos returns the image infos for window specific
... ...
@@ -441,6 +443,21 @@ func (d *Driver) GetCustomImageInfos() ([]CustomImageInfo, error) {
441 441
 		}
442 442
 
443 443
 		imageData.ID = id
444
+
445
+		// For now, hard code that all base images except nanoserver depend on win32k support
446
+		if imageData.Name != "nanoserver" {
447
+			imageData.OSFeatures = append(imageData.OSFeatures, "win32k")
448
+		}
449
+
450
+		versionData := strings.Split(imageData.Version, ".")
451
+		if len(versionData) != 4 {
452
+			logrus.Warn("Could not parse Windows version %s", imageData.Version)
453
+		} else {
454
+			// Include just major.minor.build, skip the fourth version field, which does not influence
455
+			// OS compatibility.
456
+			imageData.OSVersion = strings.Join(versionData[:3], ".")
457
+		}
458
+
444 459
 		images = append(images, imageData)
445 460
 	}
446 461
 
... ...
@@ -616,7 +616,9 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
616 616
 		// TODO(aaronl): The manifest list spec supports optional
617 617
 		// "features" and "variant" fields. These are not yet used.
618 618
 		// Once they are, their values should be interpreted here.
619
-		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS {
619
+		// TODO(jstarks): Once os.version and os.features are present,
620
+		// pass these, too.
621
+		if image.ValidateOSCompatibility(manifestDescriptor.Platform.OS, manifestDescriptor.Platform.Architecture, "", nil) == nil {
620 622
 			manifestDigest = manifestDescriptor.Digest
621 623
 			break
622 624
 		}
623 625
new file mode 100644
... ...
@@ -0,0 +1,38 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"runtime"
5
+	"strings"
6
+)
7
+
8
+func archMatches(arch string) bool {
9
+	// Special case x86_64 as an alias for amd64
10
+	return arch == runtime.GOARCH || (arch == "x86_64" && runtime.GOARCH == "amd64")
11
+}
12
+
13
+// ValidateOSCompatibility validates that an image with the given properties can run on this machine.
14
+func ValidateOSCompatibility(os string, arch string, osVersion string, osFeatures []string) error {
15
+	if os != "" && os != runtime.GOOS {
16
+		return fmt.Errorf("image is for OS %s, expected %s", os, runtime.GOOS)
17
+	}
18
+	if arch != "" && !archMatches(arch) {
19
+		return fmt.Errorf("image is for architecture %s, expected %s", arch, runtime.GOARCH)
20
+	}
21
+	if osVersion != "" {
22
+		thisOSVersion := getOSVersion()
23
+		if thisOSVersion != osVersion {
24
+			return fmt.Errorf("image is for OS version '%s', expected '%s'", osVersion, thisOSVersion)
25
+		}
26
+	}
27
+	var missing []string
28
+	for _, f := range osFeatures {
29
+		if !hasOSFeature(f) {
30
+			missing = append(missing, f)
31
+		}
32
+	}
33
+	if len(missing) > 0 {
34
+		return fmt.Errorf("image requires missing OS features: %s", strings.Join(missing, ", "))
35
+	}
36
+	return nil
37
+}
0 38
new file mode 100644
... ...
@@ -0,0 +1,28 @@
0
+package image
1
+
2
+import (
3
+	"runtime"
4
+	"testing"
5
+)
6
+
7
+func TestValidateOSCompatibility(t *testing.T) {
8
+	err := ValidateOSCompatibility(runtime.GOOS, runtime.GOARCH, getOSVersion(), nil)
9
+	if err != nil {
10
+		t.Error(err)
11
+	}
12
+
13
+	err = ValidateOSCompatibility("DOS", runtime.GOARCH, getOSVersion(), nil)
14
+	if err == nil {
15
+		t.Error("expected OS compat error")
16
+	}
17
+
18
+	err = ValidateOSCompatibility(runtime.GOOS, "pdp-11", getOSVersion(), nil)
19
+	if err == nil {
20
+		t.Error("expected architecture compat error")
21
+	}
22
+
23
+	err = ValidateOSCompatibility(runtime.GOOS, runtime.GOARCH, "98 SE", nil)
24
+	if err == nil {
25
+		t.Error("expected OS version compat error")
26
+	}
27
+}
0 28
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+// +build !windows
1
+
2
+package image
3
+
4
+func getOSVersion() string {
5
+	// For Linux, images do not specify a version.
6
+	return ""
7
+}
8
+
9
+func hasOSFeature(_ string) bool {
10
+	// Linux currently has no OS features
11
+	return false
12
+}
0 13
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/pkg/system"
6
+)
7
+
8
+// Windows OS features
9
+const (
10
+	FeatureWin32k = "win32k" // The kernel windowing stack is required
11
+)
12
+
13
+func getOSVersion() string {
14
+	v := system.GetOSVersion()
15
+	return fmt.Sprintf("%d.%d.%d", v.MajorVersion, v.MinorVersion, v.Build)
16
+}
17
+
18
+func hasOSFeature(f string) bool {
19
+	switch f {
20
+	case FeatureWin32k:
21
+		return system.HasWin32KSupport()
22
+	default:
23
+		// Unrecognized feature.
24
+		return false
25
+	}
26
+}
... ...
@@ -48,9 +48,11 @@ type V1Image struct {
48 48
 // Image stores the image configuration
49 49
 type Image struct {
50 50
 	V1Image
51
-	Parent  ID        `json:"parent,omitempty"`
52
-	RootFS  *RootFS   `json:"rootfs,omitempty"`
53
-	History []History `json:"history,omitempty"`
51
+	Parent     ID        `json:"parent,omitempty"`
52
+	RootFS     *RootFS   `json:"rootfs,omitempty"`
53
+	History    []History `json:"history,omitempty"`
54
+	OSVersion  string    `json:"os.version,omitempty"`
55
+	OSFeatures []string  `json:"os.features,omitempty"`
54 56
 
55 57
 	// rawJSON caches the immutable JSON associated with this image.
56 58
 	rawJSON []byte
... ...
@@ -127,6 +127,11 @@ func (is *store) Create(config []byte) (ID, error) {
127 127
 		return "", errors.New("too many non-empty layers in History section")
128 128
 	}
129 129
 
130
+	err = ValidateOSCompatibility(img.OS, img.Architecture, img.OSVersion, img.OSFeatures)
131
+	if err != nil {
132
+		return "", err
133
+	}
134
+
130 135
 	dgst, err := is.fs.Set(config)
131 136
 	if err != nil {
132 137
 		return "", err
... ...
@@ -3,6 +3,7 @@ package v1
3 3
 import (
4 4
 	"encoding/json"
5 5
 	"fmt"
6
+	"reflect"
6 7
 	"regexp"
7 8
 	"strings"
8 9
 
... ...
@@ -118,8 +119,15 @@ func MakeV1ConfigFromConfig(img *image.Image, v1ID, parentV1ID string, throwaway
118 118
 	}
119 119
 
120 120
 	// Delete fields that didn't exist in old manifest
121
-	delete(configAsMap, "rootfs")
122
-	delete(configAsMap, "history")
121
+	imageType := reflect.TypeOf(img).Elem()
122
+	for i := 0; i < imageType.NumField(); i++ {
123
+		f := imageType.Field(i)
124
+		jsonName := strings.Split(f.Tag.Get("json"), ",")[0]
125
+		// Parent is handled specially below.
126
+		if jsonName != "" && jsonName != "parent" {
127
+			delete(configAsMap, jsonName)
128
+		}
129
+	}
123 130
 	configAsMap["id"] = rawJSON(v1ID)
124 131
 	if parentV1ID != "" {
125 132
 		configAsMap["parent"] = rawJSON(parentV1ID)
126 133
new file mode 100644
... ...
@@ -0,0 +1,55 @@
0
+package v1
1
+
2
+import (
3
+	"encoding/json"
4
+	"testing"
5
+
6
+	"github.com/docker/docker/image"
7
+)
8
+
9
+func TestMakeV1ConfigFromConfig(t *testing.T) {
10
+	img := &image.Image{
11
+		V1Image: image.V1Image{
12
+			ID:     "v2id",
13
+			Parent: "v2parent",
14
+			OS:     "os",
15
+		},
16
+		OSVersion: "osversion",
17
+		RootFS: &image.RootFS{
18
+			Type: "layers",
19
+		},
20
+	}
21
+	v2js, err := json.Marshal(img)
22
+	if err != nil {
23
+		t.Fatal(err)
24
+	}
25
+
26
+	// Convert the image back in order to get RawJSON() support.
27
+	img, err = image.NewFromJSON(v2js)
28
+	if err != nil {
29
+		t.Fatal(err)
30
+	}
31
+
32
+	js, err := MakeV1ConfigFromConfig(img, "v1id", "v1parent", false)
33
+	if err != nil {
34
+		t.Fatal(err)
35
+	}
36
+
37
+	newimg := &image.Image{}
38
+	err = json.Unmarshal(js, newimg)
39
+	if err != nil {
40
+		t.Fatal(err)
41
+	}
42
+
43
+	if newimg.V1Image.ID != "v1id" || newimg.Parent != "v1parent" {
44
+		t.Error("ids should have changed", newimg.V1Image.ID, newimg.V1Image.Parent)
45
+	}
46
+
47
+	if newimg.RootFS != nil {
48
+		t.Error("rootfs should have been removed")
49
+	}
50
+
51
+	if newimg.V1Image.OS != "os" {
52
+		t.Error("os should have been preserved")
53
+	}
54
+}
... ...
@@ -1,11 +1,14 @@
1 1
 package system
2 2
 
3 3
 import (
4
-	"fmt"
5 4
 	"syscall"
6 5
 	"unsafe"
7 6
 )
8 7
 
8
+var (
9
+	ntuserApiset = syscall.NewLazyDLL("ext-ms-win-ntuser-window-l1-1-0")
10
+)
11
+
9 12
 // OSVersion is a wrapper for Windows version information
10 13
 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx
11 14
 type OSVersion struct {
... ...
@@ -17,17 +20,18 @@ type OSVersion struct {
17 17
 
18 18
 // GetOSVersion gets the operating system version on Windows. Note that
19 19
 // docker.exe must be manifested to get the correct version information.
20
-func GetOSVersion() (OSVersion, error) {
20
+func GetOSVersion() OSVersion {
21 21
 	var err error
22 22
 	osv := OSVersion{}
23 23
 	osv.Version, err = syscall.GetVersion()
24 24
 	if err != nil {
25
-		return osv, fmt.Errorf("Failed to call GetVersion()")
25
+		// GetVersion never fails.
26
+		panic(err)
26 27
 	}
27 28
 	osv.MajorVersion = uint8(osv.Version & 0xFF)
28 29
 	osv.MinorVersion = uint8(osv.Version >> 8 & 0xFF)
29 30
 	osv.Build = uint16(osv.Version >> 16)
30
-	return osv, nil
31
+	return osv
31 32
 }
32 33
 
33 34
 // Unmount is a platform-specific helper function to call
... ...
@@ -58,3 +62,12 @@ func CommandLineToArgv(commandLine string) ([]string, error) {
58 58
 
59 59
 	return newArgs, nil
60 60
 }
61
+
62
+// HasWin32KSupport determines whether containers that depend on win32k can
63
+// run on this machine. Win32k is the driver used to implement windowing.
64
+func HasWin32KSupport() bool {
65
+	// For now, check for ntuser API support on the host. In the future, a host
66
+	// may support win32k in containers even if the host does not support ntuser
67
+	// APIs.
68
+	return ntuserApiset.Load() == nil
69
+}
61 70
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+package system
1
+
2
+import "testing"
3
+
4
+func TestHasWin32KSupport(t *testing.T) {
5
+	s := HasWin32KSupport() // make sure this doesn't panic
6
+
7
+	t.Logf("win32k: %v", s) // will be different on different platforms -- informative only
8
+}
... ...
@@ -59,10 +59,7 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
59 59
 // console which supports ANSI emulation, or fall-back to the golang emulator
60 60
 // (github.com/azure/go-ansiterm).
61 61
 func useNativeConsole() bool {
62
-	osv, err := system.GetOSVersion()
63
-	if err != nil {
64
-		return false
65
-	}
62
+	osv := system.GetOSVersion()
66 63
 
67 64
 	// Native console is not available before major version 10
68 65
 	if osv.MajorVersion < 10 {