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> Upstream-commit: 194eaa5c0f843257e66b68bd735786308a9d93b2 Component: engine
This commit is contained in:
38
components/engine/image/compat.go
Normal file
38
components/engine/image/compat.go
Normal file
@ -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
|
||||
}
|
||||
28
components/engine/image/compat_test.go
Normal file
28
components/engine/image/compat_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
13
components/engine/image/compat_unix.go
Normal file
13
components/engine/image/compat_unix.go
Normal file
@ -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
|
||||
}
|
||||
27
components/engine/image/compat_windows.go
Normal file
27
components/engine/image/compat_windows.go
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
55
components/engine/image/v1/imagev1_test.go
Normal file
55
components/engine/image/v1/imagev1_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user