diff --git a/components/engine/daemon/commit.go b/components/engine/daemon/commit.go index 2dd1f7c49d..c8bf230c2e 100644 --- a/components/engine/daemon/commit.go +++ b/components/engine/daemon/commit.go @@ -138,6 +138,8 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str var history []image.History rootFS := image.NewRootFS() + osVersion := "" + var osFeatures []string if container.ImageID != "" { img, err := daemon.imageStore.Get(container.ImageID) @@ -146,6 +148,8 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str } history = img.History rootFS = img.RootFS + osVersion = img.OSVersion + osFeatures = img.OSFeatures } l, err := daemon.layerStore.Register(rwTar, rootFS.ChainID()) @@ -180,8 +184,10 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str Author: c.Author, Created: h.Created, }, - RootFS: rootFS, - History: history, + RootFS: rootFS, + History: history, + OSFeatures: osFeatures, + OSVersion: osVersion, }) if err != nil { diff --git a/components/engine/daemon/daemon_windows.go b/components/engine/daemon/daemon_windows.go index 2d07011ad4..b1ad26c11b 100644 --- a/components/engine/daemon/daemon_windows.go +++ b/components/engine/daemon/daemon_windows.go @@ -110,10 +110,7 @@ func verifyDaemonSettings(config *Config) error { func checkSystem() error { // Validate the OS version. Note that docker.exe must be manifested for this // call to return the correct version. - osv, err := system.GetOSVersion() - if err != nil { - return err - } + osv := system.GetOSVersion() if osv.MajorVersion < 10 { return fmt.Errorf("This version of Windows does not support the docker daemon") } @@ -135,10 +132,7 @@ func configureMaxThreads(config *Config) error { func (daemon *Daemon) initNetworkController(config *Config) (libnetwork.NetworkController, error) { // TODO Windows: Remove this check once TP4 is no longer supported - osv, err := system.GetOSVersion() - if err != nil { - return nil, err - } + osv := system.GetOSVersion() if osv.Build < 14260 { // 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 } // Convert imageData to valid image configuration - for i := range imageInfos { - name := strings.ToLower(imageInfos[i].Name) + for _, info := range imageInfos { + name := strings.ToLower(info.Name) type registrar interface { RegisterDiffID(graphID string, size int64) (layer.Layer, error) @@ -374,13 +368,13 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro if !ok { return errors.New("Layerstore doesn't support RegisterDiffID") } - if _, err := r.RegisterDiffID(imageInfos[i].ID, imageInfos[i].Size); err != nil { + if _, err := r.RegisterDiffID(info.ID, info.Size); err != nil { return err } // layer is intentionally not released rootFS := image.NewRootFS() - rootFS.BaseLayer = filepath.Base(imageInfos[i].Path) + rootFS.BaseLayer = filepath.Base(info.Path) // Create history for base layer config, err := json.Marshal(&image.Image{ @@ -388,10 +382,12 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro DockerVersion: dockerversion.Version, Architecture: runtime.GOARCH, OS: runtime.GOOS, - Created: imageInfos[i].CreatedTime, + Created: info.CreatedTime, }, - RootFS: rootFS, - History: []image.History{}, + RootFS: rootFS, + History: []image.History{}, + OSVersion: info.OSVersion, + OSFeatures: info.OSFeatures, }) named, err := reference.ParseNamed(name) @@ -399,7 +395,7 @@ func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) erro return err } - ref, err := reference.WithTag(named, imageInfos[i].Version) + ref, err := reference.WithTag(named, info.Version) if err != nil { return err } diff --git a/components/engine/daemon/graphdriver/windows/windows.go b/components/engine/daemon/graphdriver/windows/windows.go index dd659dad0a..0ad079eaf5 100644 --- a/components/engine/daemon/graphdriver/windows/windows.go +++ b/components/engine/daemon/graphdriver/windows/windows.go @@ -401,6 +401,8 @@ type CustomImageInfo struct { Path string Size int64 CreatedTime time.Time + OSVersion string `json:"-"` + OSFeatures []string `json:"-"` } // GetCustomImageInfos returns the image infos for window specific @@ -441,6 +443,21 @@ func (d *Driver) GetCustomImageInfos() ([]CustomImageInfo, error) { } imageData.ID = id + + // For now, hard code that all base images except nanoserver depend on win32k support + if imageData.Name != "nanoserver" { + imageData.OSFeatures = append(imageData.OSFeatures, "win32k") + } + + versionData := strings.Split(imageData.Version, ".") + if len(versionData) != 4 { + logrus.Warn("Could not parse Windows version %s", imageData.Version) + } else { + // Include just major.minor.build, skip the fourth version field, which does not influence + // OS compatibility. + imageData.OSVersion = strings.Join(versionData[:3], ".") + } + images = append(images, imageData) } diff --git a/components/engine/distribution/pull_v2.go b/components/engine/distribution/pull_v2.go index 596d1c1321..6266c5b566 100644 --- a/components/engine/distribution/pull_v2.go +++ b/components/engine/distribution/pull_v2.go @@ -616,7 +616,9 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf // TODO(aaronl): The manifest list spec supports optional // "features" and "variant" fields. These are not yet used. // Once they are, their values should be interpreted here. - if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS { + // TODO(jstarks): Once os.version and os.features are present, + // pass these, too. + if image.ValidateOSCompatibility(manifestDescriptor.Platform.OS, manifestDescriptor.Platform.Architecture, "", nil) == nil { manifestDigest = manifestDescriptor.Digest break } diff --git a/components/engine/image/compat.go b/components/engine/image/compat.go new file mode 100644 index 0000000000..2873dcf6b3 --- /dev/null +++ b/components/engine/image/compat.go @@ -0,0 +1,38 @@ +package image + +import ( + "fmt" + "runtime" + "strings" +) + +func archMatches(arch string) bool { + // Special case x86_64 as an alias for amd64 + return arch == runtime.GOARCH || (arch == "x86_64" && runtime.GOARCH == "amd64") +} + +// ValidateOSCompatibility validates that an image with the given properties can run on this machine. +func ValidateOSCompatibility(os string, arch string, osVersion string, osFeatures []string) error { + if os != "" && os != runtime.GOOS { + return fmt.Errorf("image is for OS %s, expected %s", os, runtime.GOOS) + } + if arch != "" && !archMatches(arch) { + return fmt.Errorf("image is for architecture %s, expected %s", arch, runtime.GOARCH) + } + if osVersion != "" { + thisOSVersion := getOSVersion() + if thisOSVersion != osVersion { + return fmt.Errorf("image is for OS version '%s', expected '%s'", osVersion, thisOSVersion) + } + } + var missing []string + for _, f := range osFeatures { + if !hasOSFeature(f) { + missing = append(missing, f) + } + } + if len(missing) > 0 { + return fmt.Errorf("image requires missing OS features: %s", strings.Join(missing, ", ")) + } + return nil +} diff --git a/components/engine/image/compat_test.go b/components/engine/image/compat_test.go new file mode 100644 index 0000000000..dace5a843d --- /dev/null +++ b/components/engine/image/compat_test.go @@ -0,0 +1,28 @@ +package image + +import ( + "runtime" + "testing" +) + +func TestValidateOSCompatibility(t *testing.T) { + err := ValidateOSCompatibility(runtime.GOOS, runtime.GOARCH, getOSVersion(), nil) + if err != nil { + t.Error(err) + } + + err = ValidateOSCompatibility("DOS", runtime.GOARCH, getOSVersion(), nil) + if err == nil { + t.Error("expected OS compat error") + } + + err = ValidateOSCompatibility(runtime.GOOS, "pdp-11", getOSVersion(), nil) + if err == nil { + t.Error("expected architecture compat error") + } + + err = ValidateOSCompatibility(runtime.GOOS, runtime.GOARCH, "98 SE", nil) + if err == nil { + t.Error("expected OS version compat error") + } +} diff --git a/components/engine/image/compat_unix.go b/components/engine/image/compat_unix.go new file mode 100644 index 0000000000..ba5d6a0937 --- /dev/null +++ b/components/engine/image/compat_unix.go @@ -0,0 +1,13 @@ +// +build !windows + +package image + +func getOSVersion() string { + // For Linux, images do not specify a version. + return "" +} + +func hasOSFeature(_ string) bool { + // Linux currently has no OS features + return false +} diff --git a/components/engine/image/compat_windows.go b/components/engine/image/compat_windows.go new file mode 100644 index 0000000000..a8d488b0df --- /dev/null +++ b/components/engine/image/compat_windows.go @@ -0,0 +1,27 @@ +package image + +import ( + "fmt" + + "github.com/docker/docker/pkg/system" +) + +// Windows OS features +const ( + FeatureWin32k = "win32k" // The kernel windowing stack is required +) + +func getOSVersion() string { + v := system.GetOSVersion() + return fmt.Sprintf("%d.%d.%d", v.MajorVersion, v.MinorVersion, v.Build) +} + +func hasOSFeature(f string) bool { + switch f { + case FeatureWin32k: + return system.HasWin32KSupport() + default: + // Unrecognized feature. + return false + } +} diff --git a/components/engine/image/image.go b/components/engine/image/image.go index 524714083e..7a05e649fb 100644 --- a/components/engine/image/image.go +++ b/components/engine/image/image.go @@ -48,9 +48,11 @@ type V1Image struct { // Image stores the image configuration type Image struct { V1Image - Parent ID `json:"parent,omitempty"` - RootFS *RootFS `json:"rootfs,omitempty"` - History []History `json:"history,omitempty"` + Parent ID `json:"parent,omitempty"` + RootFS *RootFS `json:"rootfs,omitempty"` + History []History `json:"history,omitempty"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` // rawJSON caches the immutable JSON associated with this image. rawJSON []byte diff --git a/components/engine/image/store.go b/components/engine/image/store.go index 92ac438db7..0512203938 100644 --- a/components/engine/image/store.go +++ b/components/engine/image/store.go @@ -127,6 +127,11 @@ func (is *store) Create(config []byte) (ID, error) { return "", errors.New("too many non-empty layers in History section") } + err = ValidateOSCompatibility(img.OS, img.Architecture, img.OSVersion, img.OSFeatures) + if err != nil { + return "", err + } + dgst, err := is.fs.Set(config) if err != nil { return "", err diff --git a/components/engine/image/v1/imagev1.go b/components/engine/image/v1/imagev1.go index e27ebd4c0a..0e2e72bda5 100644 --- a/components/engine/image/v1/imagev1.go +++ b/components/engine/image/v1/imagev1.go @@ -3,6 +3,7 @@ package v1 import ( "encoding/json" "fmt" + "reflect" "regexp" "strings" @@ -118,8 +119,15 @@ func MakeV1ConfigFromConfig(img *image.Image, v1ID, parentV1ID string, throwaway } // Delete fields that didn't exist in old manifest - delete(configAsMap, "rootfs") - delete(configAsMap, "history") + imageType := reflect.TypeOf(img).Elem() + for i := 0; i < imageType.NumField(); i++ { + f := imageType.Field(i) + jsonName := strings.Split(f.Tag.Get("json"), ",")[0] + // Parent is handled specially below. + if jsonName != "" && jsonName != "parent" { + delete(configAsMap, jsonName) + } + } configAsMap["id"] = rawJSON(v1ID) if parentV1ID != "" { configAsMap["parent"] = rawJSON(parentV1ID) diff --git a/components/engine/image/v1/imagev1_test.go b/components/engine/image/v1/imagev1_test.go new file mode 100644 index 0000000000..936c55e4c5 --- /dev/null +++ b/components/engine/image/v1/imagev1_test.go @@ -0,0 +1,55 @@ +package v1 + +import ( + "encoding/json" + "testing" + + "github.com/docker/docker/image" +) + +func TestMakeV1ConfigFromConfig(t *testing.T) { + img := &image.Image{ + V1Image: image.V1Image{ + ID: "v2id", + Parent: "v2parent", + OS: "os", + }, + OSVersion: "osversion", + RootFS: &image.RootFS{ + Type: "layers", + }, + } + v2js, err := json.Marshal(img) + if err != nil { + t.Fatal(err) + } + + // Convert the image back in order to get RawJSON() support. + img, err = image.NewFromJSON(v2js) + if err != nil { + t.Fatal(err) + } + + js, err := MakeV1ConfigFromConfig(img, "v1id", "v1parent", false) + if err != nil { + t.Fatal(err) + } + + newimg := &image.Image{} + err = json.Unmarshal(js, newimg) + if err != nil { + t.Fatal(err) + } + + if newimg.V1Image.ID != "v1id" || newimg.Parent != "v1parent" { + t.Error("ids should have changed", newimg.V1Image.ID, newimg.V1Image.Parent) + } + + if newimg.RootFS != nil { + t.Error("rootfs should have been removed") + } + + if newimg.V1Image.OS != "os" { + t.Error("os should have been preserved") + } +} diff --git a/components/engine/pkg/system/syscall_windows.go b/components/engine/pkg/system/syscall_windows.go index 061e220f79..ef596f343f 100644 --- a/components/engine/pkg/system/syscall_windows.go +++ b/components/engine/pkg/system/syscall_windows.go @@ -1,11 +1,14 @@ package system import ( - "fmt" "syscall" "unsafe" ) +var ( + ntuserApiset = syscall.NewLazyDLL("ext-ms-win-ntuser-window-l1-1-0") +) + // OSVersion is a wrapper for Windows version information // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx type OSVersion struct { @@ -17,17 +20,18 @@ type OSVersion struct { // GetOSVersion gets the operating system version on Windows. Note that // docker.exe must be manifested to get the correct version information. -func GetOSVersion() (OSVersion, error) { +func GetOSVersion() OSVersion { var err error osv := OSVersion{} osv.Version, err = syscall.GetVersion() if err != nil { - return osv, fmt.Errorf("Failed to call GetVersion()") + // GetVersion never fails. + panic(err) } osv.MajorVersion = uint8(osv.Version & 0xFF) osv.MinorVersion = uint8(osv.Version >> 8 & 0xFF) osv.Build = uint16(osv.Version >> 16) - return osv, nil + return osv } // Unmount is a platform-specific helper function to call @@ -58,3 +62,12 @@ func CommandLineToArgv(commandLine string) ([]string, error) { return newArgs, nil } + +// HasWin32KSupport determines whether containers that depend on win32k can +// run on this machine. Win32k is the driver used to implement windowing. +func HasWin32KSupport() bool { + // For now, check for ntuser API support on the host. In the future, a host + // may support win32k in containers even if the host does not support ntuser + // APIs. + return ntuserApiset.Load() == nil +} diff --git a/components/engine/pkg/system/syscall_windows_test.go b/components/engine/pkg/system/syscall_windows_test.go new file mode 100644 index 0000000000..4886b2b9b4 --- /dev/null +++ b/components/engine/pkg/system/syscall_windows_test.go @@ -0,0 +1,9 @@ +package system + +import "testing" + +func TestHasWin32KSupport(t *testing.T) { + s := HasWin32KSupport() // make sure this doesn't panic + + t.Logf("win32k: %v", s) // will be different on different platforms -- informative only +} diff --git a/components/engine/pkg/term/term_windows.go b/components/engine/pkg/term/term_windows.go index a02e681f44..905c02253d 100644 --- a/components/engine/pkg/term/term_windows.go +++ b/components/engine/pkg/term/term_windows.go @@ -59,10 +59,7 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { // console which supports ANSI emulation, or fall-back to the golang emulator // (github.com/azure/go-ansiterm). func useNativeConsole() bool { - osv, err := system.GetOSVersion() - if err != nil { - return false - } + osv := system.GetOSVersion() // Native console is not available before major version 10 if osv.MajorVersion < 10 {