diff --git a/platforms/defaults_windows.go b/platforms/defaults_windows.go index fd5756516cfc9..229b9b296ea17 100644 --- a/platforms/defaults_windows.go +++ b/platforms/defaults_windows.go @@ -42,6 +42,7 @@ type windowsmatcher struct { specs.Platform osVersionPrefix string defaultMatcher Matcher + isClientOS bool } // Match matches platform with the same windows major, minor @@ -53,6 +54,31 @@ func (m windowsmatcher) Match(p specs.Platform) bool { if strings.HasPrefix(p.OSVersion, m.osVersionPrefix) { return true } + if m.isClientOS && p.OSVersion != "" { + split := strings.Split(p.OSVersion, ".") + if len(split) >= 3 { + major, err := strconv.Atoi(split[0]) + if err != nil { + return false + } + minor, err := strconv.Atoi(split[1]) + if err != nil { + return false + } + build, err := strconv.Atoi(split[2]) + if err != nil { + return false + } + + if (major == 10 && minor == 0 && build >= 20348) || + (major == 10 && minor > 0) { + // Windows 11 client machines are implicitly + // compatible with 10.0.20348 (LTSC2022), even + // when the client has a newer build version. + return true + } + } + } return p.OSVersion == "" } @@ -61,16 +87,35 @@ func (m windowsmatcher) Match(p specs.Platform) bool { // Less sorts matched platforms in front of other platforms. // For matched platforms, it puts platforms with larger revision -// number in front. +// number in front (and for Win 11 clients, larger build numbers). func (m windowsmatcher) Less(p1, p2 specs.Platform) bool { m1, m2 := m.Match(p1), m.Match(p2) if m1 && m2 { + // Build number comparison will only be used for clients + // where differing build versions can match. + b1, b2 := build(p1.OSVersion), build(p2.OSVersion) + if b1 != b2 { + return b1 > b2 + } + r1, r2 := revision(p1.OSVersion), revision(p2.OSVersion) return r1 > r2 } return m1 && !m2 } +func build(v string) int { + parts := strings.Split(v, ".") + if len(parts) < 3 { + return 0 + } + r, err := strconv.Atoi(parts[2]) + if err != nil { + return 0 + } + return r +} + func revision(v string) int { parts := strings.Split(v, ".") if len(parts) < 4 { diff --git a/platforms/defaults_windows_test.go b/platforms/defaults_windows_test.go index 4684380384a6f..4561a63e4e7ed 100644 --- a/platforms/defaults_windows_test.go +++ b/platforms/defaults_windows_test.go @@ -80,6 +80,7 @@ func TestMatchComparerMatch_WCOW(t *testing.T) { defaultMatcher: &matcher{ Platform: Normalize(DefaultSpec()), }, + isClientOS: false, } for _, test := range []struct { platform imagespec.Platform @@ -158,6 +159,7 @@ func TestMatchComparerMatch_LCOW(t *testing.T) { }, ), }, + isClientOS: false, } for _, test := range []struct { platform imagespec.Platform @@ -210,6 +212,7 @@ func TestMatchComparerLess(t *testing.T) { defaultMatcher: &matcher{ Platform: Normalize(DefaultSpec()), }, + isClientOS: false, } platforms := []imagespec.Platform{ { @@ -268,3 +271,98 @@ func TestMatchComparerLess(t *testing.T) { }) assert.Equal(t, expected, platforms) } + +func TestMatchComparerClientOSMatch(t *testing.T) { + m := windowsmatcher{ + Platform: DefaultSpec(), + osVersionPrefix: "10.0.22000", + defaultMatcher: &matcher{ + Platform: Normalize(DefaultSpec()), + }, + isClientOS: true, + } + for _, test := range []struct { + platform imagespec.Platform + match bool + }{ + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.2114", + }, + match: false, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.20348.169", + }, + match: true, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.22000", + }, + match: true, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + }, + match: true, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "linux", + }, + match: false, + }, + } { + assert.Equal(t, test.match, m.Match(test.platform), "should match %b, %s to %s", test.match, m.Platform, test.platform) + } +} + +func TestMatchComparerClientOSPriority(t *testing.T) { + m := windowsmatcher{ + Platform: DefaultSpec(), + osVersionPrefix: "10.0.22000", + defaultMatcher: &matcher{ + Platform: Normalize(DefaultSpec()), + }, + isClientOS: true, + } + platforms := []imagespec.Platform{ + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.20348.169", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.22000", + }, + } + expected := []imagespec.Platform{ + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.22000", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.20348.169", + }, + } + sort.SliceStable(platforms, func(i, j int) bool { + return m.Less(platforms[i], platforms[j]) + }) + assert.Equal(t, expected, platforms) +} diff --git a/platforms/platforms_windows.go b/platforms/platforms_windows.go index 950e2a2ddbb57..5aa23b268bb8f 100644 --- a/platforms/platforms_windows.go +++ b/platforms/platforms_windows.go @@ -18,8 +18,24 @@ package platforms import ( specs "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/sys/windows/registry" ) +func isClientOS() bool { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return false + } + defer k.Close() + + installationType, _, err := k.GetStringValue("InstallationType") + if err != nil { + return false + } + + return installationType == "Client" +} + // NewMatcher returns a Windows matcher that will match on osVersionPrefix if // the platform is Windows otherwise use the default matcher func newDefaultMatcher(platform specs.Platform) Matcher { @@ -30,5 +46,6 @@ func newDefaultMatcher(platform specs.Platform) Matcher { defaultMatcher: &matcher{ Platform: Normalize(platform), }, + isClientOS: isClientOS(), } }