Last active
October 29, 2024 13:57
-
-
Save rleap-m/ca19e2e6058f360cd276d4c64d699579 to your computer and use it in GitHub Desktop.
MCR Installer for Windows with customized genUrlFromVersionAndChannel function (resolves to latest build) and workaround for ENGINE-1050 (WinRM)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env pwsh | |
# | |
# This script is meant for quick & easy install on Windows via an elevated command prompt: | |
# | |
# PS>Invoke-WebRequest -Uri get.mirantis.com/install.ps1 | |
# PS>Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process | |
# PS>.\install.ps1 | |
# | |
# To obtain explicit versions of the script, append version number after replacing | |
# '.' and '-' characters in version with '_': | |
# | |
# PS>Invoke-WebRequest -Uri https://get.mirantis.com/install_1_0_5.ps1 -o install.ps1 | |
# | |
# For more usage information run: | |
# | |
# PS>Get-Help .\install.ps1 | |
# | |
# Copyright Mirantis Inc | |
# | |
# Script build information: | |
# COMMIT_SHA=${SCRIPT_COMMIT_SHA} | |
# COMMIT_SHA_PVAL=${SCRIPT_COMMIT_SHA_PVAL} | |
# SEMVER_VERSION=${SCRIPT_SEMVER_VERSION} | |
# PUBLISH_STRING=${SCRIPT_PUBLISH_STRING} | |
# | |
<# | |
.SYNOPSIS | |
Installs required binaries for UCP install | |
.DESCRIPTION | |
The install.ps1 script installs DockerEE and containerd | |
binaries and services to the local machine. | |
The script has inbuilt-defaults for everything and can be run | |
without specifying any values. Script parameters and env | |
variables can be used to overrule the default values. | |
Parameter values take precedence over env variables. | |
Both take precedence over inbuilt default values. | |
The script needs to be executed from an elevated command prompt. | |
Should you want to change the default daemon configs, you may | |
want to have the alternative configurations and the related | |
collateral in-place before executing the script. For example | |
if you would like to enable TLS with docker, please make sure | |
that the certificates are stored appropriately and the daemon | |
configuration file is written before invoking the script. | |
.PARAMETER DownloadUrl | |
[Alternately specified by $Env:DOWNLOAD_URL] | |
Specify an alternative repository to download container runtime | |
packages. Please consult the Mirantis product installation | |
documentation for air-gapped installs to learn more about setting | |
up a repository mirror. | |
.PARAMETER Channel | |
[Alternately specified by $Env:CHANNEL] | |
Specifies the channel to be used for picking the binaries. | |
Examples of channels are: stable, test etc. Stable is used as | |
the default channel. | |
.PARAMETER DockerVersion | |
[Alternately specified by $Env:DOCKER_VERSION] | |
Specifies the version number for the DockerEE binaries to install. | |
Latest is used as the default version. | |
.PARAMETER ContainerdVersion | |
[Alternately specified by $Env:CONTAINERD_VERSION] | |
Specifies the version number for the containerd binaries to install | |
Latest is used as the default version. | |
.PARAMETER DryRun | |
If specified, list different steps that would be used | |
without actually invoking them. | |
.PARAMETER Uninstall | |
If specified, uninstalls all packages. This entails | |
unregistering the corresponding services and removing paths | |
for the package from the registry. | |
All other script parameters (except DryRun and DestPath) are | |
ignored if this switch is specified. Common parameters such | |
as -Verbose are still honored. | |
.PARAMETER Ver | |
Print version info for the script and exit | |
.PARAMETER NoServiceStarts | |
If specified, services are not started on successful install. | |
By default, all services installed by the script are | |
left in a running state before exit. | |
.PARAMETER DestPath | |
Path to the directory under which binaries will be installed. | |
By default, this path is %PROGRAMFILES% | |
.PARAMETER OfflinePackagesPath | |
The folder for airgap/offline scenarios. For use when the | |
offline or DownloadOnly parameters are specified. Used to | |
either save the downloaded packages for later offline use | |
or for pointing to previously downloaded packages for | |
offline install. | |
.PARAMETER Offline | |
Install packages in offline/airgap mode. By default the | |
current directory will be used to look for previously | |
downloaded packages. That can be overridden by using | |
the OfflinePackagesPath parameter. | |
.PARAMETER DownloadOnly | |
Download and save packages for later offline/airgap install | |
.PARAMETER EngineOnly | |
Skip all steps except those related to Docker EE engine | |
.PARAMETER ShowVersions | |
Display a list of all available versions for each package | |
with the specified Download URL and Channel. Can be used to | |
determine the version number to use when you want to specify | |
an explicit version number at install time. | |
.INPUTS | |
None. You cannot pipe objects to install.ps1. | |
.OUTPUTS | |
None. install.ps1 does not generate any output. | |
.EXAMPLE | |
PS> .\install.ps1 | |
.EXAMPLE | |
PS> .\install.ps1 -Verbose | |
.NOTES | |
1. In scenarios where you have existing installed software that has its own | |
copies of OpenSSL libraries, you may run into the following error: | |
OpenSSL error: error:0F06D065:common libcrypto routines:FIPS_mode_set:fips mode not supported | |
This is often hit if you have ming/mingw64 as a part of your PATH env | |
variable. To work around this, ensure that the offending software is | |
not on the PATH and run the script again. | |
2. The script supports airgap functionality by providing access to | |
download packages while online as well as to install those selfsame | |
packages while offline. | |
For downloads, please ensure that the script has access to the internet. | |
Use the -DownloadOnly parameter. By default the script will use the | |
current directory to store the packages after download. This can be | |
changed by specifying the path explicitly with the -OfflinePackagesPath | |
parameter. | |
For offline/airgap install, please use the -Offline parameter. By default | |
the script will look for pacakage in the current directory. This can be | |
changed by specifying the -OfflinePackagesPath parameter. | |
While downloading using -DownloadOnly parameter, make sure that the | |
download path is accessible to the script, especially if you run the | |
script without administrative rights. | |
#> | |
# The following is required so that the script can be invoked with named | |
# parameters (e.g. -ContainerdVersion 1.3.4...). If a parameter is used; | |
# its type is checked by powershell - we give a higher precedence to the | |
# parameters specified this way vs. the same value specified by env vars. | |
# Parameters gotten at invocation time. Some of these values are "merged" | |
# with values specified by env vars - see reconcileParams. Others are | |
# used as-is. | |
[CmdletBinding(PositionalBinding=$FALSE)] | |
param ( | |
[string]$DownloadUrl, # Pointer to CDN for the package zip files. | |
[string]$Channel, # | |
[string]$DockerVersion, # | |
[string]$ContainerdVersion, # | |
[switch]$DryRun, # Gives an overview of what would happen on invoking the script. Is | |
# idempotent - can be run repeatedly without impacting system state. | |
[switch]$Uninstall, # Uninstall all installed packages | |
[switch]$Ver, # Print version Info and exit | |
[switch]$NoServiceStarts, # Do not start the services at the end. Useful if certificates need to be | |
# used or config needs to be built before starting one or more daemons. | |
# For example, script invoker may want to build docker config file before | |
# starting the dockerd service. | |
[string]$DestPath, # A folder to which all installs will be done. Currently there is no way | |
# to specify the name of the leaf folder and they are hardcoded. | |
[string]$OfflinePackagesPath, # A folder for airgap/offline scenarios. If specified, will be used to save | |
# and retrieve packages for this scenario. | |
[switch]$Offline, # Install packages in offline/airgap mode | |
[switch]$DownloadOnly, # Download and save packages for later offline/airgap install | |
[switch]$EngineOnly, # Skip everything except DockerEE | |
[switch]$ShowVersions # Display a list of available versions on specified [or default] channel | |
) | |
$ErrorActionPreference="Stop" | |
if (-not $DryRun) { | |
$global:ProgressPreference = 'SilentlyContinue' | |
} | |
# Updated manually | |
New-Variable -Name 'SCRIPT_SEMVER_VERSION' -Value '${SCRIPT_SEMVER_VERSION}' -Option Constant | |
# Leave the value blank except when intending to make an external release | |
# in which case set the value to a small phrase about the release e.g. | |
# 'Beta Refresh for UCP 3.3.0' | |
New-Variable -Name 'PUBLISH_STRING' -Value '${SCRIPT_PUBLISH_STRING}' -Option Constant | |
# Updated automatically by the CI pipeline | |
New-Variable -Name 'SCRIPT_COMMIT_SHA' -Value '${SCRIPT_COMMIT_SHA_PVAL}' -Option Constant | |
# Internal values[constants/variables] | |
$script:intgenUrl='' # The effective Url for the package - see function genUrlFromVersionAndChannel | |
$script:intDownloadUrl='' | |
$script:intChannel='' | |
$script:intDockerVersion='' | |
$script:intContainerdVersion='' | |
$script:intDestPath='' | |
$script:intOfflinePackagesPath='' | |
New-Variable -Name 'DOCKER_PKG_NAME' -Value 'docker' -Option Constant | |
New-Variable -Name 'CONTAINERD_PKG_NAME' -Value 'containerd' -Option Constant | |
New-Variable -Name 'DOCKER_SVC_NAME' -Value 'docker' -Option Constant | |
New-Variable -Name 'CONTAINERD_SVC_NAME' -Value 'containerd' -Option Constant | |
New-Variable -Name 'CONTAINERS_FEATURE_NOT_INSTALLED' -Value @" | |
Installing the containers feature. It is a prerequisite for containers on Windows and requires a reboot. | |
"@ -Option Constant | |
New-Variable -Name 'DEFAULT_DOWNLOAD_URL' -Value "https://repos.mirantis.com" -Option Constant | |
New-Variable -Name 'DEFAULT_CHANNEL' -Value 'stable' -Option Constant | |
New-Variable -Name 'DEFAULT_DOCKER_VERSION' -Value 'latest' -Option Constant | |
New-Variable -Name 'DEFAULT_CONTAINERD_VERSION' -Value 'latest' -Option Constant | |
New-Variable -Name 'DEFAULT_DEST_PATH' -Value "$env:ProgramFiles" -Option Constant | |
New-Variable -Name 'dockerExists' -Scope 'Script' -Value $FALSE | |
New-Variable -Name 'containerdExists' -Scope 'Script' -Value $FALSE | |
New-Variable -Name 'mustLogoff' -Scope 'Script' -Value $FALSE | |
New-Variable -Name 'initDockerVer' -Scope 'Script' -Value '' | |
New-Variable -Name 'initContainerdVer' -Scope 'Script' -Value '' | |
New-Variable -Name 'finalDockerVer' -Scope 'Script' -Value '' | |
New-Variable -Name 'finalContainerdVer' -Scope 'Script' -Value '' | |
New-Variable -Name 'EXIT_REBOOT_MESSAGE' -Value "Your machine needs to be rebooted now. Installed packages will not work without reboot." -Option Constant | |
New-Variable -Name 'EXIT_LOGOFF_MESSAGE' -Value "The system-wide PATH has been updated. To use docker.exe and other CLI tools, please logoff and logon to update your PATH." -Option Constant | |
# PortNumber, Type, In/Out/Inout | |
# Name of the generated rule will be "docker_[PortNumber]" | |
$allPorts = (2376, "tcp", "in"), ` | |
(2377, "tcp", "in"), ` | |
(4789, "udp", "inout"), ` | |
(6443, "tcp", "inout"), ` # kube | |
(7946, "tcp", "inout"), ` | |
(7946, "udp", "inout"), ` | |
(10250, "tcp", "in"), ` # kubelet HTTPS port | |
(12376, "udp", "inout") | |
# To ensure that we can overwrite the package binaries | |
# successfully, we need to stop the corresponding | |
# service. Sometimes this can impact to network and | |
# cause failure in download. To guard against it, we | |
# download the packages first and saved their downloaded | |
# location for user later on. | |
$downloadedPackageLocation = @{} | |
function Default { | |
param( [string]$cmdlineValue, [string]$envvarValue, [string]$defaultValue ) | |
$value="" | |
if (![string]::IsNullOrWhiteSpace($cmdlineValue)) { | |
$value=$cmdlineValue.Trim() | |
return $value | |
} | |
if (![string]::IsNullOrWhiteSpace($envvarValue)) { | |
$value=$envvarValue.Trim() | |
return $value | |
} | |
# Use the default value no matter what it is | |
$value="$defaultValue" | |
return $value | |
} | |
function updateServicesInstallStatus { | |
blank | |
verboseLog "Checking state of existing services" | |
if (Get-Service $DOCKER_SVC_NAME -ErrorAction SilentlyContinue) { | |
verboseLog "Service $DOCKER_SVC_NAME exists" | |
$script:dockerExists=$TRUE | |
} else { | |
verboseLog "Service $DOCKER_SVC_NAME does not exist" | |
} | |
if ($EngineOnly) { | |
return | |
} | |
if (Get-Service $CONTAINERD_SVC_NAME -ErrorAction SilentlyContinue) { | |
verboseLog "Service $CONTAINERD_SVC_NAME exists" | |
$script:containerdExists=$TRUE | |
} else { | |
verboseLog "Service $CONTAINERD_SVC_NAME does not exist" | |
} | |
} | |
function getServicesInstallStatus { | |
updateServicesInstallStatus | |
} | |
function versionAfterDeprecation { | |
param($verToCheck) | |
$versionRemoved = [System.Version]::new(20, 10, 15) | |
# first, trim off any extra version identifiers like -tp1, -rc2, whatever. | |
$verToCheck = $verToCheck.split("-")[0] | |
# after 25.0, Mirantis versions of the engine use an m suffix, like | |
# 25.0.7m1, or similar. This function only needs to know if the version is | |
# greater than the removed version, so we can ignore the Mirantis suffix. | |
# split it off, as above. | |
$verToCheck = $verToCheck.split("m")[0] | |
# doing both splits as above should trim the version down into something | |
# that can be cast to System.Version. | |
try { | |
return ([System.Version]$verToCheck -ge $versionRemoved) | |
} catch { | |
return $true | |
} | |
} | |
function deleteDeprecatedPlugins { | |
$deprecatedPlugins = "docker-app.exe", "docker-cluster.exe", "docker-registry.exe" | |
$isCurrentVerGreater = $false | |
if ($script:intDockerVersion -notcontains "latest") { | |
$isCurrentVerGreater = versionAfterDeprecation $script:intDockerVersion | |
} | |
if ($script:intDockerVersion.Contains("latest") -or $script:intDockerVersion.Contains("nightly") -or $isCurrentVerGreater) { | |
Foreach ($plugin in $deprecatedPlugins) { | |
Remove-Item "$env:ProgramFiles\Docker\cli-plugins\$plugin" -Force -ErrorAction SilentlyContinue | |
} | |
} | |
} | |
function ensureExistingServicesStarted { | |
blank | |
if (-not $EngineOnly) { | |
if ($script:containerdExists) { | |
if (-not $DryRun) { | |
Start-Service -Name $CONTAINERD_SVC_NAME | |
} | |
verboseLog "Started service $CONTAINERD_SVC_NAME" | |
} | |
} | |
if ($script:dockerExists) { | |
if (-not $DryRun) { | |
deleteDeprecatedPlugins | |
Start-Service -Name $DOCKER_SVC_NAME | |
} | |
verboseLog "Started service $DOCKER_SVC_NAME" | |
} | |
verboseLog "Ensured all services are started" | |
} | |
function ensureExistingServicesStopped { | |
blank | |
if ($script:dockerExists) { | |
if (-not $DryRun) { | |
Stop-Service -Name $DOCKER_SVC_NAME -Force | |
} | |
verboseLog "Stopped service $DOCKER_SVC_NAME" | |
} | |
if ($EngineOnly) { | |
verboseLog "Ensured any installed services are in a stopped state" | |
return | |
} | |
if ($script:containerdExists) { | |
if (-not $DryRun) { | |
Stop-Service -Name $CONTAINERD_SVC_NAME -Force | |
} | |
verboseLog "Stopped service $CONTAINERD_SVC_NAME" | |
} | |
verboseLog "Ensured any installed services are in a stopped state" | |
} | |
function blank { | |
Write-Verbose "" | |
} | |
function verboseLog { | |
Write-Verbose "$args" | |
} | |
function infoLog { | |
Write-Information "$args" | |
} | |
function errorLog { | |
Write-Error "$args" | |
} | |
function VerboseWithCheck { | |
param($pfx, $sfx) | |
if (![string]::IsNullOrWhiteSpace($sfx)) { | |
verboseLog "$pfx`:$sfx" | |
} else { | |
verboseLog "$pfx`:[unspecified]" | |
} | |
} | |
function downloadPackageToInstall { | |
param($pkgname, $channel, $version) | |
verboseLog "Downloading binaries for $pkgName" | |
blank | |
$downloadedPackageLocation["$pkgname"] = "" | |
genUrlFromVersionAndChannel "$pkgname" "$channel" "$version" | |
$downloadUrl = $script:genUrl | |
# Trim trailing / | |
$downloadUrl = $downloadUrl.TrimEnd('/') | |
if ($Offline) { | |
# Offline install - files already available. | |
# Verify and exit if offline files missing. | |
$tempZipFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "$pkgname.zip" | |
if (-not (Test-Path $tempZipFilePath -PathType leaf)) { | |
errorLog "Offline install: zip file $pkgname.zip not found at expected path $tempZipFilePath" | |
exit | |
} | |
} else { | |
if ($DownloadOnly) { | |
$tempZipFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "$pkgname.zip" | |
$tempDockerZapFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "docker-ci-zap.exe" | |
if (-not (Test-Path $tempDockerZapFilePath -PathType leaf)) { | |
Invoke-WebRequest -Uri "https://github.com/moby/docker-ci-zap/raw/master/docker-ci-zap.exe" -OutFile $tempDockerZapFilePath | |
} | |
} else { | |
[string] $tempname = [System.Guid]::NewGuid() | |
$tempZipFilePath = Join-Path -Path "$script:intDestPath" -ChildPath "$tempname.zip" | |
} | |
"Downloading $pkgname zip into $tempZipFilePath from: $downloadUrl - this may take some time" | |
Invoke-WebRequest "$downloadUrl" -UseBasicParsing -OutFile "$tempZipFilePath" | |
"Download of package $pkgname finished" | |
} | |
verboseLog "Downloaded binaries for $pkgName to $tempZipFilePath" | |
$downloadedPackageLocation["$pkgname"] = "$tempZipFilePath" | |
} | |
function downloadAllPackagesForInstall { | |
downloadPackageToInstall "Docker" "$script:intChannel" "$script:intDockerVersion" | |
if ($EngineOnly) { | |
return | |
} | |
downloadPackageToInstall "Containerd" "$script:intChannel" "$script:intContainerdVersion" | |
} | |
<# | |
Replacing with custom version for Software Testing Team's purposes - @rleap | |
function genUrlFromVersionAndChannel { | |
# All parameters below will always have a value due to defaults | |
param($pkgname, $channel, $version) | |
$script:genUrl = "$script:intDownloadUrl`/win`/static`/$channel`/x86_64`/$pkgname`-$version`.zip" | |
# AWS is case sensitive and we use all lowers - ensure that. | |
$script:genUrl = $script:genUrl.ToLower() | |
verboseLog "genUrl: $pkgname $channel $version $script:genUrl" | |
} | |
#> | |
# Replaces ^^ - @rleap | |
function genUrlFromVersionAndChannel { | |
# All parameters below will always have a value due to defaults | |
param($pkgname, $channel, $version) | |
if (($pkgname -eq 'Docker') -and (!($version -match '-'))) { | |
Write-Verbose "Resolving latest build based on [$pkgname] version [$version]..." | |
$url = "$script:intDownloadUrl`/win`/static`/$script:intChannel`/x86_64`/" | |
$resp = Invoke-WebRequest -Uri $url -UseBasicParsing | |
if ($resp.StatusCode -eq 200) { | |
# RegEx and custom object code stolen from 'parsePackageVersions' function | |
# @srl - I wasn't getting a hit on this - because it is a case-sensitive search. Added in the /I to correct | |
# $tmpPkg = $resp.Links.href | findstr $pkgname-[0-9] | |
$tmpPkg = $resp.Links.href | findstr /I $pkgname-[0-9] | |
$tmpPkgs = $tmpPkg.split(' ') | |
$packageRegex = "^$($pkgname.ToLower())-\s*(?<fullVersion>(?<major>\d\d?)\.(?<minor>\d\d?)\.(?<patch>\d\d?)(m(?<mver>\d))?((-|~+)(?<pre>tp|rc|beta|nightly|dev)\.?(?<prever>\d)?)?(\+fips)?)\.zip$" | |
$releases = $tmpPkgs | Select-String -CaseSensitive -Pattern $packageRegex | ForEach-Object { | |
[PSCustomObject]@{ | |
name = $_.Matches.Value | |
fullVersion = $_.Matches[0].Groups['fullVersion'].Value | |
major = [int]$_.Matches[0].Groups['major'].Value | |
minor = [int]$_.Matches[0].Groups['minor'].Value | |
patch = [int]$_.Matches[0].Groups['patch'].Value | |
mver = [int]$_.Matches[0].Groups['mver'].Value | |
pre = $_.Matches[0].Groups['pre'].Value | |
prever = [int]$_.Matches[0].Groups['prever'].Value | |
} | |
} | |
$releases = $releases | Sort-Object -Property major,minor,patch,mver,@{Expression={ | |
switch ($_.pre) | |
{ | |
'' {0} | |
'rc' {1} | |
'tp' {2} | |
'beta' {3} | |
'nightly' {4} | |
'dev' {5} | |
} | |
}; Descending=$true },prever | |
$semverDockerVer = [System.Version] $version | |
$latestBuild = $releases | Where-Object -FilterScript { | |
($_.major -eq $semVerDockerVer.Major) -and ($_.minor -eq $semVerDockerVer.Minor) -and ($_.patch -eq $semVerDockerVer.Build) | |
} | Select-Object -Last 1 | |
if ($latestBuild) { | |
Write-Verbose "Resolving latest build to [$($latestBuild.name)]." | |
$version = ($latestBuild.name.substring(0,$latestBuild.name.LastIndexOf('.'))).Substring($pkgname.Length+1) | |
} | |
else { | |
Write-Warning "Unable to resolve a [$pkgname] build corresponding to version [$version]" | |
} | |
} | |
else { | |
Write-Warning "Unexpected response code [$($resp.StatusCode)] when querying [$url]" | |
} | |
} | |
$script:genUrl = ("$script:intDownloadUrl`/win`/static`/$channel`/x86_64`/$pkgname`-$version`.zip").ToLower() | |
verboseLog "genUrl: $pkgname $channel $version $script:genUrl" | |
} | |
function openPortWorker { | |
param ($cmd) | |
Invoke-Expression $cmd|Out-Null | |
if ($LASTEXITCODE -ne 0) { | |
Write-Warning "$cmd failed with $LASTEXITCODE. Please try to open firewall port manually" | |
} else { | |
verboseLog "Opened port successfully" | |
} | |
} | |
# Failures opening ports are non-fatal but must be detected and logged | |
function openPorts { | |
foreach ($curPort in $allPorts) | |
{ | |
$curPortNumber = $curPort[0] | |
$curPortProtocol = $curPort[1] | |
$curPortDirection = $curPort[2] | |
if (-not $DryRun) { | |
if ($curPortDirection -eq "in" -or $curPortDirection -eq "inout") { | |
verboseLog "Opening IN port $curPortNumber[$curPortDirection] for $curPortProtocol" | |
$cmd = "netsh advfirewall firewall add rule name=`"docker`_$curPortNumber`_in`" dir=in action=allow protocol=$curPortProtocol localport=$curPortNumber" | |
openPortWorker "$cmd" | |
} | |
if ($curPortDirection -eq "out" -or $curPortDirection -eq "inout") { | |
verboseLog "Opening OUT port $curPortNumber[$curPortDirection] for $curPortProtocol" | |
$cmd = "netsh advfirewall firewall add rule name=`"docker`_$curPortNumber`_out`" dir=out action=allow protocol=$curPortProtocol localport=$curPortNumber" | |
openPortWorker "$cmd" | |
} | |
} else { | |
verboseLog "Opening port $curPortNumber[$curPortDirection] for $curPortProtocol" | |
} | |
} | |
} | |
function updatedDryRunPkgVer { | |
param ($pkgname, $tempZipFilePath) | |
$pkgname = $pkgname.ToLower() | |
$parent = [System.IO.Path]::GetTempPath() | |
[string] $name = [System.Guid]::NewGuid() | |
$tempDir = Join-Path $parent $name | |
New-Item -ItemType Directory -Path $tempDir|Out-Null | |
Expand-Archive -Path "$tempZipFilePath" -DestinationPath "$tempDir" -Force | |
# The following is intentionally fatal - we do want | |
# to detect any packaging issues etc. immediately. | |
if ($pkgname -eq "docker") { | |
$tmp = & "$tempDir`\Docker`\docker.exe" -v|Out-String | |
if ($LASTEXITCODE -ne 0) { | |
errorLog "$cmd returned $LASTEXITCODE" | |
exit | |
} | |
$tmp = $tmp.Trim().split(' ',3) | |
$script:finalDockerVer = $tmp[2] | |
} elseif ($pkgname -eq "containerd") { | |
$tmp = & "$tempDir`\Containerd`\ctr.exe" -v|Out-String | |
if ($LASTEXITCODE -ne 0) { | |
errorLog "$cmd returned $LASTEXITCODE" | |
exit | |
} | |
$tmp = $tmp.Trim().split(' ',3) | |
$script:finalContainerdVer = $tmp[2].Substring(1) | |
} | |
Remove-Item -Recurse -Force "$tempDir" | |
} | |
function workerFunc { | |
param($pkgname) | |
$tempZipFilePath = $downloadedPackageLocation["$pkgname"] | |
"Using preloaded zip File $tempZipFilePath for installing package $pkgname" | |
if (-not $DryRun) { | |
verboseLog "Expanding archive $tempZipFilePath into $script:intDestPath" | |
Expand-Archive -Path "$tempZipFilePath" -DestinationPath "$script:intDestPath" -Force | |
} else { | |
updatedDryRunPkgVer $pkgname $tempZipFilePath | |
} | |
if (-not $Offline) { | |
verboseLog "Removing temporary Zip File $tempZipFilePath" | |
Remove-Item -Force "$tempZipFilePath" | |
} | |
} | |
function ensurePathNotExist { | |
param ($pathtoremove) | |
$pathtoremove = $pathtoremove.ToLower().Trim().Trim('"').TrimStart('"') | |
verboseLog "Ensure $pathtoremove is removed from PATH, if present" | |
$newPath = "" | |
$notChanged = $TRUE | |
$oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path | |
ForEach ($curPathOrig in $oldpath.split(";")) { | |
$curPath = $curPathOrig.ToLower().Trim().Trim('"').TrimStart('"') | |
if ($curPath -ne $pathtoremove) { | |
# Add it to new path | |
$newPath += $curPathOrig + ';' | |
} else { | |
# Do not add - set a flag. | |
$notChanged = $FALSE | |
} | |
} | |
if (-not $notChanged) { | |
verboseLog "Removing $pathtoremove from PATH" | |
if (-not $DryRun) { | |
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newpath | |
} | |
} else { | |
verboseLog "$pathtoremove not found in PATH" | |
} | |
} | |
function checkPathExists { | |
param ($oldpath, $pathToCheck) | |
$oldpath = $oldpath.ToLower().Trim() | |
$pathToCheck = $pathToCheck.ToLower().Trim() | |
# Present at the very end without any separator | |
if ($oldpath.endsWith($pathToCheck)) { | |
return $TRUE | |
} | |
if ($oldpath.contains("$pathToCheck`;")) { | |
return $TRUE | |
} | |
if ($oldpath.contains("$pathToCheck`"")) { | |
return $TRUE | |
} | |
return $FALSE | |
} | |
# In case a service already existed, unregister and register it | |
# so that we always have a consistent state at the end. | |
# Verified (code and testing) that --unregister-service does not | |
# cause any existing windows events to be lost. | |
function processPackage { | |
param($pkgname, $pkgSvcBinary, $pkgSvcExists) | |
blank | |
verboseLog "Processing package $pkgName" | |
blank | |
verboseLog "Installing binaries for $pkgName" | |
workerFunc $pkgname | |
verboseLog "$pkgname package installed" | |
blank | |
blank | |
$svcname = $pkgname.ToLower() | |
verboseLog "Installing $svcname service" | |
$pkgDirPath = Join-Path "$intDestPath" -ChildPath "$pkgname" | |
$pkgBinPath = Join-Path "$pkgDirPath" -ChildPath "$pkgSvcBinary" | |
if ($pkgSvcExists) { | |
verboseLog "Unregistering the existing $svcname service" | |
if (-not $DryRun) { | |
& "$pkgBinPath" --unregister-service | |
} | |
} | |
verboseLog "Invoking $pkgBinPath to register $svcname service" | |
if (-not $DryRun) { | |
& "$pkgBinPath" --register-service | |
if ($LASTEXITCODE -ne 0) { | |
errorLog "Failed to register $svcname service - exit code $LASTEXITCODE" | |
exit | |
} | |
} | |
if (-not $DryRun) { | |
Set-Service docker -StartupType Automatic | |
} | |
verboseLog "Service $svcname registered and set to automatic" | |
# Make sure the binary location is in the path | |
$oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path | |
$pathExists = checkPathExists "$oldpath" "$pkgDirPath" | |
if (-not $pathExists) { | |
verboseLog "Adding $pkgDirPath to system PATH" | |
if (-not $DryRun) { | |
$newpath = "$oldpath`;$pkgDirPath" | |
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newpath | |
} | |
} else { | |
verboseLog "Skipping PATH modification: $pkgDirPath already exists in system PATH" | |
} | |
# Modify the current PATH also - so that calls to ctr and docker.exe work | |
# OK to do so for dryrun as well since this only impacts the current process. | |
$pathExists = checkPathExists "$Env:Path" "$pkgDirPath" | |
if (-not $pathExists) { | |
$Env:Path += "`;$pkgDirPath" | |
} | |
blank | |
} | |
# In case a service already existed, unregister and register it | |
# so that we always have a consistent state at the end. | |
# Verified (code and testing) that --unregister-service does not | |
# cause any existing windows events to be lost. | |
function uninstallPackage { | |
param($pkgname, $pkgSvcBinary, $pkgSvcExists) | |
blank | |
verboseLog "Uninstalling package $pkgName" | |
blank | |
blank | |
$svcname = $pkgname.ToLower() | |
verboseLog "Uninstalling $svcname service" | |
$pkgDirPath = Join-Path "$intDestPath" -ChildPath "$pkgname" | |
$pkgBinPath = Join-Path "$pkgDirPath" -ChildPath "$pkgSvcBinary" | |
if ($pkgSvcExists) { | |
verboseLog "Unregistering the existing $svcname service" | |
if (-not $DryRun) { | |
& "$pkgBinPath" --unregister-service | |
} | |
} | |
# We need to "zap" the docker folder instead of deleting it. | |
if($Offline) { | |
# For offline, we expect/recommend docker-ci-zap to be present in $script:intOfflinePackagesPath | |
$tempDockerZapFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "docker-ci-zap.exe" | |
if (-not (Test-Path $tempDockerZapFilePath -PathType leaf)) { | |
errorLog "Docker Zap util is unavailable at $tempDockerZapFilePath" | |
exit | |
} | |
& $tempDockerZapFilePath -folder $pkgDirPath | |
} | |
else { | |
Invoke-WebRequest -Uri "https://github.com/moby/docker-ci-zap/raw/master/docker-ci-zap.exe" -OutFile "$env:TEMP\docker-ci-zap.exe" | |
& $env:TEMP\docker-ci-zap.exe -folder $pkgDirPath | |
# Remove the util | |
Remove-Item $env:TEMP\docker-ci-zap.exe | |
} | |
# Make sure the binary location is removed from the path | |
ensurePathNotExist "$pkgDirPath" | |
} | |
function reconcileParams { | |
verboseLog "Reconciling parameters for the script" | |
# Binaries CDN location - a default value must be specified | |
$script:intDownloadUrl = Default ` | |
"$DownloadUrl" ` | |
"$env:DOWNLOAD_URL" ` | |
$DEFAULT_DOWNLOAD_URL | |
verboseLog "Using Docker Url: $script:intDownloadUrl" | |
# Channel name (e.g. test, stable etc.) | |
$script:intChannel = Default ` | |
"$Channel" ` | |
"$env:CHANNEL" ` | |
$DEFAULT_CHANNEL | |
VerboseWithCheck "Using Channel" "$script:intChannel" | |
# Docker and containerd binaries version | |
# For offline, we shouldn't care about latest. | |
if(-not $Offline) { | |
$packages = getPackageIndex | |
$DEFAULT_DOCKER_VERSION = getNumericallyHigherVersion $packages.x86_64.docker | |
$DEFAULT_CONTAINERD_VERSION = getNumericallyHigherVersion $packages.x86_64.containerd | |
} | |
$script:intDockerVersion = Default ` | |
"$DockerVersion" ` | |
"$env:DOCKER_VERSION" ` | |
$DEFAULT_DOCKER_VERSION | |
VerboseWithCheck "Using Docker Version" "$script:intDockerVersion" | |
$script:intContainerdVersion = Default ` | |
"$ContainerdVersion" ` | |
"$env:CONTAINERD_VERSION" ` | |
$DEFAULT_CONTAINERD_VERSION | |
VerboseWithCheck "Using Containerd Version" "$script:intContainerdVersion" | |
# Destination path for installing the binaries | |
$script:intDestPath = Default ` | |
"$DestPath" ` | |
"" ` | |
$DEFAULT_DEST_PATH | |
verboseLog "Using Destination Path: $script:intDestPath" | |
$defLocation = Get-Location | |
# Path for saving/loading for airgap installs | |
$script:intOfflinePackagesPath = Default ` | |
"$OfflinePackagesPath" ` | |
"" ` | |
$defLocation | |
verboseLog "Using Offline Packages Path: $script:intOfflinePackagesPath" | |
} | |
function parsePackageVersions { | |
# The names of these packages are like follows: | |
# | |
# * 1.7.20 | |
# * 1.7.22-rc.1 | |
# * 25.0.6-tp2 | |
# * 25.0.7m1-tp1 | |
# | |
# This means file names are composed of a few parts: | |
# | |
# 25 . 0 . 7 m1 - tp 1 +fips | |
# └┬─┘ └┬┘ └┬┘└┬─┘└┬┘└─┬┘└┬┘└┬┘└──┬──┘ | |
# │ │ │ │ │ │ │ │ └ fips designator, if this is a fips package (optional) | |
# │ │ │ │ │ │ │ └ prerelease version (optional) | |
# │ │ │ │ │ │ └ might be a dot here | |
# │ │ │ │ │ └ prerelease designator (optional) | |
# │ │ │ │ └ this might also be some number of ~ | |
# │ │ │ └ Mirantis release version (not present prior to 25.0) | |
# │ │ └ patch version | |
# │ └ "minor" version (but not really, because we're not semvar) | |
# └ "major" version (but as above) | |
# We need to separate out and classify all the versiony bits. | |
param($packages) | |
# We use a regular expression, because they're good for parsing this sort | |
# of thing. To hopefully make it more readable, we will use some named | |
# captures: | |
# * major | |
# * minor | |
# * patch | |
# * mver | |
# * pre | |
# * prever | |
$packageRegex = '^\s*(?<fullVersion>(?<major>\d\d?)\.(?<minor>\d\d?)\.(?<patch>\d\d?)(m(?<mver>\d))?((-|~+)(?<pre>tp|rc|beta|nightly|dev)\.?(?<prever>\d)?)?(\+fips)?)$' | |
return $packages | Select-String -CaseSensitive -Pattern $packageRegex | % { | |
[PSCustomObject]@{ | |
name = $_.Matches.Value | |
# fullVersion is the version of the package without the package | |
# name at the beginning or .zip at the end. It is what is | |
# ultimately passed into the download-y bits. | |
fullVersion = $_.Matches[0].Groups['fullVersion'].Value | |
major = [int]$_.Matches[0].Groups['major'].Value | |
minor = [int]$_.Matches[0].Groups['minor'].Value | |
patch = [int]$_.Matches[0].Groups['patch'].Value | |
mver = [int]$_.Matches[0].Groups['mver'].Value | |
pre = $_.Matches[0].Groups['pre'].Value | |
prever = [int]$_.Matches[0].Groups['prever'].Value | |
} | |
} | Sort-Object -Property major,minor,patch,mver,@{Expression={ | |
# custom sort of the prerelease designator field, because these | |
# are not in actually alphabetical order | |
switch ($_.pre) | |
{ | |
'' {0} | |
'rc' {1} | |
'tp' {2} | |
'beta' {3} | |
'nightly' {4} | |
'dev' {5} | |
} | |
}; Descending=$true }, prever | |
} | |
function getNumericallyHigherVersion { | |
param($packages) | |
return parsePackageVersions $packages | | |
Select-Object -Last 1 -ExpandProperty fullVersion | |
} | |
function getPackageIndex { | |
$indexUrl = "$script:intDownloadUrl`/win`/static`/$script:intChannel`/index.json" | |
return (Invoke-WebRequest -UseBasicParsing -Uri $indexUrl).Content | ConvertFrom-Json | |
} | |
# Because we only ever use the versions for informational messages, | |
# we treat errors here as non-fatal. | |
function getDockerVer { | |
if (Get-Command "docker.exe" -ErrorAction SilentlyContinue) | |
{ | |
$tmp = docker -v|Out-String | |
if ($LASTEXITCODE -eq 0) { | |
$tmp = $tmp.Trim().split(' ',3) | |
return $tmp[2] | |
} else { | |
verboseLog "docker -v returned $LASTEXITCODE" | |
} | |
} else { | |
verboseLog "docker not in PATH" | |
} | |
return '' | |
} | |
function getContainerdVer { | |
if (Get-Command "ctr.exe" -ErrorAction SilentlyContinue) | |
{ | |
$tmp = ctr -v|Out-String | |
if ($LASTEXITCODE -eq 0) { | |
$tmp = $tmp.Trim().split(' ',3) | |
return $tmp[2].Substring(1) | |
} else { | |
verboseLog "ctr version returned $LASTEXITCODE" | |
} | |
} else { | |
verboseLog "ctr not in PATH" | |
} | |
return '' | |
} | |
function initState { | |
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
reconcileParams | |
getServicesInstallStatus | |
if ($script:dockerExists) { | |
$script:initDockerVer = getDockerVer | |
} | |
if ($EngineOnly) { | |
return | |
} | |
if ($script:containerdExists) { | |
$script:initContainerdVer = getContainerdVer | |
} | |
} | |
function processAllPackagesForInstall { | |
processPackage "Docker" "dockerd.exe" $script:dockerExists | |
$script:dockerExists = $TRUE | |
if ($EngineOnly) { | |
return | |
} | |
processPackage "Containerd" "containerd.exe" $script:containerdExists | |
$script:containerdExists = $TRUE | |
} | |
function processAllPackagesForUninstall { | |
if ($script:dockerExists) { | |
uninstallPackage "Docker" "dockerd.exe" $script:dockerExists | |
if (![string]::IsNullOrWhiteSpace($script:initDockerVer)) { | |
"Uninstalled package Docker $script:initDockerVer" | |
} else { | |
"Uninstalled package Docker" | |
} | |
$script:dockerExists = $FALSE | |
} else { | |
"Uninstall - package Docker does not exist" | |
} | |
if ($EngineOnly) { | |
return | |
} | |
if ($script:containerdExists) { | |
uninstallPackage "Containerd" "containerd.exe" $script:containerdExists | |
if (![string]::IsNullOrWhiteSpace($script:initContainerdVer)) { | |
"Uninstalled package Containerd $script:initContainerdVer" | |
} else { | |
"Uninstalled package Containerd" | |
} | |
$script:containerdExists = $FALSE | |
} else { | |
"Uninstall - package Containerd does not exist" | |
} | |
} | |
function printScriptVer { | |
"install.ps1 version $SCRIPT_SEMVER_VERSION build $SCRIPT_COMMIT_SHA" | |
if (![string]::IsNullOrWhiteSpace($PUBLISH_STRING)) { | |
"For $PUBLISH_STRING" | |
} | |
} | |
function printPackageVersions { | |
reconcileParams | |
$json = getPackageIndex | |
forEach ($curPkg in ("docker", "containerd")) { | |
$omitlatest=@($json.x86_64.$curPkg) -notmatch 'latest' | |
[array]::Reverse($omitlatest) | |
Write-Host "$curPkg`:" ($omitlatest -join ", ") | |
} | |
} | |
function installOrUninstall { | |
if ($Ver) { | |
printScriptVer | |
exit | |
} | |
if ($ShowVersions) { | |
printPackageVersions | |
exit | |
} | |
$rebootReminder = $FALSE | |
if (-not $DryRun -and -not $DownloadOnly) { | |
if (-not ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))) { | |
# Possible TODO: Addressing the need to have a non-admin access docker client | |
# This requires: | |
# 1. Specifying a security group whose users can access docker client even if they are not admins | |
# 2. Creation of this security group and adding a user to it | |
# Both of the above need admits so we cannot do that automatically at this stage. | |
# We could possibly provide guidance to what an adinistrator user of this script needs to do. | |
errorLog "Installation of Docker EE requires administrator rights on Windows" | |
"By adding the group option to the Docker Daemon config file, it is possible to execute Docker commands as non-admin." | |
"Specifying the Security group for allowing non-admins access to Docker:https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/configure-docker-daemon#set-docker-security-group" | |
"To create SG and add user to it, execute as an admin: net localgroup <Group Name> /add && net localgroup <Group Name> <User Name> /add" | |
exit | |
} | |
} | |
if (-not $DownloadOnly) { | |
# The script should work as long as its prerequisites | |
# [Containers feature] is installed. | |
# Make sure containers feature is enabled on the host | |
if (-not $Uninstall) { | |
if ((get-windowsoptionalfeature -Online -FeatureName containers).State -ne 'Enabled') { | |
blank | |
blank | |
if (-not $DryRun) { | |
"$CONTAINERS_FEATURE_NOT_INSTALLED" | |
Invoke-Expression "Enable-WindowsOptionalFeature -Online -FeatureName containers -NoRestart -WarningAction SilentlyContinue -All"|Out-Null | |
$rebootReminder = $TRUE | |
} else { | |
"Containers feature is not installed and is required for Docker EE" | |
"Dry run is ON - proceeding as if the feature was installed without actually installing it" | |
} | |
blank | |
blank | |
} else { | |
verboseLog "Verified that the feature Containers is installed" | |
} | |
} | |
} | |
blank | |
initState | |
# Stopping services can impact network connectivity. | |
# This is pronounced when dockerd is stopped and we | |
# have reports of downloads failing as a result. So | |
# we download before stopping the service(s). | |
if (-not $Uninstall) { | |
downloadAllPackagesForInstall | |
if ($DownloadOnly) { | |
exit | |
} | |
if (-not $DryRun -and -not $EngineOnly) { | |
md -Force c:\k\cni\config | Out-Null | |
} | |
} | |
# Stop existing services so that we could overwrite the binaries | |
ensureExistingServicesStopped | |
if ($Uninstall) { | |
# Uninstall services | |
processAllPackagesForUninstall | |
} else { | |
# Install services | |
processAllPackagesForInstall | |
# For DryRun, the version numbers have already been updated | |
if (-not $DryRun) { | |
$script:finalDockerVer = getDockerVer | |
$script:finalContainerdVer = getContainerdVer | |
} | |
if (-not $EngineOnly) { | |
openPorts | |
} | |
# Unless we are asked to leave the services in a stopped state, we need to start them. | |
if (-not $rebootReminder -and -not $NoServiceStarts) { | |
ensureExistingServicesStarted | |
} | |
blank | |
# Before exiting, emit a message about different packages installed/upgraded | |
# and their versions number(s) [pre-install and post-install]. | |
if ($script:finalDockerVer -ne '') { | |
if ($script:initDockerVer -ne '' -and $script:initDockerVer -ne $script:finalDockerVer) { | |
"Updated Docker from $script:initDockerVer to $script:finalDockerVer" | |
} else { | |
"Installed Docker $script:finalDockerVer" | |
} | |
} | |
if (-not $EngineOnly) { | |
if ($script:finalContainerdVer -ne '') { | |
if ($script:initContainerdVer -ne '' -and $script:initContainerdVer -ne $script:finalContainerdVer) { | |
"Updated Containerd from $script:initContainerdVer to $script:finalContainerdVer" | |
} else { | |
"Installed Containerd $script:finalContainerdVer" | |
} | |
} | |
} | |
blank | |
"Install/upgrade completed" | |
# Reboot > Logoff. So show only reboot message even if logoff is also set. | |
if ($rebootReminder) { | |
Write-Warning $EXIT_REBOOT_MESSAGE | |
} elseif ($script:mustLogoff) { | |
Write-Warning $EXIT_LOGOFF_MESSAGE | |
} | |
} | |
} | |
# @rleap - see issue ENGINE-1050 | |
verboseLog "This is a customized installer built for the software testing team." | |
# if (-Not ($MyInvocation.InvocationName -eq '.' -or $MyInvocation.Line -eq '')) { | |
installOrUninstall | |
# } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment