Private/CommonToolUtilities.psm1
########################################################################### # # # Copyright (c) Microsoft Corporation. All rights reserved. # # # # This code is licensed under the MIT License (MIT). # # # ########################################################################### $ModuleParentPath = Split-Path -Parent $PSScriptRoot Import-Module -Name "$ModuleParentPath\Private\UpdateEnvironmentPath.psm1" -Force class ContainerTool { [ValidateNotNullOrEmpty()][string]$Feature [ValidateNotNullOrEmpty()][string]$Version [ValidateNotNullOrEmpty()][string]$Uri [string]$InstallPath [string]$DownloadPath [string]$EnvPath } class FileDigest { [string]$HashFunction [string]$Digest FileDigest([string]$hashFunction, [string]$digest) { $this.HashFunction = $hashFunction $this.Digest = $digest } } class FileDownloadParameters { [ValidateSet("Containerd", "Buildkit", "nerdctl", "WinCNIPlugin")] [string]$Feature [string]$Repo [string]$Version = "latest" [ValidateSet("386", "amd64", "arm64", "arm")] [string]$OSArchitecture = "$env:PROCESSOR_ARCHITECTURE" [string]$DownloadPath = "$HOME\Downloads" [string]$ChecksumSchemaFile [string]$FileFilterRegEx FileDownloadParameters( [string]$feature, [string]$repo, [string]$version = "latest", [string]$arch = "$env:PROCESSOR_ARCHITECTURE", [string]$downloadPath = "$HOME\Downloads", [string]$checksumSchemaFile = $null, [string]$fileFilterRegEx = $null ) { $this.Feature = $feature $this.Repo = $repo $this.Version = $version $this.OSArchitecture = $arch $this.DownloadPath = $downloadPath $this.ChecksumSchemaFile = $checksumSchemaFile $this.FileFilterRegEx = $fileFilterRegEx } } Add-Type @' public enum ActionConsent { Yes = 0, No = 1 } '@ $HASH_FUNCTIONS = @("SHA1", "SHA256", "SHA384", "SHA512", "MD5") $HASH_FUNCTIONS_STR = $HASH_FUNCTIONS -join '|' # SHA1|SHA256|SHA384|SHA512|MD5 $NERDCTL_CHECKSUM_FILE_PATTERN = "(?<hashfunction>(?:^({0})))" -f ($HASH_FUNCTIONS -join '|') $NERDCTL_FILTER_SCRIPTBLOCK_STR = { (("{0}" -match "$NERDCTL_CHECKSUM_FILE_PATTERN") -and "{0}" -notmatch ".*.asc$") }.ToString() Set-Variable -Option AllScope -scope Global -Visibility Public -Name "CONTAINERD_REPO" -Value "containerd/containerd" -Force Set-Variable -Option AllScope -scope Global -Visibility Public -Name "BUILDKIT_REPO" -Value "moby/buildkit" -Force Set-Variable -Option AllScope -scope Global -Visibility Public -Name "NERDCTL_REPO" -Value "containerd/nerdctl" -Force Set-Variable -Option AllScope -scope Global -Visibility Public -Name "WINCNI_PLUGIN_REPO" -Value "microsoft/windows-container-networking" -Force Set-Variable -Option AllScope -scope Global -Visibility Public -Name "CLOUDNATIVE_CNI_REPO" -Value "containernetworking/plugins" -Force function Get-LatestToolVersion($tool) { # Get the repository based on the tool $repository = switch ($tool.ToLower()) { "containerd" { $CONTAINERD_REPO } "buildkit" { $BUILDKIT_REPO } "nerdctl" { $NERDCTL_REPO } "wincniplugin" { $WINCNI_PLUGIN_REPO } "cloudnativecni" { $CLOUDNATIVE_CNI_REPO } Default { Throw "Couldn't get latest $tool version. Invalid tool name: '$tool'." } } # Get the latest release version URL string $uri = "https://api.github.com/repos/$repository/releases/latest" Write-Debug "Getting the latest $tool version from $uri" # Get the latest release version try { $response = Invoke-WebRequest -Uri $uri -UseBasicParsing $version = ($response.content | ConvertFrom-Json).tag_name return $version.TrimStart("v") } catch { Throw "Couldn't get $tool latest version from $uri. $($_.Exception.Message)" } } function Test-EmptyDirectory($path) { if (-not (Test-Path -Path $path)) { return $true } $pathItems = Get-ChildItem -Path $path -Recurse -ErrorAction Ignore | Where-Object { !$_.PSIsContainer -or ($_.PSIsContainer -and $_.GetFileSystemInfos().Count -ne 0) } if (!$pathItems) { return $true } $itemCount = $pathItems | Measure-Object return ($itemCount.Count -eq 0) } function Get-ReleaseAssets { [OutputType([PSCustomObject])] param ( [string]$repo, # containers repo-owner/$repo-name [string]$version, [string]$OSArch ) function Invoke-GitHubApi { param($uri) try { Write-Debug "Invoking GitHub API. URI: $uri" $response = Invoke-RestMethod -Uri "$uri" -Headers @{ "User-Agent" = "PowerShell" } return $response } catch { Throw "GitHub API error. URL: `"$uri`". Error: $($_.Exception.Message)" } } Write-Debug "Getting release assets:`n`trepo: $repo`n`trelease version: $version`n`trelease architecture: $OSArch " $baseApiUrl = "https://api.github.com/repos/$repo" if ($version -eq "latest") { $apiUrl = "$baseApiUrl/releases/latest" } else { # We use this method to get the release assets for a specific version # because creating a string for the version tag is not always consistent # e.g. "v1.0.0" vs "1.0.0" # The q parameter used for searching is not available in the GitHub API's /tags endpoint. # GitHub's API for listing tags (/repos/{owner}/{repo}/tags) does not support querying or filtering # directly through a q parameter like some other endpoints might (e.g., the search API). # Get all releases tags $response = Invoke-GitHubApi -Uri "$baseApiUrl/tags" $releaseTag = $response | Where-Object { ($_.name.TrimStart("v")) -eq ($version.TrimStart("v")) } if (-not $releaseTag) { Throw "Couldn't find release tags for the provided version: '$version'" } if ($releaseTag.Count -gt 1) { Write-Warning "Found multiple release tags for the provided version: '$version'. Using the first tag." } $releaseTagName = $releaseTag | Select-Object -First 1 -ExpandProperty name # Get the release with the specified tag Write-Debug "Release tag: $releaseTagName" $apiUrl = "$baseApiUrl/releases/tags/$releaseTagName" } $response = Invoke-GitHubApi -Uri "$apiUrl" # Filter list of assets by architecture and file name $releaseAssets = $response | Select-Object -Property name, url, created_at, published_at, ` @{ l = "version"; e = { $_.tag_name } }, ` @{ l = "assets_url"; e = { $_.assets[0].url } }, ` @{ l = "release_assets"; e = { $_.assets | Where-Object { ( # Filter assets by OS (windows) and architecture # In the "zip|tar.gz" regex, we do not add the "$" at the end to allow for checksum files to be included # The checksum files end with eg: ".tar.gz.sha256sum" ($_.name -match "(windows(.+)$OSArch)") -or # nerdctl checksum files are named "SHA256SUMS". (& ([ScriptBlock]::Create($NERDCTL_FILTER_SCRIPTBLOCK_STR -f $_.name))) ) } | ForEach-Object { Write-Debug ("Asset name: {0}" -f $_.Name) [PSCustomObject]@{ "asset_name" = $_.name "asset_download_url" = $_.browser_download_url "asset_size" = $_.size } } } } # Check if any release assets were found for the specified architecture $archReleaseAssets = $releaseAssets.release_assets | Where-Object { ($_.asset_name -match "windows(.+)$OSArch") } if ($archReleaseAssets.Count -eq 0) { Throw "Couldn't find release assets for the provided architecture: '$OSArch'" } if ($archReleaseAssets.Count -lt 2) { Write-Warning "Some assets may be missing for the release. Expected at least 2 assets, found $($archReleaseAssets.Count)." } # Return the assets for the release. Includes the archive file for the binaries and the checksum files. return $releaseAssets } function Get-InstallationFile { [OutputType([string[]])] param( [parameter(Mandatory, HelpMessage = "Information (parameters) of the file to download")] [PSCustomObject] $fileParameters ) begin { function Receive-File { param($params) $MaximumRetryCount = 3 $RetryIntervalSec = 60 $lastError = $null # Store the last exception do { try { Invoke-WebRequest -Uri $params.Uri -OutFile $params.DownloadPath -UseBasicParsing return } catch { $lastError = $_ # Store the last error for proper exception handling Write-Warning "Failed to download `"$($params.Feature)`" release assets. Retrying... ($MaximumRetryCount retries left)" Start-Sleep -Seconds $RetryIntervalSec $MaximumRetryCount -= 1 } } until ($MaximumRetryCount -eq 0) # Throw the last encountered error after all retries fail Throw "Couldn't download `"$($params.Feature)`" release assets from `"$($params.Uri)`".`n$($lastError.Exception.Message)" } function DownloadAssets { param( [string]$featureName, [string]$version, [string]$downloadPath = "$HOME\Downloads", [PSCustomObject]$releaseAssets, [PSCustomObject]$file ) $archiveFile = $checksumFile = $null foreach ($asset in $releaseAssets.PSObject.Properties) { $key = $asset.Name $asset_params = $asset.Value $destPath = Join-Path -Path $downloadPath -ChildPath $asset_params.asset_name switch ($key) { "ArchiveFileAsset" { $archiveFile = $destPath } "ChecksumFileAsset" { $checksumFile = $destPath } default { Throw "Invalid input: $key" } } $downloadParams = [PSCustomObject]@{ Feature = $featureName Version = $version Uri = $asset_params.asset_download_url DownloadPath = $destPath } Write-Debug "Downloading asset $($asset_params.asset_name)...`n`tVersion: $version`n`tURI: $($downloadParams.Uri)`n`tDestination path: $($downloadParams.DownloadPath)" try { Receive-File -Params $downloadParams } catch { Write-Error "Failed to download $($downloadParams.Feature) release assets. $($_.Exception.Message)" Throw $_ # Re-throw the exception to halt execution if a download fails } } # Verify that both the archive and checksum files were downloaded if (-not (Test-Path $archiveFile)) { Throw "Archive file not found in the release assets: `'$archiveFile`"" } if (-not (Test-Path $checksumFile)) { Throw "Checksum file not found in the release assets: `'$checksumFile`"" } # Verify checksum try { $isValidChecksum = if ([System.IO.Path]::GetExtension($checksumFile) -eq ".json") { Test-Checksum -JSON -DownloadedFile $archiveFile -ChecksumFile $checksumFile -SchemaFile $file.ChecksumSchemaFile } else { Test-Checksum -DownloadedFile $archiveFile -ChecksumFile $checksumFile } } catch { Write-Error "Checksum verification process failed: $($_.Exception.Message)" Throw $_ # Re-throw the exception if checksum verification fails } # Remove the checksum file after verification if (Test-Path -Path $checksumFile) { Remove-Item -Path $checksumFile -Force -ErrorAction SilentlyContinue } if (-not $isValidChecksum) { Write-Error "Checksum verification failed for $archiveFile. The file will be deleted." # Remove the checksum file after verification if (Test-Path -Path $archiveFile) { Remove-Item -Path $archiveFile -Force -ErrorAction SilentlyContinue } Throw "Checksum verification failed. One or more files are corrupted." } return $archiveFile } } process { # Fetch the release assets based on the provided parameters $releaseAssets = Get-ReleaseAssets -repo $fileParameters.Repo -version $fileParameters.Version -OSArch $fileParameters.OSArchitecture # Filter file names based on the provided regex or default logic if ([string]::IsNullOrWhiteSpace($fileParameters.FileFilterRegEx)) { # Default logic to filter the archive and checksum files $filteredAssets = $releaseAssets.release_assets | Where-Object { # In the "zip|tar.gz" regex, we do not add the "$" at the end to allow for checksum files to be included # The checksum files end with eg: ".sha256sum" ($_.asset_name -match ".*(.zip|.tar.gz)") -or # Buildkit checksum files are named ending with ".provenance.json" or ".sbom.json" # We only need the ".sbom.json" file ($_.asset_name -match ".sbom.json$") -or # nerdctl checksum files are named "SHA256SUMS". Check file names that have such a format. (& ([ScriptBlock]::Create($NERDCTL_FILTER_SCRIPTBLOCK_STR -f $_.asset_name))) } } else { # Use the provided regex to filter the archive and checksum files $fileFilterRegEx = $fileParameters.FileFilterRegEx -replace "<__VERSION__>", "v?$($releaseAssets.version.TrimStart('v'))" Write-Debug "File filter: `"$fileFilterRegEx`"" $filteredAssets = $releaseAssets.release_assets | Where-Object { $_.asset_name -match $fileFilterRegEx } } # Pair archive and checksum files $assetsToDownload = @() $archiveExtensionStr = @(".zip", ".tar.gz", ".tgz") -join "|" $failedDownloads = @() foreach ($asset in $filteredAssets) { if ($asset.asset_name -notmatch "(?<extension>$archiveExtensionStr)$") { continue } $fileExtension = $matches.extension # Remove the trailing archive file extension to get the checksum file name $assetFileName = $asset.asset_name -replace "$fileExtension", "" # Find the checksum file that matches the archive $checksumAsset = $filteredAssets | Where-Object { ($_.asset_name -match "(?:(^$assetFileName).*($HASH_FUNCTIONS_STR))") -or # Buildkit checksum is in .sbom.json ($_.asset_name -match ".sbom.json$") -or (& ([ScriptBlock]::Create($NERDCTL_FILTER_SCRIPTBLOCK_STR -f $_.asset_name))) } if (-not $checksumAsset) { Write-Error "Checksum file for $assetFileName not found. Skipping download." $failedDownloads += $asset.asset_name continue } $assetsToDownload += @{ FeatureName = $releaseAssets.name Version = $releaseAssets.version DownloadPath = $fileParameters.DownloadPath File = $fileParameters ReleaseAssets = [PSCustomObject]@{ ArchiveFileAsset = $asset ChecksumFileAsset = $checksumAsset } } } if ($failedDownloads) { $errorMsg = "Failed to find checksum files for $($failedDownloads -join ', ')." } # Download the archive and verify checksum $archiveFiles = $failedDownloads = @() Write-Debug "Assets to download count: $($assetsToDownload.ReleaseAssets.Count)" foreach ($asset in $assetsToDownload) { try { Write-Debug "Downloading $($asset.FeatureName) assets..." $archiveFile = DownloadAssets @asset Write-Debug "Downloaded archive file: `"$archiveFile`"" $archiveFiles += $archiveFile } catch { Write-Error "Failed to download assets for `"$($asset.FeatureName)`". $_" $failedDownloads += $asset.FeatureName } } if ($errorMsg) { Throw "Some files were not downloaded. $errorMsg" } if ($failedDownloads) { Throw "Failed to download assets for $($failedDownloads -join ', '). See logs for detailed error information." } # Return the archive file path. May be multiple files. # eg. containerd may contain containerd, cri-containerd, and cri-containerd-cni return $archiveFiles } end { Write-Information "File download and verification process completed." } } function Test-CheckSum { param( [Parameter(Mandatory, ParameterSetName = 'Default')] [Parameter(Mandatory, ParameterSetName = 'JSON')] [ValidateNotNullOrEmpty()] [string] $DownloadedFile, [Parameter(Mandatory, ParameterSetName = 'Default')] [Parameter(Mandatory, ParameterSetName = 'JSON')] [ValidateNotNullOrEmpty()] [string] $ChecksumFile, [Parameter(Mandatory, ParameterSetName = 'JSON')] [switch]$JSON, [Parameter(Mandatory, ParameterSetName = 'JSON')] [ValidateNotNullOrEmpty()] [string]$SchemaFile, [Parameter(ParameterSetName = 'JSON')] [ScriptBlock]$ExtractDigestScriptBlock, [Parameter(ParameterSetName = 'JSON')] [System.Array]$ExtractDigestArguments ) Write-Debug "Checksum verification...`n`tSource file: $DownloadedFile`n`tChecksum file: $ChecksumFile" if (-not (Test-Path -Path $downloadedFile)) { Throw "Couldn't find source file: `"$downloadedFile`"." } if (-not (Test-Path -Path $ChecksumFile)) { Throw "Couldn't find checksum file: `"$ChecksumFile`"." } if ($JSON) { Write-Debug "Checksum file format: JSON" Write-Debug "SchemaFile: $SchemaFile" return ( Test-JSONChecksum ` -DownloadedFile $downloadedFile ` -ChecksumFile $ChecksumFile ` -SchemaFile $SchemaFile ` -ExtractDigestScriptBlock $ExtractDigestScriptBlock ` -ExtractDigestArguments $ExtractDigestArguments ) } Write-Debug "Checksum file format: Text" return Test-FileChecksum -DownloadedFile $DownloadedFile -ChecksumFile $ChecksumFile } function Test-FileChecksum { # https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/usedeclaredvarsmorethanassignments?view=ps-modules#special-cases [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "isValid", Justification = "Special case")] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "found", Justification = "Special case")] [OutputType([bool])] param( [parameter(Mandatory)] [string] $DownloadedFile, [Parameter(Mandatory)] [string] $checksumFile ) # Get download file name from downloaded file $downloadedFileName = Split-Path -Leaf $DownloadedFile $isValid = $false try { # Extract algorithm from checksum file name if ($checksumFile -notmatch "(?<hashfunction>$HASH_FUNCTIONS_STR)") { Throw "Invalid hash function. Supported hash functions: $($HASH_FUNCTIONS -join ', ')" } $algorithm = $matches.hashfunction Write-Debug "Algorithm: $algorithm" $downloadedChecksum = Get-FileHash -Path $DownloadedFile -Algorithm $algorithm # checksum is stored in a file named SHA256SUMS # checksum file content format: <checksum> <filename> # separate by two spaces $found = $false Get-Content -Path $checksumFile | ForEach-Object { # Split the read line to extract checksum and filename if ($_ -notmatch "^([\d\w]+)(\s){1,2}([\S]+)$") { Throw "Invalid checksum file content format in $checksumFile. Expected format: <checksum> <filename>." } # 0: full match, 1: checksum, 2: space, 3: filename $checksum = $matches[1] $filename = $matches[3] # Check if the downloaded file name matches any of the file names in the checksum file if ($filename -match "^(?:\.\/release\/)?($downloadedFileName)$") { $isValid = $downloadedChecksum.Hash -eq $checksum $found = $true return } } if (-not $found) { Throw "Checksum not found for `"$downloadedFileName`" in $checksumFile" } } catch { Throw "Checksum verification failed for $DownloadedFile. $_" } finally { # Delete checksum file Write-Debug "Deleting checksum file $checksumFile" if (Test-Path -Path $checksumFile) { Remove-Item -Path $checksumFile -Force -ErrorAction Ignore } } Write-Debug "Checksum verification status. {success: $isValid}" return $isValid } function Test-JSONChecksum { [OutputType([bool])] param( [parameter(Mandatory)] [string] $DownloadedFile, [parameter(Mandatory)] [string] $checksumFile, [Parameter(Mandatory)] [string] $SchemaFile, [Parameter(HelpMessage = "Script block to extract checksum from JSON file. If the ScriptBlock takes any parameters, pass them as an object with ``ExtractDigestArguments``. The function must return a FileDigest object with HashFunction (string) and Digest (string).")] [ScriptBlock]$ExtractDigestScriptBlock, [Parameter(HelpMessage = "Parameters to pass to the script block.")] [System.Array]$ExtractDigestArguments ) # Validate the checksum file $isJsonValid = ValidateJSONChecksumFile -ChecksumFilePath $checksumFile -SchemaFile $SchemaFile Write-Debug "Checksum JSON file validation status. {success: $isJsonValid}" if ($null -eq $ExtractDigestScriptBlock) { Write-Debug "Using default JSON checksum extraction script block" $ExtractDigestScriptBlock = ${function:GenericExtractDigest} $ExtractDigestArguments = @($DownloadedFile, $checksumFile) } # Invoke the script block to extract the file digest Write-Debug "Extracting file digest from $checksumFile" $extractedFileDigest = & $ExtractDigestScriptBlock @ExtractDigestArguments # Since Invoke() returns a collection, we need to extract the first item if ( ($null -eq $extractedFileDigest) -or ($extractedFileDigest.Count -ne 1) -or ($extractedFileDigest[0].GetType().Name -ne "FileDigest") ) { Throw 'Invalid value. Requires a value with type "FileDigest".' } # Validate the hash function and checksum $isValid = $false try { $algorithm = $extractedFileDigest[0].HashFunction.ToUpper() $digest = $extractedFileDigest[0].Digest # Validate the hash function if ($HASH_FUNCTIONS -notcontains $algorithm) { Throw "Invalid hash function, `"$algorithm`". Supported algorithms are: $($HASH_FUNCTIONS -join ', ')" } # Validate the checksum $hash = Get-FileHash -Path $DownloadedFile -Algorithm $algorithm $isValid = ($digest -eq $hash.Hash) } catch { Throw "Checksum verification failed for $downloadedFile. $_" } finally { # Delete checksum checksumFile Write-Debug "Deleting checksum file $checksumFile" if (Test-Path -Path $checksumFile) { Remove-Item -Path $checksumFile -Force -ErrorAction Ignore } } Write-Debug "Checksum verification status. {success: $isValid}" return $isValid } function ValidateJSONChecksumFile { param( [parameter(Mandatory = $true, HelpMessage = "Downloaded checksum file path")] [String]$ChecksumFilePath, [parameter(Mandatory = $true, HelpMessage = "JSON schema file path")] [String]$SchemaFile ) Write-Debug "Validating JSON checksum file...`n`tChecksum file path: $ChecksumFilePath`n`tSchema file: $SchemaFile" # Check if the schema file exists if (-not (Test-Path -Path $SchemaFile)) { Throw "Couldn't find the JSON schema file: `"$SchemaFile`"." } $schemaFileContent = Get-Content -Path $SchemaFile -Raw if ([string]::IsNullOrWhiteSpace($schemaFileContent)) { Throw "Invalid schema file: $SchemaFile. Schema file is empty." } # Test JSON checksum file is valid try { $isValidJSON = Test-Json -Json "$(Get-Content -Path $ChecksumFilePath -Raw)" -Schema "$schemaFileContent" return $isValidJSON } catch { Throw "Invalid JSON format in checksum file. $_" } } function GenericExtractDigest { param( [parameter(Mandatory = $true, HelpMessage = "Downloaded tool file path")] [String]$DownloadedFile, [parameter(Mandatory = $true, HelpMessage = "Downloaded checksum file path")] [String]$ChecksumFile ) Write-Debug "Extracting digest from $checksumFile using default script block for in-toto SBOM format" # Read the JSON file and get the checksum $jsonContent = Get-Content -Path $ChecksumFile -Raw | ConvertFrom-Json # Check if using in-toto SBOM format: https://github.com/in-toto/attestation/tree/v0.1.0/spec#statement if ($jsonContent._type -notlike "https://in-toto.io/*") { Throw( -join ( "Invalid checksum JSON format. Expected in-toto SBOM format: $($jsonContent._type). ", "Please provide an appropriate script block to extract the digest for $($jsonContent._type) format.")) return } # Check if the downloaded filename is the same as subject.name $downloadedFileName = Split-Path "$DownloadedFile" -Leaf for ($i = 0; $i -lt $jsonContent.subject.Count; $i++) { $subject = $jsonContent.subject[$i] if ($subject.name -ne $downloadedFileName) { continue } $digest = $subject.digest $algorithm = ($digest | Get-Member -MemberType NoteProperty).Name $checksum = $digest.$algorithm return ([FileDigest]::new($algorithm, $checksum)) } Throw "Downloaded file name does not match the subject name ($($subject.name)) in the JSON file." } function Get-DefaultInstallPath($tool) { switch ($tool) { "buildkit" { $executable = "build*.exe" } Default { $executable = "$tool.exe" } } $source = Get-Command -Name $executable -ErrorAction Ignore | ` Where-Object { $_.Source -like "*$tool*" } | ` Select-Object Source -Unique if ($source) { return (Split-Path -Parent $source[0].Source) -replace '(\\bin)$', '' } return "$Env:ProgramFiles\$tool" } function Install-RequiredFeature { param( [string] $Feature, [string] $InstallPath, [string[]] $SourceFile, [string] $EnvPath, [boolean] $cleanup, # Use by WinCNI plugin to avoid updating the environment path [boolean] $UpdateEnvPath = $true ) # Create the directory to untar to Write-Information -InformationAction Continue -MessageData "Extracting $Feature to $InstallPath" if (!(Test-Path $InstallPath)) { New-Item -ItemType Directory -Force -Path $InstallPath | Out-Null } # Untar file $failed = @() foreach ($file in $SourceFile) { if (-not (Test-Path -Path $file)) { Throw "Couldn't find source file: `"$file`"." } Write-Debug "Expand archive:`n`tSource file: $file`n`tDestination Path: $InstallPath" $cmdOutput = Invoke-ExecutableCommand -executable "tar.exe" -arguments "-xf `"$file`" -C `"$InstallPath`"" -timeout (60 * 2) if ($cmdOutput.ExitCode -ne 0) { Write-Error "Failed to expand archive `"$file`" at `"$InstallPath`". Exit code: $($cmdOutput.ExitCode). $($cmdOutput.StandardError.ReadToEnd())" $failed += $file } } if ($failed) { Throw "Couldn't expand archive file(s) $($failed -join ','). See logs for detailed error information." } # Add to env path if ($UpdateEnvPath -and -not [string]::IsNullOrWhiteSpace($envPath)) { Add-FeatureToPath -Feature $feature -Path $envPath } # Clean up if ($CleanUp) { Write-Output "Cleanup to remove downloaded files" if (Test-Path -Path $SourceFile -ErrorAction SilentlyContinue) { Remove-Item -Path $SourceFile -Force -ErrorAction Ignore } } } function Add-FeatureToPath ($Path, $Feature) { @("User", "System") | ForEach-Object { Update-EnvironmentPath -Tool $Feature -Path $Path -Action 'Add' -PathType $_ } } function Remove-FeatureFromPath { [CmdletBinding( SupportsShouldProcess = $true )] param( [string]$Feature ) process { if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "$feature will be removed from Environment paths")) { @("User", "System") | ForEach-Object { Update-EnvironmentPath -Tool $Feature -Action 'Remove' -PathType $_ } } else { # Code that should be processed if doing a WhatIf operation # Must NOT change anything outside of the function / script return } } } function Test-ServiceRegistered ($service) { $scQueryResult = (sc.exe query $service) | Select-String -Pattern "SERVICE_NAME: $service" return ($null -ne $scQueryResult) } function Invoke-ServiceAction ($Action, $service) { if (!(Test-ServiceRegistered -Service $service)) { throw "$service service does not exist as an installed service." } $serviceInfo = Get-Service $service -ErrorAction Ignore if (!$serviceInfo) { Throw "$service service does not exist as an installed service." } switch ($Action) { 'Start' { Invoke-StartService -Service $service } 'Stop' { Invoke-StopService -Service $service } Default { Throw 'Not implemented' } } } function Invoke-StartService($service) { process { try { Start-Service -Name $service # Waiting for the service to come to a steady state (Get-Service -Name $service -ErrorAction Ignore).WaitForStatus('Running', '00:00:30') Write-Debug "Success: { Service: $service, Action: 'Start' }" } catch { Throw "Couldn't start $service service. $_" } } } function Invoke-StopService($service) { process { try { Stop-Service -Name $service -NoWait -Force # Waiting for the service to come to a steady state (Get-Service -Name $service -ErrorAction Ignore).WaitForStatus('Stopped', '00:00:30') Write-Debug "Success: { Service: $service, Action: 'Stop' }" } catch { Throw "Couldn't stop $service service. $_" } } } function Test-ConfFileEmpty($Path) { if (!(Test-Path -LiteralPath $Path)) { return $true } $isFileNotEmpty = (([System.IO.File]::ReadAllText($Path)) -match '\S') return (-not $isFileNotEmpty ) } function Uninstall-ProgramFiles($path) { try { Get-Item -Path "$path" -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force } catch { $errMsg = $_ if ($errMsg -match "denied") { Write-Error "Failed to delete directory: '$path'. Access to path denied. To resolve this issue, see https://github.com/microsoft/containers-toolkit/blob/main/docs/docs/FAQs.md#resolving-uninstallation-error-access-to-path-denied" } else { Write-Error "Failed to delete directory: '$path'. $_" } } } function Invoke-ExecutableCommand { [OutputType([System.Diagnostics.Process])] param ( [parameter(Mandatory)] [String] $executable, [parameter(Mandatory)] [String] $arguments, [Parameter(Mandatory = $false, HelpMessage = "Period of time to wait (in seconds) for the associated process to exit. Default is 15 seconds.")] [Int32] $timeout = 15 ) Write-Debug "Executing '$executable $arguments'" $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = $executable $pinfo.RedirectStandardError = $true $pinfo.RedirectStandardOutput = $true $pinfo.UseShellExecute = $false $pinfo.Arguments = $arguments $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo $p.Start() | Out-Null # Blocks the current thread of execution until the time has elapsed or the process has exited. $p.WaitForExit($timeout * 1000) | Out-Null if (-not $p.HasExited) { Write-Debug "Execution did not complete in $timeout seconds." } else { Write-Debug "Command execution completed. Exit code: $($p.ExitCode)" } return $p } Export-ModuleMember -Variable CONTAINERD_REPO, BUILDKIT_REPO, NERDCTL_REPO, WINCNI_PLUGIN_REPO, CLOUDNATIVE_CNI_REPO Export-ModuleMember -Function Get-LatestToolVersion Export-ModuleMember -Function Get-DefaultInstallPath Export-ModuleMember -Function Test-EmptyDirectory Export-ModuleMember -Function Get-InstallationFile Export-ModuleMember -Function Install-RequiredFeature Export-ModuleMember -Function Invoke-ExecutableCommand Export-ModuleMember -Function Test-ServiceRegistered Export-ModuleMember -Function Add-FeatureToPath Export-ModuleMember -Function Remove-FeatureFromPath Export-ModuleMember -Function Invoke-ServiceAction Export-ModuleMember -Function Test-ConfFileEmpty Export-ModuleMember -Function Uninstall-ProgramFiles Export-ModuleMember -Function Test-CheckSum # SIG # Begin signature block # MIIoRAYJKoZIhvcNAQcCoIIoNTCCKDECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD5E3ayxuJhyOJ0 # 23s8KKLrxNEslJs3zqI3EZxMp+wlR6CCDYswggYJMIID8aADAgECAhMzAAAD9LjE # XeFOcLZ+AAAAAAP0MA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwNzE3MjEwMjM1WhcNMjUwOTE1MjEwMjM1WjCBiDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWlj # cm9zb2Z0IDNyZCBQYXJ0eSBBcHBsaWNhdGlvbiBDb21wb25lbnQwggEiMA0GCSqG # SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv3P8bL08GKolFW7QNDVOF0aM4iqMxVvAW # VM124/82xbjAraJkKxieMrQa1Fc95LVGgxmJIi5R6QKMz2MO9bnwC7kSkPqoZJil # 26bRLY6jinjbwPpK3TzbW7z9bXfWw5bPFlt72NVIdXJ3xtHoYa+AOi++CF2Ry7+7 # o1AzvotJwG6lQSiCMKeMt8apqEF1f+QkDFEUv5tezw9748DeHW9orvo4IPzWa7vW # QgljB08LKSnzTN9/Jot2coWpFv4YuEoJZmR2ofPJMnDUUruDORTXnxwhfvd/wUmI # SoEysSqobkNV+qFuUmSShYrx8R1zHm7P6G/iRMIKYmSrIYBKUvndAgMBAAGjggFz # MIIBbzAfBgNVHSUEGDAWBgorBgEEAYI3TBEBBggrBgEFBQcDAzAdBgNVHQ4EFgQU # Dz4uMjS8YCSZaU0449GJYQ1ufyowRQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEWMBQGA1UEBRMNMjMxNTIyKzUwMjUxODAfBgNV # HSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNo # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0Ey # MDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZF # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQ # Q0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL # BQADggIBABUCAiEn4g8i5T3VCP8160IY4ERdvZi5QZ2pSnBPW1dswVhLxkNTiCTV # XKDjTQ4EwDBNSZZGJePz4+t86pKhlBON3S7wswf5fCovJLlIiKbw+E4TZeY6xAxd # +5zV7Q2lsQhPHxiOY0PIGUE0KJfv/DQUulD8DrE0rru7yOO+DJI0muoK0BbHhRfd # mAJhp2gbYRkarEIkhML9m3gR12mCBb69Vocm4IyOBivUPMjjvQMkERF7cR07k2uP # 6dmpR8wtof9la0/K0wgiP5XuQUsAqgzhXrljH7dK7nqGrBDjJtrRdYfvVL+Rcz9i # YZO280g2uNtac5em3HOEsactAL7XKqZ4o7s9sRyp/bTNLLRmhFMB729IL+Hi0YM7 # C8th3HZ5nP+77L46KUGip6QgRIJs+EO0YNW+AwgMxPfKpTx/Ggh8Z85kP7HLDZJk # ZdPO/3cgVOTO4ax21vO2yMPCdfoGGr2ZLZw4SjEbGuOZJ22iGMV7tBvHk8nWAt3q # +j/icAq99GA1nIPnw3jK3K9OwGqwA9eiWsO8/bHMm6s50UKIFupMKm6qObosaVBy # R58rf8Cxumka7hPy1eSJSzQyA4UqYNTWuChsTfqgRLmLomS6yAu7t4r/bM4mGl+2 # Ki+avhQ4COm3jWWd0V6UGIP3T4zaKNs2GWFBIYsb/6XVvvi7pz/JMIIHejCCBWKg # AwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3Qg # Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYw # NzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGf # Qhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRg # JGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NE # t13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnn # Db6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+E # GvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+t # GSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxh # H2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AV # s70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f # 7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3D # KI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9Jaw # vEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1Ud # DgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi # AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRy # LToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3Js # Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDEx # XzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0 # cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDEx # XzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/ # BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2Nz # L3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAA # bwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUA # A4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+ # vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4H # Limb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6 # aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiX # mE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn # +N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAq # ZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5h # YbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/ # RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXm # r/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMyk # XcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGg8wghoLAgEB # MIGVMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNV # BAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAP0uMRd4U5w # tn4AAAAAA/QwDQYJYIZIAWUDBAIBBQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQB # gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE # MSIEIAih5WVYQw/XLv11645meHVV+wvFHu+NW4WZf85+zxBeMEQGCisGAQQBgjcC # AQwxNjA0oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEcgBpodHRwczovL3d3dy5taWNy # b3NvZnQuY29tIDANBgkqhkiG9w0BAQEFAASCAQArvapeCiUzYgJ0S36TLJB+YA5k # p+/oZSUlqjK4lsZYh2XB1ch8HDlh5ZoX7BpGgWr2pUNiOn/ee6NspMDDNcjGyBgv # LO+Y8HY4+sMVn9FuCKLrTtzFr/0BpbZubtYIyhC6KaGFjjYRHSD8FfBxRyS/vB2S # lOX+dfOfqXzD6c4FFfmHkFHUnakRXNV7/8+bvfAlo2wLeRaJuXGAWeXkWLH4eI4V # h/re0pQabqNbmLhod3m/+6rHolrH0tsz7u6mJm5NXxiLeSAJZ+yeT/vbggz6ZUBo # EOEySr1MCRoIP7Qn5mY3ZzV/dKclUGIqSBfG7BlyhLtQKbW3AKMyFVVxthoIoYIX # lzCCF5MGCisGAQQBgjcDAwExgheDMIIXfwYJKoZIhvcNAQcCoIIXcDCCF2wCAQMx # DzANBglghkgBZQMEAgEFADCCAVIGCyqGSIb3DQEJEAEEoIIBQQSCAT0wggE5AgEB # BgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEICge72xIxQq7QqX7AYmRSro+ # tz//K79xTiMr2uBYkJQeAgZoJdLn0t0YEzIwMjUwNTE1MTYxOTQ5LjYyMlowBIAC # AfSggdGkgc4wgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsT # Hm5TaGllbGQgVFNTIEVTTjpBNDAwLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9z # b2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEe0wggcgMIIFCKADAgECAhMzAAACAnlQ # dCEUfbihAAEAAAICMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMB4XDTI1MDEzMDE5NDI0NFoXDTI2MDQyMjE5NDI0NFowgcsxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jv # c29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # TjpBNDAwLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALd5Knpy5xQY # 6Rw+Di8pYol8RB6yErZkGxhTW0Na9C7ov2Wn52eqtqMh014fUc3ejPeKIagla43Y # dU1mRw63fxpYZ5szSBRQ60+O4uG47l3rtilCwcEkBaFy978xV2hA+PWeOICNKI6s # vzEVqsUsjjpEfw14OEA9dwmlafsAjMLIiNk5onYNYD7pDA3PCqMGAil/WFYXCoe8 # 8R53LSei1du1Z9P28JIv2x0Mror8cf0expjnAuZRQHtJ+4sajU5YSbownIbaOLGq # L03JGjKl0Xx1HKNbEpGXYnHC9t62UNOKjrpeWJM5ySrZGAz5mhxkRvoSg5213Rcq # HcvPHb0CEfGWT7p4jBq+Udi44tkMqh085U3qPUgn1uuiVjqZluhDnU6p7mcQzmH9 # YlfbwYtmKgSQk3yo57k/k/ZjH0eg6ou6BfTSoLPGrgEObzEfzkcrG8oI7kqKSilp # EYa1CVeMPK6wxaWsdzJK3noOEvh1xWeft0W8vnTO9CUVkyFWh6FZJCSRa5SUIKog # 6tN7tFuadt0miwf7uUL6fneCcrLg6hnO5R6rMKdIHUk1c8qcmiM/cN7nHCymLm1S # 9AU1+V8ZOyNmBACAMF2D8M7RMaAtEMq9lAJnmoi5elBHKDfvJznV73nPxTabKxTR # edKlZ6KAeqTI4C0N9wimrka/sdX51rZHAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQU # 2ga5tQ+M/Z/yJ+Qgq/DLWuVIdNkwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacb # UzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAo # MSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1w # JTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAK # BggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAIPz # doVBTE3fseQ6gkMzWZocVlVQZypNBw+c4PpShhEyYMq/QZpseUTzYBiAs+5WW6Sf # se0k8XbPSOdOAB9EyfbokUs8bs79dsorbmGsE8nfSUG7CMBNW3nxQDUFajuWyafK # u6v/qHwAXOtfKte2W/NBippFhj2TRQVjkYz6f1hoQQrYPbrx75r4cOZZ761gvYf7 # 07hDUxAtqD5yI3AuSP/5CXGleJai70q8A/S0iT58fwXfDDlU5OL1pn36o+OzPDfU # fid22K8FlofmzlugmYfYlu0y5/bLuFJ0l0TRRbYHQURk8siZ6aUqGyUk1WoQ7tE+ # CXtzzVC5VI7nx9+mZvC1LGFisRLdWw+CVef04MXsOqY8wb8bKwHij9CSk1Sr7BLt # s5FM3Oocy0f6it3ZhKZr7VvJYGv+LMgqCA4J0TNpkN/KbXYYzprhL4jLoBQinv8o # ikCZ9Z9etwwrtXsQHPGh7OQtEQRYjhe0/CkQGe05rWgMfdn/51HGzEvS+DJruM1+ # s7uiLNMCWf/ZkFgH2KhR6huPkAYvjmbaZwpKTscTnNRF5WQgulgoFDn5f/yMU7X+ # lnKrNB4jX+gn9EuiJzVKJ4td8RP0RZkgGNkxnzjqYNunXKcr1Rs2IKNLCZMXnT1i # f0zjtVCzGy/WiVC7nWtVUeRI2b6tOsvArW2+G/SZMIIHcTCCBVmgAwIBAgITMwAA # ABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3Qg # Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAw # OTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6c # BwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWN # E893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8 # OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6O # U8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6 # BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75x # qRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrb # qn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XY # cz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK # 12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJR # XRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnG # rnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsG # AQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBe # Yl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/Bggr # BgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1Jl # cG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQM # HgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1Ud # IwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0 # dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0Nl # ckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKG # Pmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0 # XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEk # W+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zR # oZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1 # AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthIS # EV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4s # a3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32 # THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMB # V0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5P # ndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUx # UYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi # 6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6C # baUFEMFxBmoQtB1VM1izoXBm8qGCA1AwggI4AgEBMIH5oYHRpIHOMIHLMQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3Nv # ZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046 # QTQwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNl # cnZpY2WiIwoBATAHBgUrDgMCGgMVAEmJSGkJYD/df+NnIjLTJ7pEnAvOoIGDMIGA # pH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQELBQAC # BQDr0FFkMCIYDzIwMjUwNTE1MTE0MTI0WhgPMjAyNTA1MTYxMTQxMjRaMHcwPQYK # KwYBBAGEWQoEATEvMC0wCgIFAOvQUWQCAQAwCgIBAAICAIwCAf8wBwIBAAICEfYw # CgIFAOvRouQCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgC # AQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsFAAOCAQEAqqUjDKJUQO5S # 5oWMRobJDrdADBYfc7j4mRkYrEUPK4jrYzGKExolvKc4XPg4FTbNNC26hewAakPw # Yzv/W7lVUvT9bC/SDlDbHIBlJ8fn5W9xERKAGJW/RsAO419HsXOWuRUHfPDKxRXk # 3keFzUKCWZ/W5lvKPbcuWsOBEgeRopyjtt5WL8HJrI7utXSEMRg5EF4rza6a+mmK # 5o93iGgHcfSdYw6JxD6/BIwyu7Ym0wGrlfTGjC7b3lVvnD2ToFSmLdZdpJtg4AtH # PF/IEvV0O8gIFtuncGwmu844MkSqa17rc80SsUDnYDOOdyhkBJwAW26ikitsppQr # 3ny6MonygTGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy # MDEwAhMzAAACAnlQdCEUfbihAAEAAAICMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkq # hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEILzs/8/YUM0O # mO1cxt9gYvT4CVahk6gxjEGEZoXWd4uJMIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB # 5DCBvQQg843qARgHlsvNcta5SYvxl3zFcCypeSx50XKiV8yUX+wwgZgwgYCkfjB8 # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N # aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAgJ5UHQhFH24oQABAAAC # AjAiBCCneRkQYbjGMp3Wxu2U5Dh+xgPqhLWg/GvX2xjkcWuAyzANBgkqhkiG9w0B # AQsFAASCAgAVQ+WIex7CZY6l04dRn8ZUVYJWWODaw66PWE55yYlxUqHRJ+UyKGCJ # pEEfWV93kVygr8np/z2YYiX+ZmGrdhXQpFaCwfxfZOpPeKqf1qypozccMW+ijAZu # BLT6cWh5zzecJIEmip4++GL9Gc/vYt06TaFLb8ZNQp1/yKidUx3nfYR+gaT3MeaN # qxrJ6Ow3afzDnZfXi+nRe3yiVhSuTPgchmOfZJ3DJshAR96pCD9UgGJO6a8ioyCT # IXteJFrWVq2d8Kf40xonGGeoaL67ADNDW3dTyE6TzAfOv2NWWonDVEYHByL4UrVZ # CRZFAku71nHoQHanF8hHLpTDy/JkhNroGqfeqIn9Q5sb99Vq7oXkpMEWNyk3Jqga # lHGewSjWXTqqJmLSlIuisV8D4TMxq3TMIqZTiVScQHmaLEo8eDQd0D15UMM3BAZZ # IlykJn+Ng+GKUmVF0IfEJWp8zOHZwkTwEpe/WNmGNlKBkvz5c+ie1EZvjhH/6B1q # 4yNnmfwMymX1Xp0o+/ExszsbFQr3gcwIDV21PIFBBQE7I3bfKhGr6iunze8m2ne8 # 7df/tpSJJp0T8caA0fxX3FCkkalNCGi09Qea645cwWFI0ZcyY9wHESwrhn+ZQbvb # yFCouvOnyCJAbg4n1VvVPMF84Xd5oscKE/k/KLTqtF0UPgwKx3vk1g== # SIG # End signature block |