8000 windows: daemon: allow pulling/running of newer images under Hyper-V. · aznashwan/moby@0280642 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0280642

Browse files
committed
windows: daemon: allow pulling/running of newer images under Hyper-V.
Following the addition of Hyper-V isolation, it should be possible for Windows hosts to run container images with older/newer OS versions given a Windows host with the RS5 updates and above. This patch includes the following changes: - [distribution/pull_v2] enable pulling images newer than the host on RS5+ - [daemon/create] add version checks which prevent running newer/older images unless using Hyper-V isolation Previously, pulling a Windows image with a newer OS version than the host was prevented by moby#36327, whose behavior is preserved on pre-RS5 hosts. Signed-off-by: Nashwan Azhari <nazhari@cloudbasesolutions.com>
1 parent cba6f2d commit 0280642

File tree

5 files changed

+184
-3
lines changed

5 files changed

+184
-3
lines changed

daemon/create.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ func (daemon *Daemon) containerCreate(ctx context.Context, opts createOpts) (con
6767
return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err)
6868
}
6969

70+
var img *image.Image
7071
if opts.params.Platform == nil && opts.params.Config.Image != "" {
71-
img, err := daemon.imageService.GetImage(ctx, opts.params.Config.Image, imagetypes.GetImageOpts{Platform: opts.params.Platform})
72+
img, err = daemon.imageService.GetImage(ctx, opts.params.Config.Image, imagetypes.GetImageOpts{Platform: opts.params.Platform})
7273
if err != nil {
7374
return containertypes.CreateResponse{}, err
7475
}
@@ -99,6 +100,11 @@ func (daemon *Daemon) containerCreate(ctx context.Context, opts createOpts) (con
99100
return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err)
100101
}
101102

103+
err = daemon.checkImageIsolationCompatiblity(opts.params.HostConfig, img)
104+
if err != nil {
105+
return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err)
106+
}
107+
102108
ctr, err := daemon.create(ctx, opts)
103109
if err != nil {
104110
return containertypes.CreateResponse{Warnings: warnings}, err

daemon/create_unix.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
containertypes "github.com/docker/docker/api/types/container"
1313
mounttypes "github.com/docker/docker/api/types/mount"
1414
"github.com/docker/docker/container"
15+
"github.com/docker/docker/image"
1516
"github.com/docker/docker/oci"
1617
volumeopts "github.com/docker/docker/volume/service/opts"
1718
"github.com/opencontainers/selinux/go-selinux/label"
@@ -93,3 +94,10 @@ func (daemon *Daemon) populateVolumes(c *container.Container) error {
9394
}
9495
return nil
9596
}
97+
98+
// checkImageIsolationCompatiblity checks whether the provided image is compatible
99+
// with the host config's isolation settings.
100+
// This is currently only used for determining image compatiblity on Windows.
101+
func (daemon *Daemon) checkImageIsolationCompatiblity(hostConfig *containertypes.HostConfig, img *image.Image) error {
102+
return nil
103+
}

daemon/create_windows.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ package daemon // import "github.com/docker/docker/daemon"
33
import (
44
"context"
55
"fmt"
6+
"strconv"
7+
"strings"
68

9+
"github.com/Microsoft/hcsshim/osversion"
710
containertypes "github.com/docker/docker/api/types/container"
811
"github.com/docker/docker/container"
12+
"github.com/docker/docker/image"
913
volumemounts "github.com/docker/docker/volume/mounts"
1014
volumeopts "github.com/docker/docker/volume/service/opts"
15+
"github.com/sirupsen/logrus"
1116
)
1217

1318
// createContainerOSSpecificSettings performs host-OS specific container create functionality
@@ -74,3 +79,66 @@ func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Con
7479
}
7580
return nil
7681
}
82+
83+
// checkImageIsolationCompatiblity checks whether the provided image is compatible
84+
// with the host config's isolation settings.
85+
func (daemon *Daemon) checkImageIsolationCompatiblity(hostConfig *containertypes.HostConfig, img *image.Image) error {
86+
hostOSV := osversion.Get()
87+
isHyperV := hostConfig.Isolation.IsHyperV()
88+
// fallback on default daemon isolation if none was explicitly provided:
89+
if hostConfig.Isolation.IsDefault() {
90+
isHyperV = daemon.defaultIsolation.IsHyperV()
91+
}
92+
return checkImageCompatibilityForHostIsolation(hostOSV, img.OSVersion, isHyperV)
93+
}
94+
95+
// checkImageCompatibilityForHostIsolation checks whether the provided `imageOSVersion`
96+
// should be able to run on the provided host OS version given Hyper-V isolation.
97+
// Its contained logic can be distilled into:
98+
// - if imageOS == hostOS => can run under any isolation mode
99+
// - if imageOS < hostOS => must be run under Hyper-V
100+
// - if imageOS > hostOS => can be run under Hyper-V only on an RS5 (1809+) host
101+
// Please see the below:
102+
// https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-2016%2Cwindows-10#windows-server-host-os-compatibility
103+
func checkImageCompatibilityForHostIsolation(hostOSV osversion.OSVersion, imageOSVersion string, isHyperV bool) error {
104+
var err error
105+
var imageOSBuildU64 uint64
106+
splitImageOSVersion := strings.Split(imageOSVersion, ".") // eg 10.0.16299.nnnn
107+
if len(splitImageOSVersion) >= 3 {
108+
if imageOSBuildU64, err = strconv.ParseUint(splitImageOSVersion[2], 10, 16); err == nil {
109+
logrus.Debugf("parsed Windows build number %d from image OS version %s", imageOSBuildU64, imageOSVersion)
110+
} else {
111+
return fmt.Errorf("failed to ParseUint() Windows image build %q from image version %q: %s", splitImageOSVersion[2], imageOSVersion, err)
112+
}
113+
} else {
114+
return fmt.Errorf("failed to split and parse Windows image version %q (was expecting format like '10.0.16299.nnnn')", imageOSVersion)
115+
}
116+
truncatedImageOSVersion := fmt.Sprintf("%s.%s.%s", splitImageOSVersion[0], splitImageOSVersion[1], splitImageOSVersion[2])
117+
118+
imageOSBuild := uint16(imageOSBuildU64)
119+
if imageOSBuild == hostOSV.Build {
120+
// same image version should run on identically-versioned host regardless of isolation:
121+
logrus.Debugf("image version %s is trivially compatible with host version %s", truncatedImageOSVersion, hostOSV.ToString())
122+
return nil
123+
} else if imageOSBuild < hostOSV.Build {
124+
// images older than the host must be run under Hyper-V:
125+
if isHyperV {
126+
logrus.Debugf("older image version %s is compatible with host version %s under Hyper-V", truncatedImageOSVersion, hostOSV.ToString())
127+
return nil
128+
} else {
129+
return fmt.Errorf("an older Windows version %s-based image can only be run on a %s host using Hyper-V isolation", truncatedImageOSVersion, hostOSV.ToString())
130+
}
131+
} else {
132+
// images newer than the host can only run if the host is RS5 and is using Hyper-V:
133+
if hostOSV.Build < osversion.RS5 {
134+
return fmt.Errorf("a Windows version %s-based image is incompatible with a %s host", truncatedImageOSVersion, hostOSV.ToString())
135+
} else {
136+
if isHyperV {
137+
logrus.Debugf("newer Windows image version %s is compatible with host version %s under Hyper-V", truncatedImageOSVersion, hostOSV.ToString())
138+
return nil
139+
} else {
140+
return fmt.Errorf("a newer Windows version %s-based image can only be run on a %s host using Hyper-V isolation", truncatedImageOSVersion, hostOSV.ToString())
141+
}
142+
}
143+
}
144+
}

daemon/create_windows_test.go

Lines changed: 88 additions & 0 deletions
6D38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package daemon
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/Microsoft/hcsshim/osversion"
8+
"gotest.tools/v3/assert"
9+
is "gotest.tools/v3/assert/cmp"
10+
)
11+
12+
func TestCheckImageCompatibilityForHostIsolation(t *testing.T) {
13+
testCases := []struct {
14+
hostBuildNo uint16
15+
imageBuildNo uint16
16+
isHyperV bool
17+
expectedErrMsg string
18+
}{
19+
// coincinding build numbers are valid regardless of isolation:
20+
{
21+
hostBuildNo: osversion.RS1,
22+
imageBuildNo: osversion.RS1,
23+
isHyperV: false,
24+
expectedErrMsg: "",
25+
},
26+
{
27+
hostBuildNo: osversion.RS1,
28+
imageBuildNo: osversion.RS1,
29+
isHyperV: true,
30+
expectedErrMsg: "",
31+
},
32+
// images older than the host must be run using Hyper-V:
33+
{
34+
hostBuildNo: osversion.RS2,
35+
imageBuildNo: osversion.RS1,
36+
isHyperV: false,
37+
expectedErrMsg: "an older Windows version .*-based image can only be run on a .* host using Hyper-V isolation",
38+
},
39+
{
40+
hostBuildNo: osversion.RS2,
41+
imageBuildNo: osversion.RS1,
42+
isHyperV: true,
43+
expectedErrMsg: "",
44+
},
45+
// images newer than the host can not run prior to host version RS5:
46+
{
47+
hostBuildNo: osversion.RS4,
48+
imageBuildNo: osversion.RS5,
49+
isHyperV: false,
50+
expectedErrMsg: "a Windows version .*-based image is incompatible with a .* host",
51+
},
52+
{
53+
hostBuildNo: osversion.RS4,
54+
imageBuildNo: osversion.RS5,
55+
isHyperV: true,
56+
expectedErrMsg: "a Windows version .*-based image is incompatible with a .* host",
57+
},
58+
// images newer than an RS5+ host can only run using Hyper-V:
59+
{
60+
hostBuildNo: osversion.RS5,
61+
imageBuildNo: osversion.V21H2Server, // ltsc2022
62+
isHyperV: false,
63+
expectedErrMsg: "a newer Windows version .*-based image can only be run on a .* host using Hyper-V isolation",
64+
},
65+
{
66+
hostBuildNo: osversion.RS5,
67+
imageBuildNo: osversion.V21H2Server,
68+
isHyperV: true,
69+
expectedErrMsg: "",
70+
},
71+
}
72+
73+
for _, testCase := range testCases {
74+
hostVersion := osversion.OSVersion{
75+
Version: 0,
76+
MajorVersion: 0,
77+
MinorVersion: 0,
78+
Build: testCase.hostBuildNo,
79+
}
80+
imageOSVersion := fmt.Sprintf("10.0.%d", testCase.imageBuildNo)
81+
err := checkImageCompatibilityForHostIsolation(hostVersion, imageOSVersion, testCase.isHyperV)
82+
if testCase.expectedErrMsg == "" {
83+
assert.NilError(t, err)
84+
} else {
85+
assert.Check(t, is.Regexp(testCase.expectedErrMsg, err.Error()))
86+
}
87+
}
88+
}

distribution/pull_v2_windows.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,26 @@ func (mbv manifestsByVersion) Swap(i, j int) {
125125
func checkImageCompatibility(imageOS, imageOSVersion string) error {
126126
if imageOS == "windows" {
127127
hostOSV := osversion.Get()
128+
// NOTE: RS5 no longer carries extra requirements for image build number compatibility
129+
// when running later images under Hyper-V, so we allow this check to pass and only error
130+
// later if the image is attempted to be run under Process isolation.
131+
if hostOSV.Build >= osversion.RS5 {
132+
return nil
133+
}
134+
128135
splitImageOSVersion := strings.Split(imageOSVersion, ".") // eg 10.0.16299.nnnn
129136
if len(splitImageOSVersion) >= 3 {
130-
if imageOSBuild, err := strconv.Atoi(splitImageOSVersion[2]); err == nil {
131-
if imageOSBuild > int(hostOSV.Build) {
137+
if imageOSBuild, err := strconv.ParseUint(splitImageOSVersion[2], 10, 16); err == nil {
138+
if uint16(imageOSBuild) > hostOSV.Build {
132139
errMsg := fmt.Sprintf("a Windows version %s.%s.%s-based image is incompatible with a %s host", splitImageOSVersion[0], splitImageOSVersion[1], splitImageOSVersion[2], hostOSV.ToString())
133140
logrus.Debugf(errMsg)
134141
return errors.New(errMsg)
142+
} else {
143+
logrus.Debugf("image version %s.%s.%s is compatible with host version %s", splitImageOSVersion[0], splitImageOSVersion[1], splitImageOSVersion[2], hostOSV.ToString())
135144
}
136145
}
146+
} else {
147+
logrus.Warnf("failed to split and parse Windows image version %q (was expecting format like '10.0.16299.nnnn')", imageOSVersion)
137148
}
138149
}
139150
return nil

0 commit comments

Comments
 (0)
0