Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop relying on ReleaseId/DisplayVersion registry keys #191

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions ue4docker/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,8 @@ def build():
logger.info('Detected max image size: {:.0f}GB'.format(DockerUtils.maxsize()), False)
logger.info('Visual Studio: {}'.format(config.visualStudio), False)

# Verify that the specified base image tag is not a release that has reached End Of Life (EOL)
if not config.ignoreEOL and WindowsUtils.isEndOfLifeWindowsVersion(config.basetag):
logger.error('Error: detected EOL base OS image tag: {}'.format(config.basetag), False)
logger.error('This version of Windows has reached End Of Life (EOL), which means', False)
logger.error('Microsoft no longer supports or maintains container base images for it.', False)
logger.error('You will need to use a base image tag for a supported version of Windows.', False)
sys.exit(1)

# Verify that the host OS is not a release that is blacklisted due to critical bugs
if config.ignoreBlacklist == False and WindowsUtils.isBlacklistedWindowsVersion() == True:
if config.ignoreBlacklist == False and WindowsUtils.isBlacklistedWindowsHost() == True:
logger.error('Error: detected blacklisted host OS version: {}'.format(WindowsUtils.systemString()), False)
logger.error('', False)
logger.error('This version of Windows contains one or more critical bugs that', False)
Expand All @@ -140,9 +132,12 @@ def build():
sys.exit(1)

# Verify that the user is not attempting to build images with a newer kernel version than the host OS
if WindowsUtils.isNewerBaseTag(config.hostBasetag, config.basetag):
newer_check = WindowsUtils.isNewerBaseTag(config.hostBasetag, config.basetag)
if newer_check:
logger.error('Error: cannot build container images with a newer kernel version than that of the host OS!')
sys.exit(1)
elif newer_check is None:
logger.info('Warning: unable to determine whether host system is new enough to use specified base tag')

# Ensure the Docker daemon is configured correctly
requiredLimit = WindowsUtils.requiredSizeLimit()
Expand Down
10 changes: 7 additions & 3 deletions ue4docker/diagnostics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ def _generateWindowsBuildArgs(self, logger, basetagOverride=None, isolationOverr

# Determine the appropriate container image base tag for the host system release unless the user specified a base tag
buildArgs = []
defaultBaseTag = WindowsUtils.getReleaseBaseTag(WindowsUtils.getWindowsRelease())
baseTag = basetagOverride if basetagOverride is not None else defaultBaseTag
hostBaseTag = WindowsUtils.getHostBaseTag()
baseTag = basetagOverride if basetagOverride is not None else hostBaseTag

if baseTag is None:
raise RuntimeError('unable to determine Windows Server Core base image tag from host system. Specify it explicitly using -basetag command-line flag')

buildArgs = ['--build-arg', 'BASETAG={}'.format(baseTag)]

# Use the default isolation mode unless requested otherwise
Expand All @@ -72,7 +76,7 @@ def _generateWindowsBuildArgs(self, logger, basetagOverride=None, isolationOverr

# If the user specified process isolation mode and a different base tag to the host system then warn them
prefix = self.getPrefix()
if isolation == 'process' and baseTag != defaultBaseTag:
if isolation == 'process' and baseTag != hostBaseTag:
logger.info('[{}] Warning: attempting to use different Windows container/host versions'.format(prefix), False)
logger.info('[{}] when running in process isolation mode, this will usually break!'.format(prefix), False)

Expand Down
2 changes: 1 addition & 1 deletion ue4docker/diagnostics/diagnostic_20gig.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self):
# Setup our argument parser so we can use its help message output in our description text
self._parser = argparse.ArgumentParser(prog='ue4-docker diagnostics 20gig')
self._parser.add_argument('--isolation', default=None, choices=['hyperv', 'process'], help="Override the default isolation mode when testing Windows containers")
self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getValidBaseTags(), help="Override the default base image tag when testing Windows containers")
self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getKnownBaseTags(), help="Override the default base image tag when testing Windows containers")

def getName(self):
'''
Expand Down
2 changes: 1 addition & 1 deletion ue4docker/diagnostics/diagnostic_8gig.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self):
self._parser.add_argument('--linux', action='store_true', help="Use Linux containers under Windows hosts (useful when testing Docker Desktop or LCOW support)")
self._parser.add_argument('--random', action='store_true', help="Create a file filled with random bytes instead of zeroes under Windows")
self._parser.add_argument('--isolation', default=None, choices=['hyperv', 'process'], help="Override the default isolation mode when testing Windows containers")
self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getValidBaseTags(), help="Override the default base image tag when testing Windows containers")
self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getKnownBaseTags(), help="Override the default base image tag when testing Windows containers")

def getName(self):
'''
Expand Down
2 changes: 1 addition & 1 deletion ue4docker/diagnostics/diagnostic_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self):
self._parser = argparse.ArgumentParser(prog='ue4-docker diagnostics network')
self._parser.add_argument('--linux', action='store_true', help="Use Linux containers under Windows hosts (useful when testing Docker Desktop or LCOW support)")
self._parser.add_argument('--isolation', default=None, choices=['hyperv', 'process'], help="Override the default isolation mode when testing Windows containers")
self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getValidBaseTags(), help="Override the default base image tag when testing Windows containers")
self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getKnownBaseTags(), help="Override the default base image tag when testing Windows containers")

def getName(self):
'''
Expand Down
19 changes: 6 additions & 13 deletions ue4docker/infrastructure/BuildConfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ def addArguments(parser):
parser.add_argument('--combine', action='store_true', help='Combine generated Dockerfiles into a single multi-stage build Dockerfile')
parser.add_argument('--monitor', action='store_true', help='Monitor resource usage during builds (useful for debugging)')
parser.add_argument('-interval', type=float, default=20.0, help='Sampling interval in seconds when resource monitoring has been enabled using --monitor (default is 20 seconds)')
parser.add_argument('--ignore-eol', action='store_true', help='Run builds even on EOL versions of Windows (advanced use only)')
parser.add_argument('--ignore-blacklist', action='store_true', help='Run builds even on blacklisted versions of Windows (advanced use only)')
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output during builds (useful for debugging)')

Expand Down Expand Up @@ -166,7 +165,6 @@ def __init__(self, parser, argv):
self.excludedComponents = set(self.args.exclude)
self.baseImage = None
self.prereqsTag = None
self.ignoreEOL = self.args.ignore_eol
self.ignoreBlacklist = self.args.ignore_blacklist
self.verbose = self.args.verbose
self.layoutDir = self.args.layout
Expand Down Expand Up @@ -250,30 +248,25 @@ def _generateWindowsConfig(self):
self.opts['buildgraph_args'] = self.opts.get('buildgraph_args', '') + f' -set:VS{self.visualStudio}=true'

# Determine base tag for the Windows release of the host system
self.hostRelease = WindowsUtils.getWindowsRelease()
self.hostBasetag = WindowsUtils.getReleaseBaseTag(self.hostRelease)
self.hostBasetag = WindowsUtils.getHostBaseTag()

# Store the tag for the base Windows Server Core image
self.basetag = self.args.basetag if self.args.basetag is not None else self.hostBasetag

if self.basetag is None:
raise RuntimeError('unable to determine Windows Server Core base image tag from host system. Specify it explicitly using -basetag command-line flag')

self.baseImage = 'mcr.microsoft.com/windows/servercore:' + self.basetag
self.dllSrcImage = WindowsUtils.getDllSrcImage(self.basetag)
self.prereqsTag = self.basetag + '-vs' + self.visualStudio

# Verify that any user-specified base tag is valid
if WindowsUtils.isValidBaseTag(self.basetag) == False:
raise RuntimeError('unrecognised Windows Server Core base image tag "{}", supported tags are {}'.format(self.basetag, WindowsUtils.getValidBaseTags()))

# Verify that any user-specified tag suffix does not collide with our base tags
if WindowsUtils.isValidBaseTag(self.suffix) == True:
raise RuntimeError('tag suffix cannot be any of the Windows Server Core base image tags: {}'.format(WindowsUtils.getValidBaseTags()))

# If the user has explicitly specified an isolation mode then use it, otherwise auto-detect
if self.args.isolation is not None:
self.isolation = self.args.isolation
else:

# If we are able to use process isolation mode then use it, otherwise fallback to the Docker daemon's default isolation mode
differentKernels = WindowsUtils.isInsiderPreview() or self.basetag != self.hostBasetag
differentKernels = self.basetag != self.hostBasetag
dockerSupportsProcess = parse_version(DockerUtils.version()['Version']) >= parse_version('18.09.0')
if not differentKernels and dockerSupportsProcess:
self.isolation = 'process'
Expand Down
98 changes: 32 additions & 66 deletions ue4docker/infrastructure/WindowsUtils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .DockerUtils import DockerUtils
from pkg_resources import parse_version
import platform, sys
from typing import Optional

if platform.system() == 'Windows':
import winreg
Expand All @@ -10,18 +11,22 @@ class WindowsUtils(object):
# The oldest Windows build we support
_minimumRequiredBuild = 17763

# The latest Windows build version we recognise as a non-Insider build
_latestReleaseBuild = 19042
# This lookup table is based on the list of valid tags from <https://hub.docker.com/r/microsoft/windowsservercore/>
# and list of build-to-release mapping from https://docs.microsoft.com/en-us/windows/release-health/release-information
_knownTagsByBuildNumber = {
17763: 'ltsc2019',
18362: '1903',
18363: '1909',
19041: '2004',
19042: '20H2',
19043: '21H1',
}

# The list of Windows Server Core base image tags that we recognise, in ascending version number order
_validTags = ['ltsc2019', '1903', '1909', '2004', '20H2']
_knownTags = list(_knownTagsByBuildNumber.values())

# The list of Windows Server and Windows 10 host OS releases that are blacklisted due to critical bugs
# (See: <https://unrealcontainers.com/docs/concepts/windows-containers>)
_blacklistedReleases = ['1903', '1909']

# The list of Windows Server Core container image releases that are unsupported due to having reached EOL
_eolReleases = ['1903', '1909']
Comment on lines -23 to -24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows 10 2004 hits EOL in December, and 20H2 EOLs in May next year. So if we just disappear them entirely, then we lose support for any Windows Client platforms.

I'd keep the EOL support around until we either decide to drop Windows 10 process isolation support once there's no supported base images available, or something changes on the MS side, or some other intervening idea occurs.

Better to say "Yes, we recognise this, it's EOL, are you sure you want to continue" when we can recognise a version.

Also, these two versions are only blacklisted for Docker versions before 19.03.6, so this change actually promotes them from EOL warning with user-override to "supported", i.e. silently accepted.

Copy link
Collaborator Author

@slonopotamus slonopotamus Aug 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trick is that they're blacklisted as a host and EOL as a target. We silently allow building supported versions on EOL hosts even before this commit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows 10 2004 hits EOL in December, and 20H2 EOLs in May next year. So if we just disappear them entirely, then we lose support for any Windows Client platforms.

OK, agreed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point. I'm fine with not complaining about using an EOL host to build a supported target.

I'm ambivalent about complaining when we try to use an EOL target; I'd lean towards keeping the warning around based on the calculated base tag (i.e. after applying the user's specified base tag), but that isn't really comprehensive or reliable (e.g., basetag 1909-KB4574727-amd64 would not be recognised), so I don't mind if we drop it now.

My personal use-cases are 100% process-isolation-based (and Windows Server LTSC-based 95% of the time), so I haven't had any value from the EOL warning myself.

I'd be interested to know if @adamrehn has any concerns about dropping this (or use-cases where it prevents a bad outcome), but if not, then I'm happy to resolve this thread.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basetag 1909-KB4574727-amd64 would not be recognised

ue4-docker doesn't support building targets that are not hardcoded in valid tags list at all. #166 wanted to change that, but kinda failed.


I thought about 20H2 again. I very doubt that anyone would want to build ue4-docker images for it after it gets EOLed. If they're on a newer host, they would need to use Hyper-V isolation. And if you use Hyper-V isolation, why not just use a supported target version? Alternatively, they still run EOLed 20H2 on their host. Well, if you use EOLed Windows, also use EOLed version of ue4-docker that has 20H2 support, no?

_blacklistedHosts = [18362, 18363]

@staticmethod
def _getVersionRegKey(subkey : str) -> str:
Expand Down Expand Up @@ -54,25 +59,21 @@ def systemString() -> str:
'''
Generates a verbose human-readable version string for the Windows host system
'''
return '{} Version {} (Build {}.{})'.format(
return '{} (Build {}.{})'.format(
WindowsUtils._getVersionRegKey('ProductName'),
WindowsUtils.getWindowsRelease(),
WindowsUtils.getWindowsBuild(),
WindowsUtils._getVersionRegKey('UBR')
)

@staticmethod
def getWindowsRelease() -> str:
def getHostBaseTag() -> Optional[str]:
'''
Determines the Windows 10 / Windows Server release (1607, 1709, 1803, etc.) of the Windows host system
Retrieves the tag for the Windows Server Core base image matching the host Windows system
'''
try:
# Starting with Windows 20H2 (also known as 2009), Microsoft stopped updating ReleaseId
# and instead updates DisplayVersion
return WindowsUtils._getVersionRegKey('DisplayVersion')
except FileNotFoundError:
# Fallback to ReleaseId for pre-20H2 releases that didn't have DisplayVersion
return WindowsUtils._getVersionRegKey('ReleaseId')

hostBuild = WindowsUtils.getWindowsBuild()

return WindowsUtils._knownTagsByBuildNumber.get(hostBuild)

@staticmethod
def getWindowsBuild() -> int:
Expand All @@ -82,23 +83,14 @@ def getWindowsBuild() -> int:
return sys.getwindowsversion().build

@staticmethod
def isBlacklistedWindowsVersion(release=None):
def isBlacklistedWindowsHost() -> bool:
'''
Determines if the specified Windows release is one with bugs that make it unsuitable for use
Determines if host Windows version is one with bugs that make it unsuitable for use
(defaults to checking the host OS release if one is not specified)
'''
dockerVersion = parse_version(DockerUtils.version()['Version'])
release = WindowsUtils.getWindowsRelease() if release is None else release
return release in WindowsUtils._blacklistedReleases and dockerVersion < parse_version('19.03.6')

@staticmethod
def isEndOfLifeWindowsVersion(release=None):
'''
Determines if the specified Windows release is one that has reached End Of Life (EOL)
(defaults to checking the host OS release if one is not specified)
'''
release = WindowsUtils.getWindowsRelease() if release is None else release
return release in WindowsUtils._eolReleases
build = WindowsUtils.getWindowsBuild()
return build in WindowsUtils._blacklistedHosts and dockerVersion < parse_version('19.03.6')

@staticmethod
def isWindowsServer() -> bool:
Expand All @@ -108,28 +100,6 @@ def isWindowsServer() -> bool:
# TODO: Replace this with something more reliable
return 'Windows Server' in WindowsUtils._getVersionRegKey('ProductName')

@staticmethod
def isInsiderPreview() -> bool:
'''
Determines if the Windows host system is a Windows Insider preview build
'''
return WindowsUtils.getWindowsBuild() > WindowsUtils._latestReleaseBuild

@staticmethod
def getReleaseBaseTag(release: str) -> str:
'''
Retrieves the tag for the Windows Server Core base image matching the specified Windows 10 / Windows Server release
'''

# For Windows Insider preview builds, build the latest release tag
if WindowsUtils.isInsiderPreview():
return WindowsUtils._validTags[-1]

# This lookup table is based on the list of valid tags from <https://hub.docker.com/r/microsoft/windowsservercore/>
return {
'1809': 'ltsc2019',
}.get(release, release)

@staticmethod
def getDllSrcImage(basetag: str) -> str:
'''
Expand All @@ -142,22 +112,18 @@ def getDllSrcImage(basetag: str) -> str:
return f'mcr.microsoft.com/windows:{tag}'

@staticmethod
def getValidBaseTags() -> [str]:
'''
Returns the list of valid tags for the Windows Server Core base image, in ascending chronological release order
'''
return WindowsUtils._validTags

@staticmethod
def isValidBaseTag(tag: str) -> bool:
def getKnownBaseTags() -> [str]:
'''
Determines if the specified tag is a valid Windows Server Core base image tag
Returns the list of known tags for the Windows Server Core base image, in ascending chronological release order
'''
return tag in WindowsUtils._validTags
return WindowsUtils._knownTags

@staticmethod
def isNewerBaseTag(older: str, newer: str) -> bool:
def isNewerBaseTag(older: str, newer: str) -> Optional[bool]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking nit-pick: I'm reasonably unfond of Optional[bool], but don't have a better option, except letting the ValueError bubble up, or throwing a custom exception, and catching that in the caller.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno, these both look equally ugly to me :D

I'd actually make a enum { Newer, Older, Unknown } but don't think it will improve anything at all given that we're in Python that doesn't even have switch/case. Okay, it got it in 3.10, but that's too much new yet to use.

'''
Determines if the base tag `newer` is chronologically newer than the base tag `older`
'''
return WindowsUtils._validTags.index(newer) > WindowsUtils._validTags.index(older)
try:
return WindowsUtils._knownTags.index(newer) > WindowsUtils._knownTags.index(older)
except ValueError:
return None