Browse code

Merge pull request #21272 from Microsoft/jstarks/manifest_updates

Add os_version and os_features to Image

John Howard authored on 2016/04/06 08:16:25
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
 		}
... ...
@@ -405,6 +405,8 @@ type CustomImageInfo struct {
405 405
 	Path        string
406 406
 	Size        int64
407 407
 	CreatedTime time.Time
408
+	OSVersion   string   `json:"-"`
409
+	OSFeatures  []string `json:"-"`
408 410
 }
409 411
 
410 412
 // GetCustomImageInfos returns the image infos for window specific
... ...
@@ -445,6 +447,21 @@ func (d *Driver) GetCustomImageInfos() ([]CustomImageInfo, error) {
445 445
 		}
446 446
 
447 447
 		imageData.ID = id
448
+
449
+		// For now, hard code that all base images except nanoserver depend on win32k support
450
+		if imageData.Name != "nanoserver" {
451
+			imageData.OSFeatures = append(imageData.OSFeatures, "win32k")
452
+		}
453
+
454
+		versionData := strings.Split(imageData.Version, ".")
455
+		if len(versionData) != 4 {
456
+			logrus.Warn("Could not parse Windows version %s", imageData.Version)
457
+		} else {
458
+			// Include just major.minor.build, skip the fourth version field, which does not influence
459
+			// OS compatibility.
460
+			imageData.OSVersion = strings.Join(versionData[:3], ".")
461
+		}
462
+
448 463
 		images = append(images, imageData)
449 464
 	}
450 465
 
... ...
@@ -628,7 +628,9 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
628 628
 		// TODO(aaronl): The manifest list spec supports optional
629 629
 		// "features" and "variant" fields. These are not yet used.
630 630
 		// Once they are, their values should be interpreted here.
631
-		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS {
631
+		// TODO(jstarks): Once os.version and os.features are present,
632
+		// pass these, too.
633
+		if image.ValidateOSCompatibility(manifestDescriptor.Platform.OS, manifestDescriptor.Platform.Architecture, "", nil) == nil {
632 634
 			manifestDigest = manifestDescriptor.Digest
633 635
 			break
634 636
 		}
635 637
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
+}
... ...
@@ -57,10 +57,7 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
57 57
 // console which supports ANSI emulation, or fall-back to the golang emulator
58 58
 // (github.com/azure/go-ansiterm).
59 59
 func useNativeConsole() bool {
60
-	osv, err := system.GetOSVersion()
61
-	if err != nil {
62
-		return false
63
-	}
60
+	osv := system.GetOSVersion()
64 61
 
65 62
 	// Native console is not available before major version 10
66 63
 	if osv.MajorVersion < 10 {