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>
... | ... |
@@ -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 { |