Public/ContainerdTools.psm1

###########################################################################
# #
# Copyright (c) Microsoft Corporation. All rights reserved. #
# #
# This code is licensed under the MIT License (MIT). #
# #
###########################################################################

using module "..\Private\CommonToolUtilities.psm1"

$ModuleParentPath = Split-Path -Parent $PSScriptRoot
Import-Module -Name "$ModuleParentPath\Private\CommonToolUtilities.psm1" -Force


function Get-ContainerdLatestVersion {
    $latestVersion = Get-LatestToolVersion -Tool "containerd"
    return $latestVersion
}

function Install-Containerd {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param(
        [parameter(HelpMessage = "ContainerD version to use. Defaults to 'latest'")]
        [string]$Version = "latest",

        [parameter(HelpMessage = "Path to install containerd. Defaults to ~\program files\containerd")]
        [string]$InstallPath = "$Env:ProgramFiles\Containerd",

        [parameter(HelpMessage = "Path to download files. Defaults to user's Downloads folder")]
        [string]$DownloadPath = "$HOME\Downloads",

        [Parameter(HelpMessage = "Register and start Containerd Service")]
        [switch]$Setup,

        [Parameter(HelpMessage = 'OS architecture to download files for. Default is $env:PROCESSOR_ARCHITECTURE')]
        [ValidateSet('amd64', '386', "arm", "arm64")]
        [string]$OSArchitecture = $env:PROCESSOR_ARCHITECTURE,

        [Switch]
        [parameter(HelpMessage = "Installs Containerd even if the tool already exists at the specified path")]
        $Force
    )

    begin {
        # Check if Containerd is alread installed
        $isInstalled = -not (Test-EmptyDirectory -Path $InstallPath)

        $WhatIfMessage = "Containerd will be installed at $InstallPath"
        if ($isInstalled) {
            $WhatIfMessage = "Containerd will be uninstalled from and reinstalled at $InstallPath"
        }
        if ($Setup) {
            <# Action when this condition is true #>
            $WhatIfMessage = "Containerd will be installed at $InstallPath and containerd service will be registered and started"
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $WhatIfMessage)) {
            # Check if tool already exists at specified location
            if ($isInstalled) {
                $errMsg = "Containerd already exists at $InstallPath or the directory is not empty"
                Write-Warning $errMsg

                # Uninstall if tool exists at specified location. Requires user consent
                try {
                    Uninstall-Containerd -Path "$InstallPath" -Confirm:$false -Force:$Force | Out-Null
                }
                catch {
                    Throw "Containerd installation failed. $_"
                }
            }

            # Get Containerd version to install
            if (!$Version) {
                # Get default version
                $Version = Get-ContainerdLatestVersion
            }
            $Version = $Version.TrimStart('v')
            Write-Output "Downloading and installing Containerd v$version at $InstallPath"

            # Download files
            $downloadParams = @{
                ToolName           = "Containerd"
                Repository         = "$CONTAINERD_REPO"
                Version            = $version
                OSArchitecture     = $OSArchitecture
                DownloadPath       = $DownloadPath
                ChecksumSchemaFile = $null

                # QUESTION: Do we need them all? Containerd release contains multiple files. containerd, cri-containerd, cri-containerd-cni
                # Matches eg: containerd-1.7.21-windows-amd64.tar.gz and containerd-1.7.21-windows-amd64.tar.gz.sha256sum
                FileFilterRegEx    = "(?:^containerd-<__VERSION__>-windows-$OSArchitecture.*.tar.gz(.*)?)$"
            }
            $downloadParamsProperties = [FileDownloadParameters]::new(
                $downloadParams.ToolName,
                $downloadParams.Repository,
                $downloadParams.Version,
                $downloadParams.OSArchitecture,
                $downloadParams.DownloadPath,
                $downloadParams.ChecksumSchemaFile,
                $downloadParams.FileFilterRegEx
            )
            $sourceFile = Get-InstallationFile -FileParameters $downloadParamsProperties

            # Untar and install tool
            $params = @{
                Feature     = "containerd"
                InstallPath = $InstallPath
                SourceFile  = $sourceFile
                EnvPath     = "$InstallPath\bin"
                cleanup     = $true
            }
            Install-RequiredFeature @params

            Write-Output "Containerd v$version successfully installed at $InstallPath `n"

            $showCommands = $true
            try {
                if ($Setup) {
                    Register-ContainerdService -ContainerdPath $InstallPath -Start -Force:$true
                    $showCommands = $false
                }
            }
            catch {
                Write-Warning "Failed to setup Containerd service. $_"
            }

            if ($showCommands) {
                $commands = (Get-command -Name '*containerd*' | Where-Object { $_.Source -like 'Containers-Toolkit' -and $_.Name -ne 'Install-Containerd' }).Name
                $message = "Other useful Containerd commands: $($commands -join ', ').`nTo learn more about each command, run Get-Help <command-name>, e.g., 'Get-Help Register-ContainerdService'"
                Write-Information -MessageData $message -Tags "Instructions" -InformationAction Continue
            }

            Write-Output "For containerd usage: run 'containerd -h'`n"
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

# YAGNI: Is this necessary?
function Build-ContainerdFromSource {
    Throw "Method or operation not implemented."
}

function Start-ContainerdService {
    [CmdletBinding(
        SupportsShouldProcess = $true
    )]
    param()

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Starts containerd service")) {
            Invoke-ServiceAction -Service 'Containerd' -Action 'Start'
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Stop-ContainerdService {
    [CmdletBinding(
        SupportsShouldProcess = $true
    )]
    param()

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Stop containerd service")) {
            Invoke-ServiceAction -Service 'Containerd' -Action 'Stop'
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Register-ContainerdService {
    [CmdletBinding(
        SupportsShouldProcess = $true
    )]
    param(
        [parameter(HelpMessage = "Containerd path")]
        [String]$ContainerdPath,

        [parameter(HelpMessage = "Specify to start Containerd service after registration is complete")]
        [Switch]$Start,

        [parameter(HelpMessage = "Bypass confirmation to register containerd service")]
        [Switch]$Force
    )

    begin {
        if (!$ContainerdPath) {
            $ContainerdPath = Get-DefaultInstallPath -Tool "containerd"
        }

        $containerdExecutable = "$ContainerdPath\bin\containerd.exe"

        $WhatIfMessage = 'Registers containerd service.'
        if ($Start) {
            $WhatIfMessage = "Registers and starts containerd service."
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $WhatIfMessage)) {
            if (Test-EmptyDirectory -Path $ContainerdPath) {
                Throw "Containerd does not exist at $ContainerdPath or the directory is empty"
            }

            # Check containerd service is already registered
            if (Test-ServiceRegistered -Service 'containerd') {
                Write-Warning ( -join @("Containerd service already registered. To re-register the service, "
                        "stop the service by running 'Stop-Service containerd' or 'Stop-ContainerdService', then "
                        "run 'containerd --unregister-service'. Wait for containerd service to be deregistered, "
                        "then re-reun this command."))
                return
            }

            $consent = $force
            if (!$ENV:PESTER) {
                $consent = $force -or $PSCmdlet.ShouldContinue('', "Are you sure you want to register containerd service?")
            }

            if (!$consent) {
                Write-Error "containerd service registration cancelled."
                return
            }

            Write-Output "Configuring containerd service"

            Add-MpPreference -ExclusionProcess $containerdExecutable

            # Get default containerd config and write to file
            $containerdConfigFile = "$ContainerdPath\config.toml"
            Write-Debug "Containerd config file: $containerdConfigFile"

            $output = Invoke-ExecutableCommand -Executable $containerdExecutable -Arguments "config default"
            $output.StandardOutput.ReadToEnd() | Out-File -FilePath $containerdConfigFile -Encoding ascii -Force

            # Check config file is not empty
            $isEmptyConfig = Test-ConfFileEmpty -Path  "$containerdConfigFile"
            if ($isEmptyConfig) {
                Throw "Config file is empty. '$containerdConfigFile'"
            }

            Write-Output "Review containerd configutations at $containerdConfigFile"

            # Register containerd service
            $output = Invoke-ExecutableCommand -Executable $containerdExecutable -Arguments "--register-service --log-level debug --service-name containerd --log-file `"$env:TEMP\containerd.log`""
            if ($output.ExitCode -ne 0) {
                Throw "Failed to register containerd service. $($output.StandardError.ReadToEnd())"
            }

            $containerdService = Get-Service -Name containerd -ErrorAction SilentlyContinue
            if ($null -eq $containerdService ) {
                Throw "Failed to register containerd service. $($Error[0].Exception.Message)"
            }

            Set-Service containerd -StartupType Automatic
            Write-Output "Successfully registered Containerd service."

            if ($Start) {
                Start-ContainerdService
                Write-Output "Successfully started Containerd service."
            }
            else {
                Write-Information -InformationAction Continue -MessageData "To start containerd service, run 'Start-Service containerd' or 'Start-ContainerdService'"
            }

            Write-Debug $(Get-Service 'containerd' -ErrorAction SilentlyContinue | Format-Table -AutoSize | Out-String)
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Uninstall-Containerd {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    param(
        [parameter(HelpMessage = "Containerd path")]
        [String]$Path,

        [parameter(HelpMessage = "Bypass confirmation to uninstall Containerd")]
        [Switch] $Force
    )

    begin {
        $tool = 'Containerd'
        if (!$Path) {
            $Path = Get-DefaultInstallPath -Tool $tool
        }

        $WhatIfMessage = "Containerd will be uninstalled from $path and containerd service will be stopped and unregistered"
    }

    process {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $WhatIfMessage)) {
            if (Test-EmptyDirectory -Path $path) {
                Write-Output "$tool does not exist at $Path or the directory is empty"
                return
            }

            $consent = $force
            if (!$ENV:PESTER) {
                $consent = $force -or $PSCmdlet.ShouldContinue($Path, 'Are you sure you want to uninstall Containerd?')
            }

            if (!$consent) {
                Throw "$tool uninstallation cancelled."
            }

            Write-Warning "Uninstalling preinstalled $tool at the path $path"
            try {
                Uninstall-ContainerdHelper -Path $path
            }
            catch {
                Throw "Could not uninstall $tool. $_"
            }
        }
        else {
            # Code that should be processed if doing a WhatIf operation
            # Must NOT change anything outside of the function / script
            return
        }
    }
}

function Uninstall-ContainerdHelper {
    param(
        [ValidateNotNullOrEmpty()]
        [parameter(Mandatory = $true, HelpMessage = "Containerd path")]
        [String]$Path
    )

    if (Test-EmptyDirectory -Path $Path) {
        Write-Error "Containerd does not exist at $Path or the directory is empty."
        return
    }

    try {
        if (Test-ServiceRegistered -Service 'containerd') {
            Stop-ContainerdService
            Unregister-Containerd -ContainerdPath $Path
        }
    }
    catch {
        Throw "Could not stop or unregister containerd service. $_"
    }

    # Delete the containerd key
    Remove-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Services\containerd" -Recurse -Force -ErrorAction Ignore

    # Remove the folder where containerd is installed and related folders
    Remove-Item -Path $Path -Recurse -Force

    # Delete containerd programdata
    Uninstall-ProgramFiles "$ENV:ProgramData\Containerd"

    # Remove from env path
    Remove-FeatureFromPath -Feature "containerd"

    Write-Output "Successfully uninstalled Containerd."
}

function Unregister-Containerd ($containerdPath) {
    if (!(Test-ServiceRegistered -Service 'Containerd')) {
        Write-Warning "Containerd service does not exist as an installed service."
        return
    }

    # Unregister containerd service
    $containerdExecutable = "$ContainerdPath\bin\containerd.exe"
    $output = Invoke-ExecutableCommand -Executable $containerdExecutable -Arguments "--unregister-service"
    if ($output.ExitCode -ne 0) {
        Throw "Could not unregister containerd service. $($output.StandardError.ReadToEnd())"
    }
    else {
        # Wait for service to be unregistered
        # Failure to wait causes "The specified service has been marked for deletion." error
        Start-Sleep -Seconds 15
    }
}


Export-ModuleMember -Function Get-ContainerdLatestVersion
Export-ModuleMember -Function Install-Containerd
Export-ModuleMember -Function Start-ContainerdService -Alias Start-Containerd
Export-ModuleMember -Function Stop-ContainerdService -Alias Stop-Containerd
Export-ModuleMember -Function Register-ContainerdService
Export-ModuleMember -Function Uninstall-Containerd, Uninstall-ContainerdHelper

# SIG # Begin signature block
# MIIoUwYJKoZIhvcNAQcCoIIoRDCCKEACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCsY9/dRZnhgtF6
# 95DeapV/AA5zdbR3im9cSxHs+oZVrqCCDZowggYYMIIEAKADAgECAhMzAAAD87lq
# ZLvv9+1jAAAAAAPzMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwNzE3MjEwMjM0WhcNMjUwOTE1MjEwMjM0WjCBiDEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWlj
# cm9zb2Z0IDNyZCBQYXJ0eSBBcHBsaWNhdGlvbiBDb21wb25lbnQwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4lnJhNSR/+M4vb7CMAivTMYSYEi0EnHXS
# SDat/9BwPD2i0F1hj7W80W8dEulv51fz1NAILKbeIp2JaS777mffYHfxMUw/BcQs
# yzW6J1e5xUPNguGU2GFXrc5s+jyK39AIpE6ATZecjMDuAu4P1nVEcZKDR0fBbP7Q
# 8CFe13PgnZi3n/4UXbwaElvFkrwia4wh88gJ+7lDFkZUOI1H1xxENgP9y2LlCifr
# HOahAXcY+TaSyeZqv+E0jV5SkBq8zNFCeWyMbooJPPsSG+LQEfS2sO4xNrNHZMlJ
# iziV79h12qxLicPeswugNdD+aBXqF0fwoovPuvSwaKT/wM8okNMxAgMBAAGjggGC
# MIIBfjAfBgNVHSUEGDAWBgorBgEEAYI3TBEBBggrBgEFBQcDAzAdBgNVHQ4EFgQU
# 1Rdsgw/dRz6oCuO8sKNsrodmEw8wVAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1p
# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMx
# NTIyKzUwMjUyMTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNV
# HR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Ny
# bC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUw
# UzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# ZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQC
# MAAwDQYJKoZIhvcNAQELBQADggIBAGNFKGiwTGNU85l+83tJhB6N2BPvaT9TAkuE
# 7uUniu1VU6b1/erckmXiuVqBLg2sGzE7O122bK871fNWpiEr9/pMflah1PL/+0D6
# XCodWS41UmWILRUhSqKURZ+iceq5n4EUiKcapBKhZ6uCKXSX62CVHlvNv4N6JA7N
# UztE0h3jAEQeLoGSFlfNqvkTOobE5NwlMRSJWhQ6SYnswiTY1QU06pSTaP/eVedL
# Rk6q3t/U9ibvVwHT+eTw1ASBbZoZOxWA3FjrK9XisOT/GoyV2w9D3gzKFGOElCFp
# RvceGeRKy2N3YSRjjgV9VYlUM5G/cz4ZigACtnd4jfE7ebbKmiDv2UB1IUpxSzS4
# TAO1bkANbwJ/frfSikUPGfZS2fRRXMWZTeQL5bTBcmW7INNTIxM2MxF6QqlLtmkJ
# Z3iv9cocwQ1MzWrrd1BU/ipLwgGtbw7mCestXZjP2amFeQjucAtKt9PkENnWfUex
# rwQZnZJWF3mCVvJTh6ACU0CJyO0a2a+RiuxsvR8QpMw1gE3744SbM75nABu84/zd
# kxfRkiXro5IsUJ+eNVXVKElvFngBzpbYtTVf1Pdcs8y0sEo6FxGQkzwKNLHe28tG
# 1H3NTHUNa0LQUKRjMq/58Iav9qq4rW1QIzfXAEnBDSBFv1E4vLmx2LghqMRPyTAB
# ZurrCCn1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCB
# iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp
# TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEw
# NzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5n
# IFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAc
# Lq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDI
# OdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXp
# ZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t
# 00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD
# 2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVO
# VpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWY
# OUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0P
# UUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF
# 78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhf
# si+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCV
# mj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQB
# gjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEE
# AYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
# /zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+g
# TaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9N
# aWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBO
# BggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9N
# aWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEG
# CSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAd
# AEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAd
# MA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5
# DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzS
# Gksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9Msm
# AkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXv
# biWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ
# 2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8Cz
# TfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNb
# B5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85el
# CUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4
# GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFci
# oXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMT
# DXpQzTGCGg8wghoLAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIw
# MTECEzMAAAPzuWpku+/37WMAAAAAA/MwDQYJYIZIAWUDBAIBBQCggbAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEIGNehm1CCBthbhtWgZlwZ5tpNSYlNvSiFjBtdyA0
# lznsMEQGCisGAQQBgjcCAQwxNjA0oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEcgBpo
# dHRwczovL3d3dy5taWNyb3NvZnQuY29tIDANBgkqhkiG9w0BAQEFAASCAQBA4F1+
# teAeyimWQxfELpKioaCkBy2LMSNr7FPin10dl4oO/KeDW6AmVBkPVS98IkMmZjUJ
# fWduIXQx099lw9fODKzfrJIbMqmXFBhx2mmZ6kx5F4EivdRZpKZI1fQwFEI6Tho4
# sdaiVCNqMB6rl9OSO9DBh4/+NaONYw8wtJSinx9fJVw9jTt7/7xM21+U7P0wyGK1
# EuwbpTh2uujtikCahareWyYqzuT1z9jiM2CxiHM501RKcVovH7dNiim1jVC7AZl8
# 7IjunVfQEnevWNVJ422x0DIxzmsCr/Ui2OANi3rtlDGwA5+yop0dCYGZi3CreZLt
# y06cheET9iywFjnroYIXlzCCF5MGCisGAQQBgjcDAwExgheDMIIXfwYJKoZIhvcN
# AQcCoIIXcDCCF2wCAQMxDzANBglghkgBZQMEAgEFADCCAVIGCyqGSIb3DQEJEAEE
# oIIBQQSCAT0wggE5AgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIKQZ
# 1MEht/f5rfdKTfwBGovQFhqRiwzjC3SBToraVMQxAgZoGMRdOiMYEzIwMjUwNTE1
# MTYxOTUzLjY1OVowBIACAfSggdGkgc4wgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJh
# dGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjpGMDAyLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEe0wggcgMIIF
# CKADAgECAhMzAAACBTx1bIJEh83+AAEAAAIFMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDEzMDE5NDI0OVoXDTI2MDQy
# MjE5NDI0OVowgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsT
# Hm5TaGllbGQgVFNTIEVTTjpGMDAyLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAJKS8t93uAXWvbAZ3LzkIVhcdQLzLATIPgtEu/RZgRd55nS7li4runZx
# dXNCZk84dM3xNaobZI+VRv7s+V3MUMMCVHe9TymI6OaG72sdbjczZ1uiv2OX2CW6
# HPBR3ZJyJUZrt/23ru0zcoUpFIxcW0coXcrAEHtpWj5vWrLmn0NaWjY3kUasGocw
# RWU3LOt4digMsv6bx9Kyoy7+JhSrHMrkXhLshjk16YAHwH4DdCDBUiLrgokh94pl
# R6JYoJN/ih1SA/cBCKXGHjw/rPsBPggDR0wS0qFgsWzhb8o0MyxivsqlA8pUJwLk
# Ty4Md0p7C5vLN6eRPHLh9/U3eDzKGjk+L0F+NHRXK2uSakN96nwk/BxvgE04hc6j
# Wl90dnwS+dHskVkVCKqkxkWU7kIC5Ngfy6Nzk9QeVowAnw0Rr1MUlM5IGsHs9GB6
# H/o0nbG73LE+H+RU30Eayz3cwLpSOmjF4zjHvRvBvCIrxI0cg7wPxyqXtVJ69Rhu
# M3g2iAUXCEEKWGh0T/N4Y+rrLqLEPjPrkgdjfPAVBsFVf/D4v9Uc2f8EwazY8Yee
# VGM78qTw0ik3iyGQVCoDV8zTx+usNI0Rj1UoO2mxSkAXnVjWhDq0mFYzV3ed4JeH
# pv/o35d4c5ELCdjzcr6kUwyGDyKxdBvopGXrmSDxdgF3gnCRsqhVAgMBAAGjggFJ
# MIIBRTAdBgNVHQ4EFgQUdHpauzbtvr7IndsRn2jk10vVsvAwHwYDVR0jBBgwFoAU
# n6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFt
# cCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcw
# AoZQaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3Nv
# ZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIw
# ADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZI
# hvcNAQELBQADggIBAA67GTjtvWLvlXzVPHrGjXTE0ivjNnFhV+QXlMWraSd08eDN
# XIueyzD8cqQRlEKBlWmQoQnpjiO8bm26AyL5aO3uFQxKKmT5GkHmAVC+HXGYAQvZ
# +V6DNNYSyePTCsRKmUjlne+B/Z4ZcCv9FyoaNKmi4dsPYdj1jcXQ6XoVEMJX8cQY
# pEfOfYzmtCkUZKNpxPgOSpViZ6b8Cs59K9WiOcoQhb3XhTEa26ElKv2M6jlGpNfs
# Yipu283vOFaV36LdfF2F01x+VDP1iGgWpB7EF6eghGAA8C3AfrzFzOv0swLeX0As
# Smey87RGIo9cXiXbb859wV99qmGeX9MPCSIl/E7IAx9QXfEj37eLNPVZfYIWzZFo
# 4Crd1yiHInD7FEbQTzCQIeqRXnsFtQETMtfwv2UYnUOjFg2mgNfuJDMn1B2TKmLl
# +/vvcTcKHwD62jF4WnoWJ9BnIJzwhwgGUfJqToxtPphNVA2BD+HwUX5Lk6o/sIQI
# W8gfYq3Y/QaU670LQF9qyGdOOh2a1kVmym1S+KuT/Yc4suMGIyMPAmxAAUDMm7Ph
# m1PKiuu8RHXQaX+ZuZWIeqG80AJ9PM+It+MODK+n2zv9se78JqUqZ6IlQsaOH+XA
# CPnfX2mCCwYmpuEngVsVz6hTnN99ub+sqNVnyTE0PeKbXRCnjWfa3+qnI/oRMIIH
# cTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCB
# iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp
# TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEw
# OTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIh
# C3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNx
# WuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFc
# UTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAc
# nVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUo
# veO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyzi
# YrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9
# fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdH
# GO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7X
# KHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiE
# R9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/
# eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3
# FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAd
# BgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEE
# AYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMI
# MBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMB
# Af8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1Ud
# HwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By
# b2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQRO
# MEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2Vy
# dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4IC
# AQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pk
# bHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gng
# ugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3
# lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHC
# gRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6
# MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEU
# BHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvsh
# VGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+
# fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrp
# NPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHI
# qzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCA1AwggI4AgEBMIH5
# oYHRpIHOMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUw
# IwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5u
# U2hpZWxkIFRTUyBFU046RjAwMi0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVANWwf2nW0mf6SvAI
# y+o6FW9ett60oIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAw
# DQYJKoZIhvcNAQELBQACBQDr0HENMCIYDzIwMjUwNTE1MTM1NjI5WhgPMjAyNTA1
# MTYxMzU2MjlaMHcwPQYKKwYBBAGEWQoEATEvMC0wCgIFAOvQcQ0CAQAwCgIBAAIC
# NTsCAf8wBwIBAAICEr0wCgIFAOvRwo0CAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYK
# KwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsF
# AAOCAQEATqn652kJESmVOIUwDdRrca/2TEY4bKEPKxHzAzZQ3zZ2MTFjqaalXQut
# lnGFCYOhJHgaimEbH5dcOrBNpMucWQ/qQj8uq7mHq3WqOtn2Mbb6C3dbbnphWnVj
# KIXIMg1VY/d+NfQijeo3oaWV8WqkWq0TBtybqYu6lg2s9/Kx+lF3t1zF+KQg7c9q
# um8ZGLmU1sau4/faGjEiXyEAAQDOqX/F0bfVh4M3HmDvFemk0T8dy3ut6TV3vp4p
# hPHADUjP7yURahv1DVDs8F+50Iqo+mJbce5uENj9seoVdLUY1jbehvXyV2dnv3tE
# zngGsSmIeDN2eAM+ro9DPC6aSZjm6TGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACBTx1bIJEh83+AAEAAAIFMA0GCWCGSAFl
# AwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcN
# AQkEMSIEIJiGVQdrvYq9I4f5FYmysTvTuDc84zNgTwM8DXu3TXPGMIH6BgsqhkiG
# 9w0BCRACLzGB6jCB5zCB5DCBvQQggA0DPdx5jS6aF1YtHawmmrQ4+q0kNMBhGaMd
# WTARb0gwgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAA
# AgU8dWyCRIfN/gABAAACBTAiBCDACpw6tCWJAvq6dh5nXkZsLIVTmztOC3UL6pPp
# MbJAHzANBgkqhkiG9w0BAQsFAASCAgAPmMJe/aWKkqvJ2O+SVFRzux2VsAbOHuef
# r5wR9y0Oem1RJU+ipvWOYTp1lGgikynbd4PhNt898Km+XSjtNP2jIriS5/Scg890
# BnRrk3jNoesN7eCpwCzIjuJop/j6d3RoeEnvgN9groFo8Uci7+Vagg22Bq5KMOr0
# vdmpJWGdQ5fgu+7zPE6laJk6AW6WZPqTCUfpnJTxp4B9Kjj8vMZD3sOoAZNtBQ12
# 2TmrUGAWGCJ5fsv9KVkWmOkAY71jmWtHl9Z3yPdXqL3lL85a7+iSAsp7yZeYNFGP
# CxvroepJv70my1tyQ8Qu7wbCTD63DJZRfqLMbxtThmbUkqB5NROATkTgYoOMzSi7
# UidNbFIVEc53esq5zG0y62JqE+69Npv8cC8oiNbdAzGyyfdAJfQQ+f8vQu9SX/tB
# te+pUPK014BeFdh6tG0ABkdxdzI9x13MomDH3otPf7G2ZcGRpO16tz8iXNWYD7PB
# D6Gtq0kXEZEqjdtxv878X52s6kWY7fr2sVkZnlhHc0+KS49hxiBfObKamFlG1KvM
# 8/vB43nRzMvGtnBUMZR00hssUnpVFTgGQIzWaXK3v4J2qp+KqnkDJbYajtIjQ5NJ
# ghsQjPfSH/kizTAcQCIMDds8gYjTENstZMHbv67U1YZ1jyIouYNCyxjytl3lzkOx
# NUwTwOPO5A==
# SIG # End signature block