Kva.psm1

#########################################################################################
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# Kva Module
#
#########################################################################################

#requires -runasadministrator
using module .\Common.psm1

#region Module Constants

$moduleName       = "Kva"
$moduleVersion    = "1.0.0"

#endregion

#region Download catalog constants

# Defaults until KVA has it's own catalog
$catalogName = "aks-hci-stable-catalogs-ext"
$ringName    = "stable"
$productName = "kva"

$productInfoMapName      = "cloudop-product-information"
$productInfoMapNamespace = "cloudop-system"

#endregion

#region Script Constants

$defaultContainerRegistryServer = "ecpacr.azurecr.io"
$defaultContainerRegistryU      = "1516df5a-f1cc-4a6a-856c-03d127b02d05"
$defaultContainerRegistryP      = "92684690-48b5-4dce-856d-ef4cccb54f22"

$logFilePath = $($env:ProgramData + "\kva\kva.log")
$kubeconfigMgmtFile = "kubeconfig-mgmt"
$script:defaultTokenExpiryDays = 60

$kvaBinaries = @(
    $global:kvactlBinary,
    $global:kubectlBinary
)

$kvaBinariesMap = @{
    $global:kvactlBinary = $global:kvaCtlFullPath;
    $global:kubectlBinary = $global:kubeCtlFullPath;
}

if (!$global:config) {
    $global:config = @{}
}

# azure
$global:azureCloud = "AzureCloud"
$global:azureChinaCloud = "AzureChinaCloud"
$global:azureUSGovernment = "AzureUSGovernment"
$global:azureGermanCloud = "AzureGermanCloud"
$global:azurePPE = "AzurePPE"

$global:graphEndpointResourceIdAzureCloud = "https://graph.windows.net/"
$global:graphEndpointResourceIdAzurePPE = "https://graph.ppe.windows.net/"
$global:graphEndpointResourceIdAzureChinaCloud = "https://graph.chinacloudapi.cn/"
$global:graphEndpointResourceIdAzureUSGovernment = "https://graph.windows.net/"
$global:graphEndpointResourceIdAzureGermancloud = "https://graph.cloudapi.de/"

#endregion

#region
# Install Event Log
New-ModuleEventLog -moduleName $moduleName
#endregion

#region Private Function

function Initialize-KvaConfiguration
{
    <#
    .DESCRIPTION
        Initialize Kva Configuration.
        Wipes off any existing cached configuration
    #>

    if ($global:config.ContainsKey($moduleName)) {
        $global:config.Remove($moduleName)
    }
    $global:config += @{
        $moduleName = @{
            "cloudAgentAuthorizerPort" = 0
            "cloudAgentPort"           = 0
            "cloudLocation"           = ""
            "controlplaneVmSize"      = [VmSize]::Default
            "dnsservers"              = ""
            "gateway"                 = ""
            "group"                   = ""
            "imageDir"                = ""
            "insecure"                = $false
            "installationPackageDir"  = ""
            "installState"            = [InstallState]::NotInstalled
            "ipaddressprefix"         = ""
            "k8snodeippoolstart"      = ""
            "k8snodeippoolend"        = ""
            "kubeconfig"              = ""
            "kvaconfig"               = ""
            "kvaK8sVersion"           = ""
            "kvaName"                 = ""
            "kvaPodCidr"              = ""
            "macPoolEnd"              = ""
            "macpoolname"             = ""
            "macPoolStart"            = ""
            "manifestCache"           = [io.path]::GetTempFileName()
            "moduleVersion"           = $moduleVersion
            "proxyServerCertFile"     = ""
            "proxyServerHTTP"         = ""
            "proxyServerHTTPS"        = ""
            "proxyServerNoProxy"      = ""
            "proxyServerPassword"     = ""
            "proxyServerUsername"     = ""
            "skipUpdates"             = $false
            "stagingShare"            = ""
            "tokenExpiryDays"         = 0
            "containerRegistryServer" = ""
            "containerRegistryUser"   = ""
            "containerRegistryPass"   = ""
            "useStagingShare"         = $false
            "version"                 = ""
            "vlanid"                  = 0
            "vnetName"                = ""
            "vswitchName"             = ""
            "vnetvippoolend"          = ""
            "vnetvippoolstart"        = ""
            "workingDir"              = ""
            "catalog"                 = ""
            "ring"                    = ""
            "identity"                = ""
            "operatorTokenValidity"   = 0
            "addonTokenValidity"      = 0
        };
    }
}

#endregion

#region global config

Initialize-KvaConfiguration
#endregion

#region Exported Functions

function Install-Kva
{
    <#
    .DESCRIPTION
        Uses KVACTL to deploy a management cluster.

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -createConfigIfNotPresent -activity $activity

    $curState =  Get-ConfigurationValue -module $moduleName -type ([Type][InstallState]) -name "installState"
    if ($curState) {
        switch ($curState) {
            ([InstallState]::Installed) {
                Write-Status -moduleName $moduleName  "KVA is already be installed"
                Write-SubStatus -moduleName $moduleName  "Please use Restart-Kva to reinstall or Uninstall-Kva to uninstall."
                return
            }
            ([InstallState]::Installing) {
                Write-Status -moduleName $moduleName  "Kva is currently being installed. If thats not the case, please run Uninstall-Kva and try again"
                return
                break
            }
            ([InstallState]::NotInstalled) {
                # Fresh install
                break
            }
            Default {
                # Cleanup partial installs from previous attempts
                Uninstall-Kva -activity $activity
            }
        }
    }

    try
    {
        Install-KvaInternal -activity $activity
    }
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Error -eventId 100 -message "$activity - $_"
        Uninstall-Kva -SkipConfigCleanup:$True -activity $activity
        throw $_
    }


    Write-Status -moduleName $moduleName  "Done."
}

function Restart-Kva
{
    <#
    .DESCRIPTION
        Cleans up an existing KVA deployment and reinstalls everything. This isn't equivalent to
        executing 'Uninstall-Kva' followed by 'Install-Kva' as Restart-Kva will preserve existing
        configuration settings and any downloaded images.

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    # Skip the config cleanup to reinstall
    Uninstall-Kva -SkipConfigCleanup:$True -activity $activity

    Install-KvaInternal -activity $activity

    Write-Status -moduleName $moduleName  "Done."
}

function Uninstall-Kva
{
    <#
    .DESCRIPTION
        Removes a KVA deployment.

    .PARAMETER SkipConfigCleanup
        skips removal of the configurations after uninstall.
        After uninstall, you have to Set-KvaConfig to install again.

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [Switch]$SkipConfigCleanup,
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    try
    {
        Initialize-KvaEnvironment -activity $activity
    }
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Warning -eventId 2 -message "$activity - $_"
    }

    Set-KvaConfigValue -name "installState" -value ([InstallState]::Uninstalling)

    try
    {
        # Do not use Get-KvaConfigYaml, which can generate one, if not already there.
        if (Test-Path $kvaCtlFullPath)
        {
            $yamlFile = $global:config[$modulename]["kvaconfig"]
            if ($yamlFile -and (Test-Path $yamlFile))
            {
                $kvaRegistration = Get-KvaRegistration

                try 
                {
                    Test-KvaAzureConnection

                    # If Test-KvaAzureConnection worked, do the cleanup for the connectedcluster azure resource
                    Invoke-KvaCtlWithAzureContext -arguments "delete --configfile $yamlFile" -showOutputAsProgress -activity $activity
                }
                catch
                {
                    Write-Status -moduleName $moduleName -Verbose -msg "Uninstalling KVA without Azure Connection may result in leaked Arc Connected Clusters, Please clean up resources in portal."
                    Invoke-KvaCtl -arguments "delete --configfile $yamlFile" -showOutput -activity $activity
                }
            }
        }
    }
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Error -eventId 100 -message "$activity - $_"
    }
    try
    {
        $kubeconfig = $global:config[$moduleName]["kubeconfig"]
        if ($kubeconfig -and (Test-Path $kubeconfig))
        {
            Remove-Item $kubeconfig -ErrorAction SilentlyContinue
        }

        $kubernetesVersion = $global:config[$modulename]["kvaK8sVersion"]
        if ($kubernetesVersion)
        {
            $imageGalleryName = Get-KubernetesGalleryImageName -imagetype "Linux" -k8sVersion $kubernetesVersion

            try {
                Remove-MocGalleryImage -name $imageGalleryName -location $global:config[$modulename]["cloudLocation"] | Out-Null
            } catch {
                if (-not ($_.Exception.Message -like "*connection closed*")) {
                    Write-SubStatus -moduleName $moduleName  $("Warning: " + $_.Exception.Message)
                }
            }
        }

        # 1. Update the binaries
        if (Test-MultiNodeDeployment)
        {
            Get-ClusterNode -ErrorAction Continue | ForEach-Object {
                Uninstall-KvaBinaries -nodeName $_.Name
            }
        }
        else
        {
            Uninstall-KvaBinaries -nodeName ($env:computername)
        }
        # 2. Remove KVA Identity
        try {
            $clusterName = $($global:config[$modulename]["kvaName"])
            Remove-MocIdentity -name $clusterName  | Out-Null
        } catch {
            if (-not ($_.Exception.Message -like "*connection closed*")) {
                Write-SubStatus -moduleName $moduleName  $("Warning: " + $_.Exception.Message)
            }
        }
        # Clean CloudConfig
        Remove-Item -Path $global:kvaMetadataDirector -Force -Recurse -ErrorAction SilentlyContinue
    }
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Error -eventId 100 -message "$activity - $_"
    }
    Set-KvaConfigValue -name "installState" -value ([InstallState]::NotInstalled)
    if (!$SkipConfigCleanup.IsPresent)
    {
        Reset-Configuration -moduleName $moduleName
    }
    Write-Status -moduleName $moduleName  "Done."
}

function New-KvaCluster
{
    <#
    .DESCRIPTION
        Adds a worker cluster to the deployment.

    .PARAMETER Name
        Name of the cluster

    .PARAMETER kubernetesVersion
        Version of kubernetes to deploy

    .PARAMETER controlPlaneNodeCount
        The number of control plane (master) nodes

    .PARAMETER linuxNodeCount
        The number of Linux worker nodes

    .PARAMETER windowsNodeCount
        The number of Windows worker nodes

    .PARAMETER controlplaneVmSize
        The VM size to use for control plane nodes

    .PARAMETER loadBalancerVmSize
        The VM size to use for the cluster load balancer

    .PARAMETER linuxNodeVmSize
        The VM size to use for Linux worker nodes

    .PARAMETER windowsNodeVmSize
        The VM size to use for Windows worker nodes

    .PARAMETER enableADAuth
        Whether the call should or not setup Kubernetes for AD Auth

    .PARAMETER vnet
        The virtual network to use for the cluster. If not specified, the virtual network
        of the management cluster will be used

    .PARAMETER activity
        Activity name to use when writing progress

    .PARAMETER primaryNetworkPlugin
        Network plugin (CNI) definition. Simple string values can be passed to this parameter such as "flannel", or "calico". Defaults to "calico".
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter()]
        [String] $kubernetesVersion = $global:defaultTargetK8Version,

        [Parameter()]
        [ValidateSet(1,3,5)]
        [int] $controlPlaneNodeCount = 1,

        [Parameter()]
        [ValidateRange(0,250)]
        [int] $linuxNodeCount = 1,

        [Parameter()]
        [ValidateRange(0,250)]
        [int] $windowsNodeCount = 0,

        [Parameter()]
        [String] $controlplaneVmSize = $global:defaultControlPlaneVmSize,

        [Parameter()]
        [String] $loadBalancerVmSize = $global:defaultLoadBalancerVmSize,

        [Parameter()]
        [String] $linuxNodeVmSize = $global:defaultWorkerVmSize,

        [Parameter()]
        [String] $windowsNodeVmSize = $global:defaultWorkerVmSize,

        [Parameter()]
        [Switch] $enableADAuth,

        [Parameter()]
        [String] $activity,

        [Parameter()]
        [ValidateScript({return $true})]#Note: ValidateScript automatically constructs the NetworkPlugin object, therefore validates the parameter
        [NetworkPlugin] $primaryNetworkPlugin = [NetworkPlugin]::new(),

        [Parameter()]
        [VirtualNetwork]$vnet
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Validating cluster configuration..."
    Test-ValidClusterName -Name $Name | Out-Null

    $capiCluster = Invoke-Kubectl -arguments $("get akshciclusters/$Name") -ignoreError
    if ($null -ne $capiCluster)
    {
        throw $("The specified cluster name '$Name' already exists.")
    }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Ensure MOC is available..."
    $isCAAvailable = Wait-ForCloudAgentEndpoint -activity $activity
    if (-not $isCAAvailable)
    {
        throw $("MOC is unreachable")
    }

    $group = $("$global:cloudGroupPrefix-$Name")
    New-KvaClusterInternal -Name $Name -group $group -kubernetesVersion $kubernetesVersion `
        -controlPlaneReplicas $controlPlaneNodeCount -linuxWorkerReplicas $linuxNodeCount `
        -windowsWorkerReplicas $windowsNodeCount -controlplaneVmSize $controlplaneVmSize `
        -loadBalancerVmSize $loadBalancerVmSize -linuxNodeVmSize $linuxNodeVmSize `
        -windowsNodeVmSize $windowsNodeVmSize -enableADAuth:$enableADAuth.IsPresent `
        -activity $activity -primaryNetworkPlugin $primaryNetworkPlugin -vnet $vnet

    Write-Status -moduleName $moduleName  "Done."
}

function New-KvaClusterInternal
{
    <#
    .DESCRIPTION
        Internal function to call KVACTL to create a capicluster.

    .PARAMETER Name
        Name of the cluster

    .PARAMETER group
        Cloudagent group

    .PARAMETER kubernetesVersion
        Version of kubernetes to deploy

    .PARAMETER controlPlaneReplicas
        The number of control plane (master) replicas

    .PARAMETER linuxWorkerReplicas
        The number of Linux worker replicas

    .PARAMETER windowsWorkerReplicas
        The number of Windows worker replicas

    .PARAMETER controlplaneVmSize
        The VM size to use for control plane nodes

    .PARAMETER loadBalancerVmSize
        The VM size to use for the cluster load balancer

    .PARAMETER linuxNodeVmSize
        The VM size to use for Linux worker nodes

    .PARAMETER windowsNodeVmSize
        The VM size to use for Windows worker nodes

    .PARAMETER enableADAuth
        Whether the call should or not setup Kubernetes for AD Auth

    .PARAMETER vnet
        The virtual network to use for the cluster. If not specified, the virtual network
        of the management cluster will be used

    .PARAMETER activity
        Activity name to use when writing progress

    .PARAMETER primaryNetworkPlugin
        Network plugin (CNI) definition. Simple string values can be passed to this parameter such as "flannel", or "calico". Defaults to "calico".
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]$Name,

        [Parameter(Mandatory=$true)]
        [String]$group,

        [Parameter(Mandatory=$true)]
        [String]$kubernetesVersion,

        [Parameter(Mandatory=$true)]
        [int] $controlPlaneReplicas,

        [Parameter(Mandatory=$true)]
        [int] $linuxWorkerReplicas,

        [Parameter(Mandatory=$true)]
        [int] $windowsWorkerReplicas,

        [Parameter(Mandatory=$true)]
        [VmSize] $controlplaneVmSize,

        [Parameter(Mandatory=$true)]
        [VmSize] $loadBalancerVmSize,

        [Parameter(Mandatory=$true)]
        [VmSize] $linuxNodeVmSize,

        [Parameter(Mandatory=$true)]
        [VmSize] $windowsNodeVmSize,

        [Parameter()]
        [Switch]$enableADAuth,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name,

        [Parameter()]
        [NetworkPlugin] $primaryNetworkPlugin = [NetworkPlugin]::new(),

        [Parameter()]
        [VirtualNetwork]$vnet
    )

    Add-GalleryImage -imageType Linux -k8sVersion $kubernetesVersion -activity $activity

    if ($windowsWorkerReplicas -gt 0)
    {
        Add-GalleryImage -imageType Windows -k8sVersion $kubernetesVersion -activity $activity
    }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $("Creating workload cluster '$Name'")

    $publicKey = Get-Content -Path (Get-SshPublicKey)
    $publicKey = $publicKey.Split(" ")
    $publicKey = $($publicKey[0]+" "+$publicKey[1])

    if (-not $vnet) {
        $vnet = Get-VNetConfiguration -module $moduleName
    }

    $yaml = @"
name: $Name
version: $kubernetesVersion
controlplane:
  nodepool:
    replicas: $controlPlaneReplicas
    azurestackhcinodepool:
      hardwareprofile:
        vmsize: $controlplaneVmSize
      osprofile:
        ostype: Linux
        ssh:
          publickeys:
          - keydata: $publicKey
  servicecidr: $($global:workloadServiceCidr)
  podcidr: $($global:workloadPodCidr)
azurestackhcicluster:
  storageconfiguration:
    storagecontainer: $($global:cloudStorageContainer)
    dynamic: true
  containernetworkconfiguration:
    primaryconfiguration: $($primaryNetworkPlugin.Name)
  loadbalancer:
    vmsize: $loadBalancerVmSize
  location: $($global:config[$modulename]["cloudLocation"])
  group: $group
  virtualnetwork:
    name: "$($vnet.Name)"
"@


$yaml += "
additionalfeatures:
- featurename: secrets-encryption
- featurename: rotate-certificates"


    # TODO add switch to kvactl
    if ($enableADAuth.IsPresent)
    {
        $yaml += "
- featurename: ad-auth-webhook"

    }

    $yamlFile = $($global:config[$modulename]["installationPackageDir"]+"\"+$global:yamlDirectoryName+"\$Name.yaml")
    Set-Content -Path $yamlFile -Value $yaml -ErrorVariable err
    if ($null -ne $err -and $err.count -gt 0)
    {
        throw $err
    }

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster create --clusterconfig $yamlFile --kubeconfig $kubeconfig" -showOutput -activity $activity

    $linuxNodePoolYaml = @"
name: $Name-default-linux-nodepool
replicas: $linuxWorkerReplicas
azurestackhcinodepool:
  hardwareprofile:
    vmsize: $linuxNodeVmSize
  osprofile:
    ostype: Linux
    ssh:
      publickeys:
      - keydata: $publicKey
"@


    $linuxNodePoolYamlFile = $($global:config[$modulename]["installationPackageDir"]+"\"+$global:yamlDirectoryName+"\$Name-default-linux-nodepool.yaml")
    Set-Content -Path $linuxNodePoolYamlFile -Value $linuxNodePoolYaml -ErrorVariable err
    if ($null -ne $err -and $err.count -gt 0)
    {
        throw $err
    }

    Invoke-KvaCtl -arguments "cluster nodepool create --clustername $Name --nodepoolconfig $linuxNodePoolYamlFile --kubeconfig $kubeconfig" -showOutput -activity $activity

    $windowsNodePoolYaml = @"
name: $Name-default-windows-nodepool
replicas: $windowsWorkerReplicas
azurestackhcinodepool:
  hardwareprofile:
    vmsize: $windowsNodeVmSize
  osprofile:
    ostype: Windows
    ssh:
      publickeys:
      - keydata: $publicKey
"@


    $windowsNodePoolYamlFile = $($global:config[$modulename]["installationPackageDir"]+"\"+$global:yamlDirectoryName+"\$Name-default-windows-nodepool.yaml")
    Set-Content -Path $windowsNodePoolYamlFile -Value $windowsNodePoolYaml -ErrorVariable err
    if ($null -ne $err -and $err.count -gt 0)
    {
        throw $err
    }

    Invoke-KvaCtl -arguments "cluster nodepool create --clustername $Name --nodepoolconfig $windowsNodePoolYamlFile --kubeconfig $kubeconfig" -showOutput -activity $activity
}

function Get-Kva
{
    <#
    .DESCRIPTION
        Get the Kva management cluster

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Retrieving KVA configuration file"
    $yamlFile = Get-KvaConfigYaml

    $kubeconfig = Get-KvaCredential -activity $activity
    if (!(Test-Path $kubeconfig))
    {
        # Retrieve the kubeconfig
        Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Retrieving KVA credentials"
        Invoke-KvaCtl -arguments "retrieve --configfile $yamlFile --outfile $kubeconfig" -showOutput -activity $activity
    }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Retrieving KVA deployment status"

    $capiCluster = $null
    $kvaStatus = $null

    try{
        $kvaStatus = Invoke-KvaCtl -arguments "status --configfile $yamlFile --kubeconfig $kubeconfig" -activity $activity | ConvertFrom-Json
    }
    catch {}

    if ($kvaStatus)
    {
        # Verify that the appliance is deployed and reachable before requesting cluster information
        if (($kvaStatus.phase -ine "NotDeployed") -and
            ($kvaStatus.phase -ine "WaitingForAPIServer") -and
            ($kvaStatus.phase -ine "Failed"))
        {
            Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Retrieving management cluster details"
            $capiCluster = Get-KvaCluster -Name $global:config[$moduleName]["kvaName"] -activity $activity
        }
    }

    $status = [PSCustomObject]@{
        CapiCluster = $capiCluster
        KvaStatus = $kvaStatus
    }

    return $status
}

function Get-KvaClusters
{
    <#
    .DESCRIPTION
        Get the all kva clusters

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity
    return Get-TargetCapiClusters
}

function Get-KvaCluster
{
    <#
    .DESCRIPTION
        Validates the requested cluster name and ensures that the cluster exists.
        Returns the cluster object.

    .PARAMETER Name
        Name of the cluster

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]$Name,

        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Write-StatusWithProgress -activity $activity -status "Retrieving cluster..." -moduleName $moduleName
    return Get-KvaClusterInternal -Name $Name
}

function Get-KvaClusterInternal
{
    <#
    .DESCRIPTION
        Validates the requested cluster name and ensures that the cluster exists.
        Returns the cluster object.

    .PARAMETER Name
        Name of the cluster
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$Name
    )

    Test-ValidClusterName -Name $Name | Out-Null
    return Get-CapiCluster -Name $Name
}

function Get-KvaClusterUpgrades
{
    <#
    .DESCRIPTION
        Gets the upgrades available for a kva cluster.

    .PARAMETER Name
        Name of the cluster.

    .PARAMETER activity
        Activity name to use when updating progress.
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    $kubeconfig = Get-KvaCredential -activity $activity
    if (-not (Test-Path $kubeconfig))
    {
        throw $("Unable to proceed due to missing kubeconfig file: $kubeconfig")
    }

    $cmdArgs = "cluster get-upgrades --clustername $Name --kubeconfig $kubeconfig"
    $upgrades = Invoke-KvaCtl -arguments $cmdArgs -activity $activity | ConvertFrom-Json

    return $upgrades
}

function Wait-ForClusterUpgrade
{
    <#
    .DESCRIPTION
        Waits for all nodes of a cluster to be running the specified kubernetes version.

    .PARAMETER kubeconfigFile
        Path to a kubeconfig file for the cluster

    .PARAMETER expectedVersion
        The version that we expect all nodes of the cluster to be running

    .PARAMETER sleepDuration
        Duration to sleep for between attempts
    #>


    param (
        [String] $kubeconfigFile,
        [String] $expectedVersion,
        [int] $sleepDuration=60
    )

    Write-Status $("Monitoring cluster upgrade progress") -moduleName $moduleName

    while ($true)
    {
        $results = @()
        $allNodesUpgraded = $true

        $nodes = (Invoke-Kubectl -ignoreError -kubeconfig $kubeconfigFile -arguments "get nodes --request-timeout=1m -o name") 2>$null
        if (-not ($nodes))
        {
            Write-SubStatus $("Unable to retrieve cluster node information. Will retry in $sleepDuration seconds...`n") -moduleName $moduleName
            Start-Sleep $sleepDuration
            continue
        }

        foreach ($node in $nodes)
        {
            $nodeJson = (Invoke-Kubectl -ignoreError -kubeconfig $kubeconfigFile -arguments $("get $node --request-timeout=1m -o json") | ConvertFrom-Json) 2>$null
            if ($nodeJson)
            {
                $result = New-Object -TypeName PsObject -Property @{Name = $($nodeJson.metadata.name); Version = $($nodeJson.status.nodeInfo.kubeletVersion); Upgraded = $true}

                if ($nodeJson.status.nodeInfo.kubeletVersion -ine $expectedVersion)
                {
                    $result.upgraded = $false
                    $allNodesUpgraded = $false
                }

                $results += $result
            }
            else
            {
                $allNodesUpgraded = $false
            }
        }

        Write-SubStatus $("Current status of cluster nodes:`n") -moduleName $moduleName
        $results | Format-Table *

        if ($allNodesUpgraded)
        {
            break
        }

        Write-SubStatus $("Cluster nodes are still upgrading. Will check again in $sleepDuration seconds...`n") -moduleName $moduleName
        Start-Sleep $sleepDuration
    }

    Write-SubStatus $("All cluster nodes are running the expected version.") -moduleName $moduleName
}

function Update-KvaCluster
{
    <#
    .DESCRIPTION
        Updates the kubernetes version of a cluster by performing an upgrade.

    .PARAMETER Name
        Name of the cluster.

    .PARAMETER nextVersion
        desired kubernetes version.

    .PARAMETER operatingSystem
        If specified, akshcicluster will only attempt an operating system upgrade.

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding(PositionalBinding=$False, SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory=$true)]
        [String]$Name,

        [Parameter()]
        [String]$nextVersion,

        [Parameter()]
        [Switch]$operatingSystem,

        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    $cluster = Get-KvaCluster -Name $Name -activity $activity

    $kubeconfig = Get-KvaCredential -activity $activity
    if (-not (Test-Path $kubeconfig))
    {
        throw $("Unable to proceed due to missing kubeconfig file: $kubeconfig")
    }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Determining upgrade path..."

    $cmdArgs = "cluster upgrade --clustername $($cluster.metadata.name) --plan --kubeconfig $kubeconfig"
    if (($null -ne $nextVersion) -and ($nextVersion -ne ""))
    {
        $cmdArgs += " --kubernetes-version $nextVersion"
    }
    if ($operatingSystem.IsPresent)
    {
        $cmdArgs += " --operating-system"
    }

    $upgradePlan = Invoke-KvaCtl -arguments $cmdArgs -activity $activity
    $upgradePlan = $upgradePlan -join "`n"

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $upgradePlan
    Write-Status -moduleName $moduleName -Verbose -msg "`n$upgradePlan`n"

    if ($upgradePlan -like "*no upgrade found*")
    {
        return
    }

    if (-not $PSCmdlet.ShouldProcess($Name, "Update the managed Kubernetes cluster"))
        {
            return
        }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Provisioning image gallery..."
    Write-SubStatus -moduleName $moduleName  $("The cluster is currently on Kubernetes version: " + $cluster.spec.clusterConfiguration.kubernetesVersion)

    # We may need to re-add the same image, to pick up just OS update
    Add-GalleryImage -imageType Linux -k8sVersion $cluster.spec.clusterConfiguration.kubernetesVersion -activity $activity

    if ($cluster.windowsWorkerReplicas -gt 0)
    {
        Add-GalleryImage -imageType Windows -k8sVersion $cluster.spec.clusterConfiguration.kubernetesVersion -activity $activity
    }

    if ($nextVersion)
    {
        Add-GalleryImage -imageType Linux -k8sVersion $nextVersion -activity $activity

        if ($cluster.windowsWorkerReplicas -gt 0)
        {
            Add-GalleryImage -imageType Windows -k8sVersion $nextVersion -activity $activity
        }
    }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Performing cluster upgrade..."
    $cmdArgs = "cluster upgrade --clustername $($cluster.metadata.name) --kubeconfig $kubeconfig"
    if (($null -ne $nextVersion) -and ($nextVersion -ne ""))
    {
        $cmdArgs += " --kubernetes-version $nextVersion"
    }
    if ($operatingSystem.IsPresent)
    {
        $cmdArgs += " --operating-system"
    }

    Invoke-KvaCtl -arguments $cmdArgs -showOutput -activity $activity

    try 
    {
        $rando = Get-Random
        $targetClusterKubeconfig = $($env:USERPROFILE+"\.kube\$Name-kubeconfig-$rando")
        Get-KvaClusterCredential -Name $cluster.metadata.name -outputLocation $targetClusterKubeconfig -activity $activity 
        
        if ($nextVersion)
        {
            Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Waiting for upgrade to complete..."
            Wait-ForClusterUpgrade -expectedVersion $nextVersion -kubeconfigFile $targetClusterKubeconfig
        }
    }
    finally
    {
        Remove-Item -Path $targetClusterKubeconfig -Force -ErrorAction SilentlyContinue
    }

}

function Set-KvaClusterNodeCount
{
    <#
    .DESCRIPTION
        Sets the configuration of a cluster. Currently used to scale the cluster
        control plane or to scale the worker nodes.

    .PARAMETER Name
        Name of the cluster

    .PARAMETER controlPlaneNodeCount
        The number of control plane nodes to scale to

    .PARAMETER linuxNodeCount
        The number of Linux worker nodes to scale to

    .PARAMETER windowsNodeCount
        The number of Windows worker nodes to scale to

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String] $Name,

        [Parameter(Mandatory=$true, ParameterSetName='controlplane')]
        [ValidateSet(1,3,5)]
        [int] $controlPlaneNodeCount,

        [Parameter(Mandatory=$true, ParameterSetName='worker')]
        [ValidateRange(0,250)]
        [int] $linuxNodeCount,

        [Parameter(Mandatory=$true, ParameterSetName='worker')]
        [ValidateRange(0,250)]
        [int] $windowsNodeCount,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    $cluster = Get-KvaCluster -Name $Name -activity $activity

    $controlPlane = $cluster.spec.controlPlaneConfiguration.replicas
    $linuxWorker = $cluster.linuxWorkerReplicas
    $windowsWorker = $cluster.windowsWorkerReplicas

    $kubeconfig = Get-KvaCredential -activity $activity

    if ($PSCmdlet.ParameterSetName -ieq "controlplane")
    {
        if ($controlPlaneNodeCount -lt 1)
        {
            throw $("Cluster '$($cluster.metadata.name)' cannot be scaled to less than 1 control plane node.")
        }

        if (($cluster.spec.controlPlaneConfiguration.replicas -gt 1) -and ($controlPlaneNodeCount -lt 3))
        {
            throw $("Cluster '$($cluster.metadata.name)' is a highly available control plane and cannot be scaled to less than 3 nodes.")
        }
        $controlPlane = $controlPlaneNodeCount

        $cmdControlPlaneArgs = "cluster scale --clustername=$Name --controlplane=$controlPlane --kubeconfig $kubeconfig"
        Invoke-KvaCtl -arguments $cmdControlPlaneArgs -showOutput -activity $activity
    }
    elseif ($PSCmdlet.ParameterSetName -ieq "worker")
    {
        if ($windowsNodeCount -gt 0)
        {
            Add-GalleryImage -imageType Windows -k8sVersion $cluster.spec.clusterConfiguration.kubernetesVersion -activity $activity
        }

        $linuxWorker = $linuxNodeCount
        $windowsWorker = $windowsNodeCount
        
        $cmdLinuxWorkerArgs = "cluster nodepool scale --clustername=$Name --nodepoolname=$Name-default-linux-nodepool --replicas=$linuxWorker --kubeconfig $kubeconfig"
        $cmdWindowsWorkerArgs = "cluster nodepool scale --clustername=$Name --nodepoolname=$Name-default-windows-nodepool --replicas=$windowsWorker --kubeconfig $kubeconfig"

        Invoke-KvaCtl -arguments $cmdLinuxWorkerArgs -showOutput -activity $activity
        Invoke-KvaCtl -arguments $cmdWindowsWorkerArgs -showOutput -activity $activity
    }

    Write-Status -moduleName $moduleName  "Done."
}

function Remove-KvaCluster
{
    <#
    .DESCRIPTION
        Removes a cluster from the deployment.

    .PARAMETER Name
        Name of the cluster

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateScript({Test-ValidClusterName -Name $_ })]
        [String]$Name,

        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $Name -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Ensure MOC is available..."
    $isCAAvailable = Wait-ForCloudAgentEndpoint -activity $activity
    if (-not $isCAAvailable)
    {
        throw $("CloudAgent is unreachable")
    }

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Removing cluster..."
    Write-SubStatus -moduleName $moduleName  "Cluster removal is in progress. This can take several minutes to complete..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster delete --clustername $Name --kubeconfig $kubeconfig" -showOutput -activity $activity

    $kubeconfig = [io.Path]::Combine($global:config[$modulename]["installationPackageDir"], "$Name-kubeconfig")
    if (Test-Path $kubeconfig)
    {
        Remove-Item $kubeconfig -ErrorAction SilentlyContinue
    }

    Write-Status -moduleName $moduleName  "Done."
}

function Update-Kva
{
    <#
    .DESCRIPTION
        Update a the kva

    .PARAMETER version
        Optional version to udate to

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [String]$version,
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    $currentVersion = Get-KvaVersion
    $currentKvaK8sVersion = $global:config[$moduleName]["kvaK8sVersion"]

    Write-SubStatus -moduleName $moduleName  "Current KVA Version: $currentVersion"

    # If no version is specified, try to move to the latest
    if (!$version) {
        # If no version is specified, use the latest
        $release = Get-LatestRelease -moduleName $moduleName
        $version = $release.Version
        Set-KvaConfigValue -name "version" -value $version
        $release = Get-ProductRelease -Version $version -module $moduleName
    } else {
        if ($version -eq $currentVersion) {
            Write-SubStatus -moduleName $moduleName  "Already in the expected version $version"
            return
        }
        $release = Get-ProductRelease -Version $version -module $moduleName
        Set-KvaConfigValue -name "version" -value $version
    }

    $workingDir = $global:config[$moduleName]["workingDir"]

    Write-StatusWithProgress -activity $activity -module $moduleName -status $("Updating to version $version")

    try {
        # Set the new K8s version
        Set-KvaConfigValue -name "kvaK8sVersion" -value ("v" +$release.CustomData.ManagementNodeImageK8sVersion)
        Set-KvaConfigValue -name "installationPackageDir" -value $([io.Path]::Combine($workingDir, $version))
        Get-KvaConfigYaml | Out-Null
        Update-KvaInternal
    } catch {
        Set-KvaConfigValue -name "kvaK8sVersion" -value $currentKvaK8sVersion
        Set-KvaConfigValue -name "version" -value $currentVersion
        Set-KvaConfigValue -name "installationPackageDir" -value $([io.Path]::Combine($workingDir, $currentVersion))
        Get-KvaConfigYaml | Out-Null
        # Revert
        Write-StatusWithProgress -activity $activity -module $moduleName -status $("Reverting to version $currentVersion")
        Update-KvaInternal
        throw
    }
}

function Update-KvaInternal
{
    <#
    .DESCRIPTION
        Update KVA

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    param (
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Updating KVA to $(Get-KvaVersion)"

    # 1. Invalidate all gallery images
    # Gallery images are downloaded and managed by KVA. So it is okay to reset it completely for now.
    # If this assumption changes, change the logic here to just remove what KVA brings in
    Reset-GalleryImage

    New-Item -ItemType Directory -Force -Path $([io.Path]::Combine($global:config[$modulename]["installationPackageDir"], $global:yamlDirectoryName)) | Out-Null
    # 2. Update the binaries
    Get-KvaRelease -version $(Get-KvaVersion) -activity $activity

    if (Test-MultiNodeDeployment)
    {
        Get-ClusterNode -ErrorAction Stop | ForEach-Object {
            Install-KvaBinaries -nodeName $_.Name
        }
    }
    else
    {
        Install-KvaBinaries -nodeName ($env:computername)
    }

    # 3. Upgrade the management appliance
    $cluster = Get-KvaClusterInternal -Name $global:config[$moduleName]["kvaName"]

    Write-Status -moduleName $moduleName  "Provisioning image gallery"
    Write-SubStatus -moduleName $moduleName  $("The cluster is currently on Kubernetes version: " + $cluster.spec.clusterConfiguration.kubernetesVersion)

    $nextVersion = $global:config[$moduleName]["kvaK8sVersion"]
    Add-GalleryImage -imageType Linux -k8sVersion $nextVersion -activity $activity

    $kubeconfig = Get-KvaCredential -activity $activity

    $updateConfig = Get-KvaUpdateConfigYaml
    $deploymentManifestLocation = $([io.Path]::Combine($global:config[$modulename]["installationPackageDir"], $global:cloudOperatorYaml))
    $privateKey = Get-SshPrivateKey

    Invoke-KvaCtl -arguments "upgrade --configfile $updateConfig --kubeconfig $kubeconfig --cloudop $deploymentManifestLocation --sshprivatekey $privateKey" -showOutput -activity $activity
}

function Add-KvaGalleryImage
{
    <#
    .DESCRIPTION
        Downloads an image and adds it to the cloud gallery.

    .PARAMETER kubernetesVersion
        Optional, override the default version of kubernetes that the image will use.

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String] $kubernetesVersion,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    if (-not $kubernetesVersion)
    {
        $kubernetesVersion = $global:config[$modulename]["kvaK8sVersion"]
    }

    Add-GalleryImage -imagetype "Linux" -k8sVersion $kubernetesVersion -activity $activity

    Write-Status -moduleName $moduleName "Done."
}

function Get-KvaVersion {
    <#
    .DESCRIPTION
        Get the current KVA version

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    return $global:config[$modulename]["version"]
}

function Get-KvaConfig
{
    <#
    .DESCRIPTION
        Loads and returns the current KVA configuration.

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Import-KvaConfig -activity $activity

    Write-Status -moduleName $moduleName  "Getting configuration for $moduleName"
    $global:config[$modulename]["installState"] = Get-ConfigurationValue -module $moduleName -type ([Type][InstallState]) -name "installState"
    $global:config[$modulename]["controlplaneVmSize"] = Get-ConfigurationValue -module $moduleName -type ([Type][VmSize]) -name "controlplaneVmSize"
    return $global:config[$modulename]
}

function Import-KvaConfig
{
    <#
    .DESCRIPTION
        Loads a configuration from persisted storage. If no configuration is present
        then a default configuration can be optionally generated and persisted.

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [Switch] $createIfNotPresent,
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Importing Configuration"

    if  (Test-Configuration -moduleName $moduleName)
    {
        Import-Configuration -moduleName $moduleName
    }
    else
    {
        throw "This machine does not appear to be configured for deployment."
    }

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Importing Configuration Completed"
}

function Set-KvaRegistration
{
    <#
    .DESCRIPTION
        Configures KVA by persisting the specified parameters to the registry.
        Any parameter which is not explictly provided by the user will be defaulted.

    .PARAMETER azureResourceGroup
        azureResourceGroup is the name of the azure resource group to place arc resources.

    .PARAMETER azureLocation
        azureLocation is the name of the azure location where the resource group lives.
    #>


    [CmdletBinding()]
    param (
        [String] $azureResourceGroup,
        [String] $azureLocation
    )

    $kvaRegistration = Get-KvaRegistration
    if ([string]::IsNullOrWhiteSpace($kvaRegistration.azureResourceGroup))
    {
        Set-KvaConfigValue -name "azureResourceGroup" -value $azureResourceGroup
        Set-KvaConfigValue -name "azureLocation" -value $azureLocation
    }
}

function Get-KvaRegistration
{
    <#
    .DESCRIPTION
        Configures KVA by persisting the specified parameters to the registry.
        Any parameter which is not explictly provided by the user will be defaulted.
    #>


    $obj = New-Object -TypeName psobject
    $obj | Add-Member -MemberType NoteProperty -Name azureResourceGroup -Value (Get-KvaConfigValue -name "azureResourceGroup")
    $obj | Add-Member -MemberType NoteProperty -Name azureLocation -Value (Get-KvaConfigValue -name "azureLocation")

    return $obj
}

function Set-KvaConfig
{
    <#
    .DESCRIPTION
        Configures KVA by persisting the specified parameters to the registry.
        Any parameter which is not explictly provided by the user will be defaulted.
    #>


    [CmdletBinding()]
    param (
        [string] $activity = $MyInvocation.MyCommand.Name,
        [String] $kvaName = (New-Guid).Guid,
        [String] $workingDir = $global:defaultWorkingDir,
        [String] $imageDir,
        [String] $version = $global:version,
        [String] $stagingShare = $global:defaultStagingShare,
        [String] $cloudLocation = $global:defaultCloudLocation,
        [Parameter(Mandatory=$true)]
        [VirtualNetwork] $vnet,
        [VmSize] $controlplaneVmSize = $global:defaultMgmtControlPlaneVmSize,
        [String] $kvaPodCIDR = $global:defaultPodCidr,
        [Switch] $kvaSkipWaitForBootstrap,
        [ProxySettings] $proxySettings = $null,
        [Switch] $skipUpdates,
        [Switch] $skipHostLimitChecks,
        [Switch] $insecure,
        [String] $macPoolStart,
        [String] $macPoolEnd,
        [switch] $useStagingShare,
        [ContainerRegistry] $containerRegistry = $null,
        [String] $catalog = $script:catalogName,
        [String] $ring = $script:ringName,
        [int] $cloudAgentPort = $global:defaultCloudAgentPort,
        [int] $cloudAgentAuthorizerPort = $global:defaultCloudAuthorizerPort,
        [String] $deploymentId = [Guid]::NewGuid().ToString(),
        [int] $tokenExpiryDays = $script:defaultTokenExpiryDays,
        [parameter(DontShow)]
        [int] $operatorTokenValidity = $global:operatorTokenValidity,
        [parameter(DontShow)]
        [int] $addonTokenValidity = $global:addonTokenValidity
    )

    # Import the existing config, if any
    try {
        Import-KvaConfig -activity $activity
    } catch {}
    $currentState = Get-ConfigurationValue -module $moduleName -type ([Type][InstallState]) -name "installState"
    if ($currentState) {
        switch ($currentState) {
            ([InstallState]::NotInstalled) {
                # Fresh install
                break
            }
            Default {
                Write-Status -moduleName $moduleName  "Kva is currently in $currentState state"
                throw "Cannot set new $moduleName configuration when in this state [$currentState]"
            }
        }
    }

    Confirm-Configuration -useStagingShare:$useStagingShare.IsPresent -stagingShare $stagingShare

    Set-ProxyConfiguration -proxySettings $proxySettings -moduleName $moduleName
    Set-ContainerRegistryConfiguration -containerRegistry $containerRegistry

    # If okay to proceed, overwrite
    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Creating configuration for $moduleName"

    Set-KvaConfigValue -name "workingDir" -value $workingDir
    Set-KvaConfigValue -name "manifestCache" -value ([io.Path]::Combine($workingDir, "$catalog.json"))
    New-Item -ItemType Directory -Force -Path $workingDir | Out-Null

    if (!$imageDir)
    {
        $imageDir = [io.Path]::Combine($workingDir, $global:imageDirectoryName)
        New-Item -ItemType Directory -Force -Path $workingDir | Out-Null
    }
    Set-KvaConfigValue -name "imageDir" -value $imageDir
    Set-KvaConfigValue -name "stagingShare" -value $stagingShare
    Set-KvaConfigValue -name "group" -value "clustergroup"
    Set-KvaConfigValue -name "cloudLocation" -value $cloudLocation
    Set-KvaConfigValue -name "moduleVersion" -value $moduleVersion
    Set-KvaConfigValue -name "catalog" -value $catalog
    Set-KvaConfigValue -name "ring" -value $ring
    Set-KvaConfigValue -name "deploymentId" -value $deploymentId
    Set-KvaConfigValue -name "operatorTokenValidity" -value $operatorTokenValidity
    Set-KvaConfigValue -name "addonTokenValidity" -value $addonTokenValidity

    Set-VNetConfiguration -module $moduleName -vnet $vnet

    Set-KvaConfigValue -name "controlplaneVmSize" -value $controlplaneVmSize
    Set-KvaConfigValue -name "skipUpdates" -value $skipUpdates.IsPresent
    Set-KvaConfigValue -name "insecure" -value $insecure.IsPresent
    Set-KvaConfigValue -name "useStagingShare" -value $useStagingShare.IsPresent
    Set-KvaConfigValue -name "macPoolStart" -value $macPoolStart
    Set-KvaConfigValue -name "macPoolEnd" -value $macPoolEnd
    Set-KvaConfigValue -name "kvaName" -value $kvaName
    Set-KvaConfigValue -name "kvaPodCidr" -value $kvaPodCIDR
    Set-KvaConfigValue -name "kvaSkipWaitForBootstrap" -value $kvaSkipWaitForBootstrap.IsPresent
    Set-KvaConfigValue -name "cloudAgentPort" -value $cloudAgentPort
    Set-KvaConfigValue -name "cloudAgentAuthorizerPort" -value $cloudAgentAuthorizerPort
    Set-KvaConfigValue -name "tokenExpiryDays" -value $tokenExpiryDays

    if (-not $version)
    {
        $version = Get-ConfigurationValue -Name "version" -module $moduleName
        if ($version)
        {
            $release = Get-ProductRelease -Version $version -module $moduleName
        }
        else
        {
            # If no version is specified, use the latest from the product catalog
            $release = Get-LatestRelease -moduleName $moduleName
            $version = $release.Version
            Set-KvaConfigValue -name "version" -value $version
        }
    }
    else
    {
        Get-LatestCatalog -moduleName $moduleName | Out-Null # This clears the cache
        $release = Get-ProductRelease -Version $version -module $moduleName
        Set-KvaConfigValue -name "version" -value $version
    }

    $installationPackageDir = ([io.Path]::Combine($workingDir, $version))
    Set-KvaConfigValue -name "installationPackageDir" -value $installationPackageDir
    New-Item -ItemType Directory -Force -Path $installationPackageDir | Out-Null

    if (-not $release.CustomData.ManagementNodeImageK8sVersion)
    {
        throw "Unable to determine management cluster kubernetes version"
    }

    Set-KvaConfigValue -name "kvaK8sVersion" -value ("v" +($release.CustomData.ManagementNodeImageK8sVersion))
    Set-KvaConfigValue -name "kubeconfig" -value $([io.Path]::Combine($workingDir, $version, $kubeconfigMgmtFile))

    # Check if this is rehydration of an already running deployment
    if ((Test-Path $global:kvaCtlFullPath))
    {
        try {
            Get-Kva | Out-Null
            Set-KvaConfigValue -name "installState" -value ([InstallState]::Installed)
            Write-SubStatus -moduleName $moduleName  "Existing configuration for module $moduleName has been loaded`n"
            return
        } catch {
            Write-Verbose -Message $_
        }
    }

    Set-KvaConfigValue -name "installState" -value ([InstallState]::NotInstalled)
    Save-ConfigurationDirectory -moduleName $moduleName  -WorkingDir $workingDir
    Save-Configuration -moduleName $moduleName
    Write-SubStatus -moduleName $moduleName  "New configuration for module $moduleName has been saved`n"
}

function Confirm-Configuration
{
    <#
    .DESCRIPTION
        Validate if the configuration can be used for the deployment

    .PARAMETER useStagingShare
        Requests a staging share to be used for downloading binaries and images (for private testing)

    .PARAMETER stagingShare
        The staging share endpoint to use when useStagingShare is requested
    #>


    param (
        [Switch] $useStagingShare,
        [String] $stagingShare
    )

    if ($useStagingShare.IsPresent -and [string]::IsNullOrWhiteSpace($stagingShare))
    {
        throw "-useStagingShare was requested, but no staging share was specified"
    }
}

function Set-KvaConfigValue {
     <#
    .DESCRIPTION
        Persists a configuration value to the registry

    .PARAMETER name
        Name of the configuration value

    .PARAMETER value
        Value to be persisted
    #>


    param (
        [String] $name,
        [Object] $value
    )

    Set-ConfigurationValue -name $name -value $value -module $moduleName
}

function Get-KvaConfigValue
{
    <#
   .DESCRIPTION
       Persists a configuration value to the registry

   .PARAMETER name
       Name of the configuration value
   #>


   param (
       [String] $name
   )

   return Get-ConfigurationValue -name $name -module $moduleName
}

function Get-KvaConfigYaml
{
    <#
    .DESCRIPTION
        Sets Configuration for the management appliance.

    .PARAMETER kubernetesVersion
        Version of kubernetes to deploy

    .PARAMETER group
        The name of the group in which the vault resides
    #>


    param (
        [String]$kubernetesVersion = $global:config[$modulename]["kvaK8sVersion"],
        [String]$group = $global:config[$modulename]["group"]
    )

    $installationPackageDir = $global:config[$moduleName]["installationPackageDir"]
    $yamlFile = $($installationPackageDir+"\yaml\appliance.yaml")
    if (-not (Test-Path $yamlFile))
    {
        New-Item -ItemType File -Force -Path $yamlFile | Out-Null
    }

    # ACL the yaml so that it is only readable by administrator
    Set-SecurePermissionFile -Path $yamlFile

    $publicKey = Get-Content -Path (Get-SshPublicKey)
    $publicKey = $publicKey.Split(" ")
    $publicKey = $($publicKey[0]+" "+$publicKey[1])

    $containerRegistryUser = $global:config[$moduleName]["containerRegistryUser"]
    $containerRegistryPass = $global:config[$moduleName]["containerRegistryPass"]
    if(-not [String]::IsNullOrEmpty($containerRegistryPass))
    {
        $securePass = $containerRegistryPass | ConvertTo-SecureString -Key $global:credentialKey
        $credential = New-Object System.Management.Automation.PSCredential -ArgumentList $containerRegistryUser, $securePass
        $containerRegistryPass = $credential.GetNetworkCredential().Password
    }

    $deploymentManifestLocation = $([io.Path]::Combine($global:config[$modulename]["installationPackageDir"], $global:cloudOperatorYaml))
    $cloudFqdn = Get-CloudFqdn
    $clusterName = $($global:config[$modulename]["kvaName"])
    $cloudPort = $global:config[$moduleName]["cloudAgentPort"].ToString()
    $cloudAuthPort = $global:config[$moduleName]["cloudAgentAuthorizerPort"].ToString()
    $kvaIdentity = $global:config[$moduleName]["identity"]
    $vnet = Get-VNetConfiguration -module $moduleName
    $operatorTokenValidity = $global:config[$moduleName]["operatorTokenValidity"]
    $addonTokenValidity = $global:config[$moduleName]["addonTokenValidity"]
    
    # Decide whether to use the macpool or not
    $useMacPool = (-not [string]::IsNullOrWhiteSpace( $global:config[$modulename]["macPoolStart"]) -and -not [string]::IsNullOrWhiteSpace($global:config[$modulename]["macPoolEnd"]))
    $macPoolName = ""
    if ($useMacPool)
    {
        $macPoolName = $vnet.MacPoolName
    }

    $dnsserver = ""
    $addressallocation = "Dynamic"
    if(-Not [string]::IsNullOrEmpty($vnet.IpAddressPrefix))
    {
        $addressallocation = "Static"
        foreach ($dns in $vnet.DnsServers)
        {
            if(-Not [string]::IsNullOrEmpty($dns))
            {
                $dnsserver += "`n - " +  $dns
            }
        }
    }

    $kvaRegistration = Get-KvaRegistration
    if (($kvaRegistration.azureResourceGroup -ne "") -and
    ($kvaRegistration.azureLocation -ne ""))
    {
        $azContext = Get-AzContext
        $subscriptionid = $azContext.Subscription.Id
        $tenantid = $azContext.Tenant.Id
        $resourcegroup = $global:config[$moduleName]["azureResourceGroup"]
        $location = $global:config[$moduleName]["azureLocation"]
    }

    Write-Status -moduleName $moduleName  "Creating KVA configuration"
    $waitForBootstrap = -not (Get-ConfigurationValue -module $moduleName -Name "kvaSkipWaitForBootstrap" -type ([Type][System.Boolean]))
    $proxySettings = Get-ProxyConfiguration -moduleName $moduleName
    $configMaps = Get-KvaConfigMaps
    $isHyperThreadingEnabled = Get-MocHyperThreadingEnabled

    $yaml = @"
clustername: $clusterName
kubernetesversion: $kubernetesVersion
sshauthorizedkey: $publicKey
lowprivilegekubeconfig: false
encryptsecrets: true
waitforbootstrapcompletion: $waitForBootstrap
containerregistry:
  name: $($global:config[$moduleName]["containerRegistryServer"])
  username: $containerRegistryUser
  password: $containerRegistryPass
networking:
  controlplanecidr: $($global:mgmtControlPlaneCidr)
  clustercidr: $($global:mgmtClusterCidr)
  podcidr: $($global:config[$moduleName]["kvaPodCidr"])
  proxy:
    http: "$($proxySettings.HTTP)"
    https: "$($proxySettings.HTTPS)"
    noproxy: "$($proxySettings.NoProxy)"
    certfilename: "$($proxySettings.CertName)"
deploymentmanifest:
  cloudoperatormanifestpath: $deploymentManifestLocation
  cni:
    type: calico
applianceagents:
  onboardingagent:
    subscriptionid: $subscriptionid
    tenantid: $tenantid
    resourcegroup: $resourcegroup
    location: $location
    infrastructure: azure_stack_hci
  billingagent:
    hyperthreading: $isHyperThreadingEnabled
azurestackhciprovider:
  cloudagent:
    address: $cloudFqdn
    port: $cloudPort
    authenticationport: $cloudAuthPort
    loginconfig: $kvaIdentity
    insecure: $($global:config[$moduleName]["insecure"])
  appliancevm:
    imagename: ""
    vmsize: $(Get-ConfigurationValue -module $moduleName -name "controlplaneVmSize" -type ([Type][VmSize]))
  loadbalancer:
    imagename: ""
  location: $($global:config[$modulename]["cloudLocation"])
  group: $group
  storagecontainer: $($global:cloudStorageContainer)
  virtualnetwork:
    name: "$($vnet.Name)"
    vswitchname: "$($vnet.VswitchName)"
    type: "Transparent"
    macpoolname: $macPoolName
    vlanid: $($vnet.VlanID)
    ipaddressprefix: $($vnet.IpAddressPrefix)
    addressallocation: $addressallocation
    gateway: $($vnet.Gateway)
    dnsservers: $dnsserver
    vippoolstart: $($vnet.VipPoolStart)
    vippoolend: $($vnet.VipPoolEnd)
    k8snodeippoolstart: $($vnet.K8snodeIPPoolStart)
    k8snodeippoolend: $($vnet.K8snodeIPPoolEnd)
  tokenvalidities:
    addontokenvalidity: $addonTokenValidity
    operatortokenvalidity: $operatorTokenValidity
"@


    if ($proxySettings.CertName)
    {
        $yaml += @"
`ncertificates:
- filename: "$($proxySettings.CertName)"
  contentb64: "$($proxySettings.CertContent)"
"@

    }

    if ($configMaps)
    {
        $yaml += "`n$configMaps"
    }

    Set-Content -Path $yamlFile -Value $yaml
    if ($null -ne $err -and $err.count -gt 0)
    {
        throw $err
    }

    Set-KvaConfigValue -name "kvaconfig" -value $yamlFile
    return $yamlFile
}

function Get-KvaUpdateConfigYaml
{
    <#
    .DESCRIPTION
        Returns a configuration file for appliance upgrade. This configuration is merged with the existing
        config stored within the appliance. It is mainly used to provide updated version information and
        config maps.
    #>


    $installationPackageDir = $global:config[$moduleName]["installationPackageDir"]
    $yamlFile = $($installationPackageDir+"\yaml\appliance-update.yaml")
    if (-not (Test-Path $yamlFile))
    {
        New-Item -ItemType File -Force -Path $yamlFile | Out-Null
    }

    Write-Status -moduleName $moduleName  "Creating KVA update configuration"
    $yaml = ""

    $configMaps = Get-KvaConfigMaps
    if ($configMaps)
    {
        $yaml += $configMaps
    }

    Set-Content -Path $yamlFile -Value $yaml
    if ($null -ne $err -and $err.count -gt 0)
    {
        throw $err
    }

    return $yamlFile
}

function Get-KvaLogs
{
    <#
    .DESCRIPTION
        Collects all the logs from the deployment

    .PARAMETER Path
        Path to store the logs

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String]$path,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    $logDir = [io.Path]::Combine($path, $moduleName)
    New-Item -ItemType Directory -Force -Path $logDir | Out-Null

    Initialize-KvaEnvironment -activity $activity

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Collecting configuration..."
    $global:config[$moduleName] > $logDir"\KvaConfig.txt"

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Collecting module information..."
    Get-Command -Module Kva | Sort-Object -Property Source > $($logDir+"\moduleinfo.txt")

    if (Test-Path -Path $script:logFilePath)
    {
        Copy-Item -Path $script:logFilePath -Destination $logDir
    }

    if (Test-Path -Path $global:config[$moduleName]["kubeconfig"])
    {
        Write-SubStatus -moduleName $moduleName  "Collecting Kubernetes Cluster Logs"

        try
        {
            $clusters = Invoke-Kubectl -arguments $("get akshciclusters -o json") | ConvertFrom-Json

            foreach ($cluster in $clusters.items)
            {
                $clusterName = $cluster.metadata.name
                Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $("Collecting logs for cluster '$clusterName'...")

                if ($clusterName -ine $global:config[$moduleName]["kvaName"])
                {
                    $rando = Get-Random
                    $kubeconfigFileLocation = $($env:USERPROFILE+"\.kube\$Name-kubeconfig-$rando")
                    Get-KvaClusterCredential -Name $clusterName -outputLocation $kubeconfigFileLocation
                }
                else
                {
                    $kubeconfigFileLocation = Get-KvaCredential
                }

                Write-SubStatus -moduleName $moduleName  "Collecting cluster $clusterName logs"
                try{
                    Invoke-Kubectl -kubeconfig $kubeconfigFileLocation -arguments "get all,pvc,pv,sc -A -o yaml" > "${logDir}\worker_objects_$clusterName.json"
                    $clusterLogDirectory = [io.Path]::Combine($logDir, "clusterlogs_$clusterName")
                    Invoke-Kubectl -kubeconfig $kubeconfigFileLocation -arguments $("cluster-info dump -A --output-directory=`"$clusterLogDirectory`"") | Out-Null
                }catch{}

                if ($clusterName -ine $global:config[$moduleName]["kvaName"])
                {
                    Remove-Item -Path $kubeconfigFileLocation -Force -ErrorAction SilentlyContinue
                }
            }
        }
        catch [Exception]
        {
            Write-Status -moduleName $moduleName  -msg "Exception caught!!!"
            Write-SubStatus -moduleName $moduleName  -msg $_.Exception.Message.ToString()
        }
    }
    else
    {
        Write-SubStatus -moduleName $moduleName  "Skipping Kubernetes Cluster Log collection as management cluster kubeconfig is not present."
    }

    Write-Status -moduleName $moduleName  "Done."
}

function Get-KvaEventLog
{
    <#
    .DESCRIPTION
        Gets all the event logs from Kva Module
    #>


    Get-WinEvent -ProviderName $moduleName -ErrorAction SilentlyContinue
}

function Repair-KvaCluster
{
    <#
    .DESCRIPTION
        Attempts to repair failed TLS on a cluster by reprovisioning control plane certs

    .PARAMETER Name
        Name of the node to reprovision certs on

    .PARAMETER sshPrivateKeyFile
        Kubeconfig for the cluster the node belongs to

    .PARAMETER fixCertificates
        If specified, will attempt to reprovision control plane certs to fix TLS errors

    .PARAMETER activity
        Activity name to use when updating progress
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter()]
        [String] $sshPrivateKeyFile,

        [Parameter()]
        [Switch] $fixCertificates,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    if ($fixCertificates.IsPresent) {
        $kvaConfig = Get-KvaConfigYaml
        Invoke-KvaCtl -arguments "cluster addons certs repair --cluster-name $Name --sshprivatekey $sshPrivateKeyFile --configfile $kvaConfig" -showOutput -activity $activity
    }
}

function Set-ContainerRegistryConfiguration
{
    <#
    .DESCRIPTION
        Sets the container registry configuration

    .PARAMETER containerRegistry
        Container registry settings
    #>


    param (
        [Parameter()]
        [ContainerRegistry] $containerRegistry
    )

    $server = ""
    $user = ""
    $pass = ""

    if ($containerRegistry)
    {
        $server = $containerRegistry.Server
        if ($containerRegistry.Credential.Username)
        {
            $user = $containerRegistry.credential.UserName
        }
        if ($containerRegistry.Credential.Password)
        {
            $pass = $containerRegistry.credential.Password | ConvertFrom-SecureString -Key $global:credentialKey
        }
    }
    else
    {
        $server = $script:defaultContainerRegistryServer
        $user = $script:defaultContainerRegistryU

        $ss = ConvertTo-SecureString $script:defaultContainerRegistryP -AsPlainText -Force
        $pass = $ss | ConvertFrom-SecureString -Key $global:credentialKey
    }

    Set-ConfigurationValue -name "containerRegistryServer" -value $server -module $moduleName
    Set-ConfigurationValue -name "containerRegistryUser" -value $user -module $moduleName
    Set-ConfigurationValue -name "containerRegistryPass" -value $pass -module $moduleName
}

#endregion

#region Billing
function Sync-KvaBilling
{
    <#
    .DESCRIPTION
        Helper function to sync billing.

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    $kubeconfig = Get-KvaCredential -activity $activity

    try
    {
        $syncResult = Invoke-KvaCtl -arguments "cluster addons billing sync --kubeconfig $kubeconfig" -activity $activity
    }
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Error -eventId 100 -message "$activity - $_"
        throw $("Sync billing failed. $_")
    }

    return $syncResult
}

function Get-KvaBillingRecords
{
    <#
    .DESCRIPTION
        Helper function to list billing records.

    .PARAMETER outputformat
        Output format of result

    .PARAMETER activity
        Activity name to use when writing progress
    #>

    
    [CmdletBinding()]
    param (
        [Parameter()]
        [String] $outputformat = "json",

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    $kubeconfig = Get-KvaCredential -activity $activity

    try
    {
        $recordsResult = Invoke-KvaCtl -arguments "cluster addons billing get-records --kubeconfig $kubeconfig --outputformat=$outputformat" -activity $activity
    }
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Error -eventId 100 -message "$activity - $_"
        throw $("Get records failed. $_")
    }

    return $recordsResult
}

function Get-KvaBillingStatus
{
    <#
    .DESCRIPTION
        Helper function to get billing status.

    .PARAMETER outputformat
        Output format of result

    .PARAMETER activity
        Activity name to use when writing progress
    #>

    
    [CmdletBinding()]
    param (
        [Parameter()]
        [String] $outputformat = "json",

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    $yamlFile = $global:config[$moduleName]["kubeconfig"]

    

    try
    {
        $statusResult = Invoke-KvaCtl -arguments "cluster addons billing get-status --kubeconfig $yamlFile --outputformat=$outputformat" -activity $activity
    } 
    catch [Exception]
    {
        Write-ModuleEventLog -moduleName $moduleName -entryType Error -eventId 100 -message "$activity - $_"
        throw $("Get billing status failed. $_")
    }

    return $statusResult
}
#endregion

#region Installation and Provisioning functions

function Install-KvaInternal
{
    <#
    .DESCRIPTION
        The main deployment method for KVA. This function is responsible for provisioning files,
        deploying the agents.

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    param (
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Set-KvaConfigValue -name "installState" -value ([InstallState]::Installing)
    try
    {
        Get-KvaRelease -version $(Get-KvaVersion) -activity $activity

        if (Test-MultiNodeDeployment)
        {
            Get-ClusterNode -ErrorAction Stop | ForEach-Object {
                Install-KvaBinaries -nodeName $_.Name
            }
        }
        else
        {
            Install-KvaBinaries -nodeName ($env:computername)
        }

        if (-not $global:config[$modulename]["insecure"])
        {
            Write-Status -moduleName $moduleName  "Creating KVA identity"
            $clusterName = $($global:config[$modulename]["kvaName"])
            $kvaIdentity = New-MocIdentity -name $clusterName -validityDays $global:config[$moduleName]["tokenExpiryDays"] -fqdn $cloudFqdn -location $global:config[$modulename]["cloudLocation"] -port $cloudPort -authport $cloudAuthPort -encode
            Set-KvaConfigValue -name "identity" -value $kvaIdentity
            New-MocRoleAssignment -identityName $clusterName -roleName "IdentityContributor"
            New-MocRoleAssignment -identityName $clusterName -roleName "RoleContributor"
            New-MocRoleAssignment -identityName $clusterName -roleName "CertificateReader"
            New-MocRoleAssignment -identityName $clusterName -roleName "VipPoolReader" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "GroupContributor" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "KeyVaultContributor" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "VirtualNetworkContributor" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "LBContributor" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "NetworkInterfaceContributor" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "VMContributor" -location $global:config[$modulename]["cloudLocation"]
            New-MocRoleAssignment -identityName $clusterName -roleName "SecretContributor" -location $global:config[$modulename]["cloudLocation"]
        }
        
        $yamlFile = Get-KvaConfigYaml -kubernetesVersion $global:config[$modulename]["kvaK8sVersion"] -group $global:config[$modulename]["group"]
        $kubeconfig = $global:config[$modulename]["kubeconfig"]

        Invoke-KvaCtlWithAzureContext -arguments "create --configfile $yamlFile --outfile $kubeconfig" -showOutputAsProgress -activity $activity
    }
    catch
    {
        Set-KvaConfigValue -name "installState" -value ([InstallState]::InstallFailed)
        throw $_
    }

    Set-SecurePermissionFile -Path $kubeconfig
    Set-KvaConfigValue -name "installState" -value ([InstallState]::Installed)
    Write-Status -moduleName $moduleName  "KVA installation is complete!"
}

function Get-KvaRelease
{
    <#
    .DESCRIPTION
        Download a KVA release (e.g. package, images, etc)

    .PARAMETER version
        Release version

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $version,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Discovering Kva release"
    $productRelease = Get-ProductRelease -version $version -moduleName $moduleName

    if (-not $productRelease.CustomData.ManagementNodeImageK8sVersion)
    {
        throw "Unable to determine required management cluster kubernetes version"
    }

    $k8sVersion = $productRelease.CustomData.ManagementNodeImageK8sVersion
    Write-SubStatus -moduleName $moduleName "Determined that kubernetes version $k8sVersion is required for the management cluster"

    Add-GalleryImage -imageType "Linux" -k8sVersion $k8sVersion -activity $activity
    Write-SubStatus -moduleName $moduleName "The management cluster image has been provisioned"

    Get-ReleaseContent -name $script:productName -version $version -activity $activity

    if (-not ($global:config[$modulename]["useStagingShare"]))
    {
        Test-AuthenticodeBinaries -workingDir $global:config[$moduleName]["installationPackageDir"] -binaries $script:kvaBinaries
    }
}

function Get-KvaConfigMaps
{
    <#
    .DESCRIPTION
        Retrieve KVA config maps
    #>


    Write-Status -moduleName $moduleName "Get KVA config maps"

    $productRelease = Get-ProductRelease -version $global:config[$modulename]["version"] -moduleName $moduleName

    $yaml = @"
configmaps:
  - name: $script:productInfoMapName
    namespace: $script:productInfoMapNamespace
    data:
      offer: "$($productRelease.ProductName)"
      version: "$($global:config[$modulename]["version"])"
      catalog: "$($global:config[$modulename]["catalog"])"
      audience: "$($global:config[$modulename]["ring"])"
      deploymentid: "$($global:config[$modulename]["deploymentId"])"
"@


    return $yaml
}

function Get-ReleaseContent
{
    <#
    .DESCRIPTION
        Download all required files and packages for the specified release

    .PARAMETER name
        Release name

    .PARAMETER version
        Release version

    .PARAMETER destination
        Destination directory for the content

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $name,

        [Parameter(Mandatory=$true)]
        [String] $version,

        [Parameter()]
        [string] $destination = $global:config[$moduleName]["installationPackageDir"],

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name

    )

    $destination = $destination -replace "\/", "\"

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $("Discovering $name release content")
    $productRelease = Get-ProductRelease -version $version -moduleName $moduleName

    # find requested release
    foreach($releaseStream in $productRelease.ProductStreamRefs)
    {
        foreach($subProductRelease in $releaseStream.ProductReleases)
        {
            if ($subProductRelease.ProductName -ieq $name)
            {
                $versionManifestPath = [io.Path]::Combine($destination, $("$name-release.json"))

                Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $("Downloading $name release content to $destination")

                $downloadParams = Get-ReleaseDownloadParameters -name $subProductRelease.ProductStream -version $subProductRelease.Version -destination $destination -parts 3 -moduleName $moduleName
                $releaseInfo = Get-DownloadSdkRelease @downloadParams

                if (-not ($global:config[$moduleName]["useStagingShare"]))
                {
                    if ($releaseInfo.Files.Count -ne 1)
                    {
                        throw $("Unexpected $name release content files downloaded. Expected 1 file, but received " + $releaseInfo.Files.Count)
                    }

                    $packagename = $releaseInfo.Files[0] -replace "\/", "\"

                    # Temporary until cross-platform signing is available
                    $auth = Get-AuthenticodeSignature -filepath $packagename
                    if (($global:expectedAuthResponse.status -ne $auth.status) -or ($auth.SignatureType -ne $global:expectedAuthResponse.SignatureType))
                    {
                        throw $("$name release content failed authenticode verification. Expected status=$($global:expectedAuthResponse.status) and type=$($global:expectedAuthResponse.SignatureType) but received status=$($auth.status) and type=$($auth.SignatureType)")
                    }

                    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $("Expanding $name package $packagename into $destination")
                    $expandoutput = expand.exe -r $packagename $destination -f:*
                    Write-SubStatus -moduleName $moduleName  "Expand result: $expandoutput"
                }

                $versionJson = $subProductRelease | ConvertTo-Json -depth 100
                set-content -path $versionManifestPath -value $versionJson -encoding UTF8

                return
            }
        }
    }

    throw "Unable to get $name release content for version $version"
}

function Initialize-KvaEnvironment
{
    <#
    .DESCRIPTION
        Executes steps to prepare the environment for day 0 operations.

    .PARAMETER createConfigIfNotPresent
        Whether the call should create a new deployment configuration if one is not already present.

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    param (
        [Switch]$createConfigIfNotPresent,
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Get-MocConfig -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Discovering configuration"
    Import-KvaConfig -createIfNotPresent:($createConfigIfNotPresent.IsPresent) -activity $activity

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Applying configuration"
    Initialize-Environment -checkForUpdates:$false -moduleName $script:moduleName
}

function Install-KvaBinaries
{
    <#
    .DESCRIPTION
        Copies KVA binaries to a node

    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$nodeName
    )
    Install-Binaries -module $moduleName -nodeName $nodeName -binariesMap $kvaBinariesMap
}

function Uninstall-KvaBinaries
{
    <#
    .DESCRIPTION
        Copies KVA binaries to a node

    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$nodeName
    )

    Uninstall-Binaries -module $moduleName -nodeName $nodeName -binariesMap $kvaBinariesMap
}

#endregion

#region Verification Functions

function Test-KvaConfiguration
{
    <#
    .DESCRIPTION
        A basic sanity test to make sure that a node is ready to deploy kubernetes.

    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [String]$nodeName
    )

    Test-KvaInstallation -nodeName $nodeName
}

function Test-KvaInstallation
{
    <#
    .DESCRIPTION
        Sanity checks an installation to make sure that all expected binaries are present.

    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [String]$nodeName
    )

    Write-SubStatus -moduleName $moduleName  "Testing for expected binaries"
    Test-Binary -nodeName $nodeName -binaryName $global:kvaCtlFullPath
    Test-Binary -nodeName $nodeName -binaryName $global:kubeCtlFullPath
}

function Get-KvaLatestVersion
{
    <#
    .DESCRIPTION
        Get the current KVA release version

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    param (
        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Write-StatusWithProgress -activity $activity -module $moduleName -status $("Discovering latest version")
    $catalog = Get-LatestCatalog -moduleName $moduleName
    $productRelease = $catalog.ProductStreamRefs[0].ProductReleases[0]

    # find latest kva release
    foreach($subProductStream in $productRelease.ProductStreamRefs)
    {
        foreach($subProductRelease in $subProductStream.ProductReleases)
        {
            if ($subProductRelease.ProductName -ieq $script:productName)
            {
                return $subProductRelease.Version
            }
        }
    }

    throw "Unable to determine KVA latest version"
}

function Reset-GalleryImage
{
    <#
    .DESCRIPTION
        Resets all gallery image
    #>


    Write-Status -moduleName $moduleName  "Resetting Gallery image"
    # 1. Invalidate all gallery images
    # Gallery images are downloaded and managed by KVA. So it is okay to reset it completely for now.
    # If this assumption changes, change the logic here to just remove what KVA brings in
    Reset-MocGalleryImage -location $global:config[$modulename]["cloudLocation"]

    # 2. Cleanup all vhd files
    $imagepath = $global:config[$moduleName]["imageDir"]
    Get-ChildItem -Path $imagepath -Filter *.vhdx | ForEach-Object {
        $tmpFile = $_.FullName
        Write-SubStatus -moduleName $moduleName  "Removing image $tmpFile"
        Remove-Item -Path $tmpFile -Force -ErrorAction SilentlyContinue
    }

    # 3. Cleanup all json manifest cache files
    Get-ChildItem -Path $imagepath -Filter *.json | ForEach-Object {
        $tmpFile = $_.FullName
        Write-SubStatus -moduleName $moduleName  "Removing image cache $tmpFile"
        Remove-Item -Path $tmpFile -Force -ErrorAction SilentlyContinue
    }
}

function Add-GalleryImage
{
    <#
    .DESCRIPTION
        Checks the gallery to see if the requested image is present. If it is missing, this function downloads
        the missing image and adds it to the gallery.

    .PARAMETER imageName
        Name of the gallery image to provision

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    param (
        # [Parameter(Mandatory=$true)]
        # [String] $imageName,

        [Parameter(Mandatory=$true)]
        [ValidateSet("Windows", "Linux")]
        [String] $imageType,

        [Parameter(Mandatory=$true)]
        [String] $k8sVersion,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Provisioning Gallery image..."

    $imageName = Get-KubernetesGalleryImageName -imagetype $imageType -k8sVersion $k8sVersion

    Write-SubStatus -moduleName $moduleName  $("Requested image is '$imageName'")
    Write-SubStatus -moduleName $moduleName  $("Checking for existing gallery image ...")
    $galleryImage = $null
    $mocLocation = $global:config[$modulename]["cloudLocation"]

    try {
        $galleryImage = Get-MocGalleryImage -name $imageName -location $mocLocation
    } catch {}

    if ($null -ine $galleryImage)
    {
        Write-SubStatus -moduleName $moduleName  $("Image is already present in the gallery.")
        return
    }

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Obtaining download information for image $imageName"
    $imageRelease = Get-ImageReleaseManifest -imageVersion $(Get-KvaVersion) -operatingSystem $imageType -k8sVersion $k8sVersion

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Downloading image $imageName"
    $result = Get-ImageRelease -imageRelease $imageRelease -imageDir $global:config[$moduleName]["imageDir"]

    Write-StatusWithProgress -activity $activity -module $moduleName -status "Adding image to cloud gallery ($imageName)"
    New-MocGalleryImage -name $imageName -location $mocLocation -imagePath $result -container $global:cloudStorageContainer  | Out-Null
}

function Invoke-KvaCtl
{
    <#
    .DESCRIPTION
        Executes a KVACTL command.

    .PARAMETER arguments
        Arguments to pass to the command.

    .PARAMETER ignoreError
        Optionally, ignore errors from the command (don't throw).

    .PARAMETER showOutput
        Optionally, show live output from the executing command.

    .PARAMETER activity
        Activity name to use when updating progress.
    #>


    param (
        [string] $arguments,
        [switch] $ignoreError,
        [switch] $showOutput,
        [string] $activity = $MyInvocation.MyCommand.Name
    )

    return Invoke-CommandLine -command $kvaCtlFullPath -arguments $("$arguments") -ignoreError:$ignoreError.IsPresent -showOutputAsProgress:$showOutput.IsPresent -progressActivity $activity -moduleName $moduleName
}

function Invoke-KvaCtlWithAzureContext
{
    <#
    .DESCRIPTION
        Executes a command and optionally ignores errors.

    .PARAMETER arguments
        Arguments to pass to the command.

    .PARAMETER ignoreError
        Optionally, ignore errors from the command (don't throw).

    .PARAMETER showOutput
        Optionally, show live output from the executing command.

    .PARAMETER showOutputAsProgress
        Optionally, show output from the executing command as progress bar updates.

    .PARAMETER credential
        credential is a PSCredential holding a user's Service Principal.

    .PARAMETER activity
        The activity name to display when showOutputAsProgress was requested.
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String]$command,

        [Parameter()]
        [String]$arguments,

        [Parameter()]
        [Switch]$ignoreError,

        [Parameter()]
        [Switch]$showOutput,

        [Parameter()]
        [Switch]$showOutputAsProgress,

        [Parameter()]
        [PSCredential] $credential,

        [Parameter()]
        [string] $activity = $MyInvocation.MyCommand.Name
    )

    $obj = New-Object -TypeName psobject

    if ($null -ne $credential)
    {
        $obj | Add-Member -MemberType NoteProperty -Name clientId -Value $credential.GetNetworkCredential().UserName
        $obj | Add-Member -MemberType NoteProperty -Name clientSecret -Value $credential.GetNetworkCredential().Password
    }
    else
    {
        $armAccessToken = Get-AzAccessToken
        $graphAccessToken = Get-GraphAccessToken

        $obj | Add-Member -MemberType NoteProperty -Name armAccessToken -Value $armAccessToken.Token
        $obj | Add-Member -MemberType NoteProperty -Name graphAccessToken -Value $graphAccessToken.Token

        $azContext = Get-AzContext
        if ($azContext.Account.Type -eq "ServicePrincipal")
        {
            $obj | Add-Member -MemberType NoteProperty -Name clientId -Value $azContext.Account.Id
            $obj | Add-Member -MemberType NoteProperty -Name clientSecret -Value $azContext.Account.ExtendedProperties.ServicePrincipalSecret
        }
    }

    $arguments += " --azure-creds-stdin"

    try
    {
        if ($showOutputAsProgress.IsPresent)
        {
            $result = ($obj | ConvertTo-Json | & $kvaCtlFullPath $arguments.Split(" ") | ForEach-Object { $status = $_ -replace "`t"," - "; Write-StatusWithProgress -activity $activity -moduleName $moduleName -Status $status }) 2>&1
        }
        elseif ($showOutput.IsPresent)
        {
            $result = ($obj | ConvertTo-Json | & $kvaCtlFullPath $arguments.Split(" ") | Out-Default) 2>&1
        }
        else
        {
            $result = ($obj | ConvertTo-Json | & $kvaCtlFullPath $arguments.Split(" ") 2>&1)
        }
    }
    catch
    {
        if ($ignoreError.IsPresent)
        {
            return
        }
        throw
    }

    $out = $result | Where-Object {$_.gettype().Name -ine "ErrorRecord"}  # On a non-zero exit code, this may contain the error
    #$outString = ($out | Out-String).ToLowerInvariant()

    if ($LASTEXITCODE)
    {
        if ($ignoreError.IsPresent)
        {
            return
        }
        $err = $result | Where-Object {$_.gettype().Name -eq "ErrorRecord"}
        throw "$command $arguments returned a non zero exit code $LASTEXITCODE [$err]"
    }
    return $out
}

function Get-KvaClusterCredential
{
    <#
    .DESCRIPTION
        Obtains the credentials for a cluster as a kubeconfig and outputs it to the requested
        location (by default the location is the current users .kube directory).

    .PARAMETER Name
        Name of the cluster to obtain the credential/kubeconfig for.

    .PARAMETER outputLocation
        Location to output the credential/kubeconfig file to.

    .PARAMETER adAuth
        To get the Active Directory SSO version of the kubeconfig.

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string] $Name,

        [Parameter()]
        [string] $outputLocation = $($env:USERPROFILE+"\.kube\config"),

        [Parameter(Mandatory=$false)]
        [Switch] $adAuth,

        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    #TODO Fix this from the kvasdk layer
    if ($Name -eq $global:config[$moduleName]["kvaName"])
    {
        throw "Get-KvaClusterCredential is only valid for target clusters"
    }

    $kubeconfig = Get-KvaCredential -activity $activity
    if (-not (Test-Path $kubeconfig))
    {
        throw $("Unable to proceed due to missing kubeconfig file: $kubeconfig")
    }

    # TODO move directory check to kva
    #
    # The following line will only return true if the passed in location
    # is a valid directory. If the path doesn't exist or if it points a
    # file it will return false.
    #
    # In the case where the user passes in an invalid path, we will fail at a later point as
    # its hard to tell whether they are passing valid location to a file to be written or an
    # path that doesn't exist.
    if (Test-Path -Path $outputLocation -PathType Container)
    {
        $outputLocation = [io.Path]::Combine($outputLocation, "kubeconfig-$Name")
    }

    # Ensure that the cluster exists
    $cluster = Get-KvaCluster -Name $Name -activity $activity    
    if ($adAuth.IsPresent)
    {
        $exePath = [io.Path]::Combine($global:installDirectory, "kubectl-adsso.exe")
        if (-not [System.IO.File]::Exists($exePath))
        {
            Get-ClientCredPluginRelease -version $(Get-KvaVersion) -destinationDir $global:installDirectory -activity $activity
        }
        
        Invoke-KvaCtl -arguments "cluster retrieve --clustername $Name --kubeconfig $kubeconfig --outfile ""$outputLocation"" --adauth" -showOutput -activity $activity
        Write-SubStatus -moduleName $moduleName  $("AD kubeconfig was be written to: $outputLocation")
    }
    else
    {
        Invoke-KvaCtl -arguments "cluster retrieve --clustername $Name --kubeconfig $kubeconfig --outfile ""$outputLocation""" -showOutput -activity $activity
        Write-SubStatus -moduleName $moduleName  $("kubeconfig will be written to: $outputLocation")
    }

    Set-SecurePermissionFile -Path $outputLocation
}

function Get-KvaCredential
{
    <#
    .DESCRIPTION
        Obtains the credentials for kva as a kubeconfig and outputs it to the default
        location).

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name)"
    }

    $kubeconfigPath = $global:config[$moduleName]["kubeconfig"]
    if (Test-Path $kubeconfigPath)
    {
        return $kubeconfigPath
    }

    $yamlFile = Get-KvaConfigYaml
    Invoke-KvaCtl -arguments "retrieve --configfile $yamlFile --outfile $kubeconfigPath" -showOutput -activity $activity

    Set-SecurePermissionFile -Path $kubeconfigPath

    return $kubeconfigPath
}

function Get-ClientCredPluginRelease
{
    <#
    .DESCRIPTION
        Download the client cred plugin release content

    .PARAMETER Version
        Version

    .PARAMETER destinationDir
        Destination directory

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $version,

        [Parameter(Mandatory=$true)]
        [string] $destinationDir,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Get-ReleaseContent -name "client-cred-plugin" -version $version -activity $activity

    $srcPath = [io.Path]::Combine($global:config[$moduleName]["installationPackageDir"], "kubectl-adsso.exe")
    Copy-Item $srcPath -Destination $destinationDir
}

function Wait-ForKubernetesSecret
{
    <#
    .DESCRIPTION
        Polls the vault for a secret to become available and returns the secret value.

    .PARAMETER name
        Name of the secret

    .PARAMETER sleepDuration
        Duration to sleep for between attempts to retrieve the secret
    #>


    param (
        [String]$name,
        [int]$sleepDuration=20
    )

    Write-SubStatus $("Polling cluster for secret '$name' to be available") -moduleName $moduleName

    while($true)
    {
        try {
            $clusterSecret = Invoke-Kubectl -arguments $("get secrets/$name")
        } catch {}

        if ($null -ne $clusterSecret)
        {
            break
        }
        Sleep $sleepDuration
    }

    return $clusterSecret
}

function Set-KvaGMSAWebhook
{
    <#
    .DESCRIPTION
        Internal helper function that installs gMSA webhook for a cluster.

    .PARAMETER Name
        Cluster Name

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding(PositionalBinding=$False)]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    # ensures cluster exists
    Get-KvaCluster -Name $Name -activity $activity | Out-Null

    try
    {
        # Schedules a gmsa webhook deployment onto the cluster master node
        $yaml = @"
apiVersion: msft.microsoft/v1
kind: AddOn
metadata:
  name: gmsa-webhook-$Name
  labels:
    msft.microsoft/capicluster-name: $Name
spec:
  configuration:
    supportedAddOnName: gmsa-webhook
    targetNamespace: kube-system
    templateType: yaml
"@

        $yamlFile = $($global:config[$moduleName]["installationPackageDir"]+"\"+$global:yamlDirectoryName+"\$Name-gmsa-webhook.yaml")
        Set-Content -Path $yamlFile -Value $yaml -ErrorVariable err
        if ($null -ne $err -and $err.count -gt 0)
        {
            throw $err
        }
        Invoke-Kubectl -arguments $("apply -f $yamlFile")

        $rando = Get-Random
        $targetClusterKubeconfig = $($env:USERPROFILE+"\.kube\$Name-kubeconfig-$rando")
        Get-KvaClusterCredential -Name $Name -outputLocation $targetClusterKubeconfig

        # wait for gmsa-webhook to be ready
        $sleepDuration = 5
        $namespace = 'kube-system'
        $selector = 'app=gmsa-webhook'
        Write-SubStatus $("Waiting for gmsa-webhook pod to be ready...") -moduleName $moduleName

        while($true)
        {
            $result = (Invoke-Kubectl -ignoreError -kubeconfig $targetClusterKubeconfig -arguments $("wait --for=condition=Ready --timeout=5m -n $namespace pod -l $selector"))
            if ($null -ne $result)
            {
                break
            }
            Start-Sleep $sleepDuration
        }

        Write-SubStatus $("Pod '$podFriendlyName' is ready.`n") -moduleName $moduleName
    }
    finally
    {
        Remove-Item -Path $targetClusterKubeconfig
    }
}

function Reset-KvaGMSAWebhook
{
    <#
    .DESCRIPTION
        Internal helper function that uninstalls gMSA webhook for a cluster.

    .PARAMETER Name
        Cluster Name

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    # ensures cluster exists
    Get-KvaCluster -Name $Name -activity $activity | Out-Null

    if (-not (Invoke-Kubectl -arguments $("get addon gmsa-webhook-$Name") -ignoreError)) {
        throw "Addon gmsa-webhook-$($Name) not installed"
    }

    try
    {
        $yaml = @"
apiVersion: msft.microsoft/v1
kind: AddOn
metadata:
  name: gmsa-webhook-$Name
  labels:
    msft.microsoft/capicluster-name: $Name
spec:
  configuration:
    supportedAddOnName: gmsa-webhook
    targetNamespace: kube-system
    templateType: yaml
"@


        $yamlFile = $($global:config[$moduleName]["installationPackageDir"]+"\"+$global:yamlDirectoryName+"\$Name-gmsa-webhook.yaml")
        Set-Content -Path $yamlFile -Value $yaml -ErrorVariable err
        if ($null -ne $err -and $err.count -gt 0)
        {
            throw $err
        }

        Invoke-Kubectl -arguments $("delete -f $yamlFile")
    }
    finally
    {
        Remove-Item $yamlFile
    }
}

function Set-KvaGMSACredentialSpec
{
<#
    .DESCRIPTION
        Helper function to install setup for gmsa deployments on a cluster.

    .PARAMETER Name
        Cluster Name

    .PARAMETER credSpecFilePath
        File Path of the JSON cred spec file

    .PARAMETER credSpecName
        Name of the k8s credential spec object the user would like to designate

    .PARAMETER secretName
        Name of the Kubernetes secret object storing the Active Directory user credentials and gMSA domain

    .PARAMETER secretNamespace
        Namespace where the Kubernetes secret object resides in

    .PARAMETER serviceAccount
        Name of the K8s service account assigned to read the k8s gMSA credspec object

    .PARAMETER clusterRoleName
        Name of the K8s clusterrole assigned to use the k8s gMSA credspec object

    .PARAMETER overwrite
        Overwrites existing gMSA setup

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification='Not a plaintext password')]
    [CmdletBinding(PositionalBinding=$False, SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$true)]
        [String]$Name,

        [Parameter(Mandatory=$true)]
        [String]$credSpecFilePath,

        [Parameter(Mandatory=$true)]
        [String]$credSpecName,

        [Parameter(Mandatory=$true)]
        [String]$secretName,

        [Parameter()]
        [String]$secretNamespace = "default",

        [Parameter()]
        [String]$serviceAccount = "default",

        [Parameter(Mandatory=$true)]
        [String]$clusterRoleName,

        [Parameter()]
        [switch]$overwrite,

        [Parameter()]
        [String]$activity
    )


    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    try
    {
        $rando = Get-Random

        $targetClusterKubeconfig = $($env:USERPROFILE+"\.kube\$Name-kubeconfig-$rando")
        Get-KvaClusterCredential -Name $Name -outputLocation $targetClusterKubeconfig

        # Converting credSpecName, secretName, serviceAccount, and clusterRoleName to lowercase to conform with RFC1123
        $credSpecName = $credSpecName.ToLower()
        $secretName = $secretName.ToLower()
        $serviceAccount = $serviceAccount.ToLower()
        $clusterRoleName = $clusterRoleName.ToLower()
        $roleBindingName = 'allow-' + $serviceAccount + '-svc-account-read-on-' + $credSpecName

        # check if namespace is created
        if (-not (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get namespace $secretNamespace") -ignoreError))
        {
            if ($PSCmdlet.ShouldProcess("kubectl get namespace $secretNamespace", $secretNamespace, "namespace check"))
            {
                throw "Namespace $secretNamespace not created. Please run kubectl create namespace $secretNamespace to create the namespace."
            }
        }

        # check if a gmsa webhook pod is running
        $webhookPodRunning = (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get pods -n kube-system") -ignoreError) | Select-String -Pattern 'gmsa-webhook.*1/1.*Running'
        if (-not $webhookPodRunning)
        {
            throw "GMSA webhook not installed. Please install the gMSA webhook."
        }

        # check if serviceaccount exists
        if (-not (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get serviceaccount $serviceAccount -n $secretNamespace") -ignoreError))
        {
            if ($PSCmdlet.ShouldProcess("kubectl get serviceaccount $serviceAccount -n $secretNamespace", "$serviceAccount in namespace $secretNamespace", "service account check"))
            {
                throw "$serviceAccount in namespace $secretNamespace not found. Please create the service account $serviceAccount in namespace $secretNamespace."
            }
        }

        # check if any of the resources already exist
        # if overwrite, delete them
        # if not overwrite, throw error
        if ($overwrite.IsPresent)
        {
            if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get GMSACredentialSpec $credSpecName") -ignoreError)
            {
                Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("delete GMSACredentialSpec $credSpecName")
            }
            if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get clusterRole $clusterRoleName") -ignoreError)
            {
                Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("delete clusterRole $clusterRoleName")
            }
            if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get roleBinding $roleBindingName -n $secretNamespace") -ignoreError)
            {
                Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("delete roleBinding $roleBindingName -n $secretNamespace")
            }
        }
        else
        {
            if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get GMSACredentialSpec $credSpecName") -ignoreError)
            {
                throw "The specified $credSpecName already exists. Rerun the cmdlet with -overwrite flag to update the credspec."
            }
            if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get clusterRole $clusterRoleName") -ignoreError)
            {
                throw "The specified $clusterRoleName already exists. Rerun the cmdlet with -overwrite flag to update the cluster role."
            }
            if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get roleBinding $roleBindingName -n $secretNamespace") -ignoreError)
            {
                throw "The specified $roleBindingName in $secretNamespace exists. Rerun the cmdlet with -overwrite flag to update the rolebinding."
            }
        }

        # check if credspec file exists
        If (-not (Test-Path -Path $credSpecFilePath)) {

            throw "File path does not exist: $credSpecFilePath"
        }

        # catch errors when get-content or convertfrom-json
        $credSpecJsonRaw = Get-Content $credSpecFilePath -raw | ConvertFrom-Json
        $adServiceAccount = $credSpecJsonRaw.DomainJoinConfig.MachineAccountName
        $domainName = $credSpecJsonRaw.DomainJoinConfig.DNSName
        $netbios = $credSpecJsonRaw.DomainJoinConfig.NetBiosName
        $gmsaSid = $credSpecJsonRaw.DomainJoinConfig.Sid
        $gmsaGuid = $credSpecJsonRaw.DomainJoinConfig.Guid

        if (-not ($adServiceAccount -and $domainName -and $netbios -and $gmsaSid -and $gmsaGuid))
        {
            throw "The credential spec JSON file $credSpecFilePath is invalid."
        }

        # create credspec yaml
        $credSpecYaml = @"
apiVersion: windows.k8s.io/v1alpha1
kind: GMSACredentialSpec
metadata:
  name: $credSpecName
credspec:
  ActiveDirectoryConfig:
    GroupManagedServiceAccounts:
    - Name: $adServiceAccount
      Scope: $netbios
    - Name: $adServiceAccount
      Scope: $domainName
    HostAccountConfig:
      PortableCcgVersion: "1"
      PluginGUID: "{FF4E3124-6831-44A0-8C34-06EC66E148C6}"
      PluginInput: SecretName=$($secretName);SecretNamespace=$($secretNamespace)
  CmsPlugins:
  - ActiveDirectory
  DomainJoinConfig:
    DnsName: $domainName
    DnsTreeName: $domainName
    Guid: $gmsaGuid
    MachineAccountName: $adServiceAccount
    NetBiosName: $netbios
    Sid: $gmsaSid
"@

        # apply and remove credspec yaml
        if ($PSCmdlet.ShouldProcess("$credspecYaml", $credSpecName, "credspec creation"))
        {
            $yamlFile = $($global:config[$modulename]["installationPackageDir"]+"\$credSpecName-$rando.yaml")
            Set-Content -Path $yamlFile -Value $credSpecYaml -ErrorVariable err
            if ($null -ne $err -and $err.count -gt 0)
            {
                Remove-Item $yamlFile
                throw $err
            }

            Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("apply -f $yamlFile")
            Remove-Item $yamlFile
        }

        # create cluster role yaml
        $clusterRoleYaml = @"
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: $clusterRoleName
rules:
- apiGroups: ["windows.k8s.io"]
  resources: ["gmsacredentialspecs"]
  verbs: ["use"]
  resourceNames: [$($credSpecName)]
"@

        # apply and remove cluster role yaml
        if ($PSCmdlet.ShouldProcess("$clusterRoleYaml", $clusterRoleName, "clusterrole creation"))
        {
            $yamlFile = $($global:config[$modulename]["installationPackageDir"]+"\$clusterRoleName-clusterrole-$rando.yaml")
            Set-Content -Path $yamlFile -Value $clusterRoleYaml -ErrorVariable err
            if ($null -ne $err -and $err.count -gt 0)
            {
                Remove-Item $yamlFile
                throw $err
            }

            Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("apply -f $yamlFile")
            Remove-Item $yamlFile
        }

        # create rolebinding yaml
        $roleBindingYaml = @"
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: allow-$($serviceAccount)-svc-account-read-on-$($credSpecName)
  namespace: $($secretNamespace)
subjects:
- kind: ServiceAccount
  name: $($serviceAccount)
  namespace: $($secretNamespace)
roleRef:
  kind: ClusterRole
  name: $clusterRoleName
  apiGroup: rbac.authorization.k8s.io
"@

        # apply and remove rolebinding yaml
        if ($PSCmdlet.ShouldProcess("$roleBindingYaml", "allow-$($serviceAccount)-svc-account-read-on-$($credSpecName)", "clusterrolebinding creation"))
        {
            # apply yaml file and delete
            $yamlFile = $($global:config[$modulename]["installationPackageDir"]+"\$clusterRoleName-rolebinding-$rando.yaml")
            Set-Content -Path $yamlFile -Value $roleBindingYaml -ErrorVariable err
            if ($null -ne $err -and $err.count -gt 0)
            {
                Remove-Item $yamlFile
                throw $err
            }

            Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("apply -f $yamlFile")
            Remove-Item $yamlFile
        }
    }
    finally
    {
        Remove-Item -Path $targetClusterKubeconfig -ErrorAction SilentlyContinue
    }
}

function Reset-KvaGMSACredentialSpec
{
    <#
    .DESCRIPTION
        Helper function to uninstall setup for gmsa deployments on a cluster.

    .PARAMETER Name
        Cluster Name

    .PARAMETER credSpecName
        Name of the k8s credential spec object the user would like to designate

    .PARAMETER serviceAccount
        K8s service account assigned to read the k8s gMSA credspec object

    .PARAMETER clusterRoleName
        Name of the K8s clusterrole assigned to use the k8s gMSA credspec object

    .PARAMETER secretNamespace
        Namespace where the Kubernetes secret object resides in

    .PARAMETER activity
        Activity name to use when writing progress
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification='Not a plaintext password')]
    [CmdletBinding(PositionalBinding=$False, SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$true)]
        [String]$Name,

        [Parameter(Mandatory=$true)]
        [String]$credSpecName,

        [Parameter()]
        [String]$serviceAccount = "default",

        [Parameter(Mandatory=$true)]
        [String]$clusterRoleName,

        [Parameter()]
        [String]$secretNamespace = "default",

        [Parameter()]
        [String]$activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    try
    {
        $noDeleteCount = 0
        $rando = Get-Random

        $targetClusterKubeconfig = $($env:USERPROFILE+"\.kube\$Name-kubeconfig-$rando")
        Get-KvaClusterCredential -Name $Name -outputLocation $targetClusterKubeconfig

        # check if namespace is created
        if (-not (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get namespace $secretNamespace") -ignoreError))
        {
            if ($PSCmdlet.ShouldProcess("kubectl get namespace $secretNamespace", $secretNamespace, "namespace check"))
            {
                throw "Namespace $secretNamespace not created. Please run kubectl create namespace $secretNamespace to create the namespace."
            }
        }

        # delete GMSACredentialSpec object if exists
        if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get GMSACredentialSpec $credSpecName") -ignoreError)
        {
            if ($PSCmdlet.ShouldProcess("kubectl delete GMSACredentialSpec $credSpecName", $credSpecName, "credspec deletion"))
            {
                Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("delete GMSACredentialSpec $credSpecName")
            }
        }
        else
        {
            $noDeleteCount += 1
            Write-SubStatus -moduleName $moduleName "GMSACredentialSpec $credSpecName does not exist, skipping deletion"
        }

        # delete clusterrole object if exists
        if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get clusterRole $clusterRoleName") -ignoreError)
        {
            if ($PSCmdlet.ShouldProcess("kubectl delete $clusterRoleName", $clusterRoleName, "clusterrole deletion"))
            {
                Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("delete clusterRole $clusterRoleName")
            }
        }
        else
        {
            $noDeleteCount += 1
            Write-SubStatus -moduleName $moduleName "ClusterRole $clusterRoleName does not exist, skipping deletion"
        }

        # delete rolebinding object if exists
        $roleBindingName = 'allow-' + $serviceAccount + '-svc-account-read-on-' + $credSpecName
        if (Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("get roleBinding $roleBindingName -n $secretNamespace") -ignoreError)
        {
            if ($PSCmdlet.ShouldProcess("kubectl delete $roleBindingName -n $secretNamespace", "allow-$($serviceAccount)-svc-account-read-on-$($credSpecName)", "clusterrolebinding deletion"))
            {
                Invoke-Kubectl -kubeconfig $targetClusterKubeconfig -arguments $("delete roleBinding $roleBindingName -n $secretNamespace")
            }
        }
        else
        {
            $noDeleteCount += 1
            Write-SubStatus -moduleName $moduleName "Rolebinding $roleBindingName in $secretNamespace does not exist, skipping deletion"
        }

        # give feedback on no objects were deleted
        if ($noDeleteCount -eq 3)
        {
            Write-SubStatus -moduleName $moduleName "GMSACredentialSpec $credSpecName, clusterrole $clusterRole, and rolebinding $rolebindingName do not exist, no objects were deleted"
        }
    }
    finally
    {
        Remove-Item -Path $targetClusterKubeconfig -ErrorAction SilentlyContinue
    }
}
#endregion

#region Image functions

function Get-ImageReleaseManifest
{
    <#
    .DESCRIPTION
        Discovers the requested image and returns the release manifest for it

    .PARAMETER imageVersion
        Image release version to target

    .PARAMETER operatingSystem
        Image operating system to target

    .PARAMETER k8sVersion
        Kubernetes version to target
    #>


    param(
        [parameter(Mandatory=$true)]
        [String] $imageVersion,

        [parameter(Mandatory=$true)]
        [String] $operatingSystem,

        [Parameter(Mandatory=$true)]
        [String] $k8sVersion
    )

    $k8sVersion = $k8sVersion.TrimStart("v")

    $productRelease = Get-ProductRelease -version $imageVersion -moduleName $moduleName
    foreach($releaseStream in $productRelease.ProductStreamRefs)
    {
        foreach($subProductRelease in $releaseStream.ProductReleases)
        {
            $vhdInfo = Get-ImageReleaseVhdInfo -release $subProductRelease
            if (-not $vhdInfo)
            {
                continue
            }

            if ($vhdInfo.CustomData.BaseOSImage.OperatingSystem -ine $operatingSystem)
            {
                continue
            }

            foreach($pkg in $vhdInfo.CustomData.K8SPackages)
            {
                if ($pkg.Version -ieq $k8sVersion)
                {
                    return $subProductRelease
                }
            }
        }
    }

    throw "Unable to locate a image release with Version: $imageVersion OS: $operatingSystem K8sVersion: $k8sVersion"
}

function Get-ImageReleaseVhdInfo
{
    <#
    .DESCRIPTION
        Discovers and returns vhd information for the specified image release

    .PARAMETER imageRelease
        Image release manifest
    #>


    param(
        [parameter(Mandatory=$True)]
        [PSCustomObject] $release
    )

    foreach ($fileRelease in $release.ProductFiles)
    {
        if ($fileRelease.CustomData.Type -ieq "vhd")
        {
            return $fileRelease
        }
    }

    return $null
}

function Get-ImageRelease
{
    <#
    .DESCRIPTION
        Download the specified image release.

    .PARAMETER imageRelease
        Image release manifest

    .PARAMETER imageDir
        Directory for local image store.
    #>


    param(
        [parameter(Mandatory=$True)]
        [PSCustomObject] $imageRelease,

        [parameter(Mandatory=$True)]
        [String] $imageDir
    )

    $downloadpath = "$imageDir\$([System.IO.Path]::GetRandomFileName().Split('.')[0])"
    New-Item -ItemType Directory -Force -Confirm:$false -Path $downloadpath | Out-Null

    try
    {
        $vhdInfo = Get-ImageReleaseVhdInfo -release $imageRelease
        $k8sVersion = $vhdInfo.CustomData.K8SPackages[0].Version
        $imageGalleryName = Get-KubernetesGalleryImageName -imagetype $vhdInfo.CustomData.BaseOSImage.OperatingSystem -k8sVersion $k8sVersion

        $imageVersionCurrent = $imageRelease.Version
        $imageVersionManifestPath = "$ImageDir\$imageGalleryName.json"
        $destinationpath = $("$ImageDir\$imageGalleryName.vhdx" -replace "\/", "\")
        if (Test-Path $imageVersionManifestPath)
        {
            $OldImageData = get-content $imageVersionManifestPath | ConvertFrom-Json
            $OldImageVersion = $OldImageData.Version
            Write-SubStatus -moduleName $moduleName "Existing image $imageVersionManifestPath has version $OldImageVersion . Requested version is $imageVersionCurrent"

            if ($imageVersionCurrent -ieq $OldImageVersion)
            {
                if (Test-Path -Path $destinationpath)
                {
                    Write-SubStatus -moduleName $moduleName "Existing image is present and up to date. Skipping download."
                    return $destinationpath
                }

                Write-SubStatus -moduleName $moduleName "Existing image is not present. Proceeding to download..."
            }
        }

        Write-Status -moduleName $moduleName $("Downloading $($imageRelease.ProductStream) (version $imageVersionCurrent) to $downloadPath")

        $downloadParams = Get-ReleaseDownloadParameters -name $imageRelease.ProductStream -version $imageVersionCurrent -destination $downloadPath -parts 10 -moduleName $moduleName
        $releaseInfo = Get-DownloadSdkRelease @downloadParams

        if ($global:config[$moduleName]["useStagingShare"])
        {
            $imageFile = $releaseInfo.Files[0] -replace "\/", "\"
        }
        else
        {
            $imageFile = Expand-SfsImage -files $releaseInfo.Files -destination $downloadPath -workingDirectory $downloadPath
        }

        $imageJson = $imageRelease | ConvertTo-Json -depth 100
        Set-Content -path $imageversionManifestPath -value $imageJson -encoding UTF8 -Confirm:$false

        if (test-path $destinationpath)
        {
            Remove-Item $destinationpath -Force -Confirm:$false
        }

        Write-SubStatus -moduleName $moduleName "Moving image from $imageFile to $destinationpath"
        Move-Item -Path $imageFile -Destination $destinationpath -Confirm:$false
    }
    finally
    {
        Remove-Item -Force $downloadpath -Recurse -Confirm:$false
    }

    return $destinationpath
}

function Expand-SfsImage
{
    <#
    .DESCRIPTION
        Expand and verify a SFS image download using authenticode.
        NOTE: This is temporary until cross-platform signing is available in Download SDK.

    .PARAMETER files
        The downloaded image files (expected to be a image file zip and a companion cab).

    .PARAMETER destination
        Destination for the expanded image file.

    .PARAMETER workingDirectory
        Working directory to use for zip and cab file expansion.
    #>


    param(
        [Parameter(Mandatory=$True)]
        [String[]] $files,

        [Parameter(Mandatory=$True)]
        [String] $destination,

        [Parameter(Mandatory=$True)]
        [String] $workingDirectory
    )

    Write-Status -moduleName $moduleName "Verifying image companion file download"

    [String[]]$cabfile = $files | Where-Object { $_ -match "\.cab$"}
    if (($null -eq $cabfile) -or ($cabfile.count -ne 1))
    {
        throw $("Unexpected number of .cab files were downloaded - count: " + $cabfile.count)
    }

    Write-SubStatus -moduleName $moduleName $("Verifying authenticode signature for $cabFile")
    $auth = Get-AuthenticodeSignature -filepath $cabfile
    if (($global:expectedAuthResponse.status -ne $auth.status) -or ($global:expectedAuthResponse.SignatureType -ne $auth.SignatureType))
    {
        throw $("KVA image companion file failed authenticode verification. Expected status=$($global:expectedAuthResponse.status) and type=$($global:expectedAuthResponse.SignatureType) but received status=$($auth.status) and type=$($auth.SignatureType)")
    }

    Write-Status -moduleName $moduleName "Expanding image companion file"
    $expandDir = "$WorkingDirectory\expand_" + [System.IO.Path]::GetRandomFileName().Split('.')[0]
    New-Item -ItemType Directory -Force -Confirm:$false -Path $expandDir | Out-Null
    $expandoutput = expand.exe $cabfile $expandDir
    Write-SubStatus -moduleName $moduleName $("Expand output: $expandoutput")
    $manifest = Get-ChildItem $expandDir | select-object -first 1
    $manifestcontents = get-content $manifest.fullname | convertfrom-json

    $packageAlgo = $manifestcontents.PackageVerification.VerificationDescriptor.Algorithm
    $packageHash = $manifestcontents.PackageVerification.VerificationDescriptor.FileHash
    $packageName = $manifestcontents.PackageVerification.VerificationDescriptor.Filename

    Write-Status -moduleName $moduleName "Verifying image file download (compressed archive)"
    Write-SubStatus -moduleName $moduleName $("Companion file requests verification of package: $packageName using algorithm: $packageAlgo and hash: $packageHash")

    [string[]]$imagezip = $files | Where-Object { $_ -match "$packageName$"}
    if ($null -eq $imagezip)
    {
        throw $("Unable to locate downloaded image file archive: $packageName")
    }

    Write-SubStatus -moduleName $moduleName $("Calculating $packageAlgo hash for image file archive: " + $imagezip[0])
    $hash = Get-Base64Hash -file $imagezip[0] -algorithm $packageAlgo
    if ($packageHash -ne $hash)
    {
        throw "KVA image file archive has an unexpected hash. Expected hash: $packageHash but the downloaded file $($imagezip[0]) has hash: $hash"
    }

    Write-Status -moduleName $moduleName "Expanding image file archive"
    $contentsDir = "$expandDir\contents"
    New-Item -ItemType Directory -Force -Confirm:$false -Path $contentsdir | Out-Null
    Expand-Archive -path $imagezip[0] -destinationpath $contentsDir -Confirm:$false | Out-Null
    Remove-Item $imagezip[0] -Confirm:$false

    [System.IO.FileInfo[]]$workimage = Get-ChildItem -r $contentsDir | Where-Object { (-not $_.psiscontainer) -and ($_.name -match "\.vhdx$")}
    if ($workimage.count -ne 1)
    {
        throw $("Expected 1 image file after expansion but found $($workimage.count)")
    }
    $content0 = $manifestcontents.ContentVerification[0].VerificationDescriptor

    Write-SubStatus -moduleName $moduleName $("Calculating $packageAlgo hash for image file: " + $workimage[0].fullname)
    $hash = Get-Base64Hash -file $workimage[0].fullname -algorithm $content0.algorithm
    if ($content0.FileHash -ne $hash)
    {
        throw "KVA image file has an unexpected hash. Expected hash: $($content0.FileHash) but the downloaded file $($workimage[0].fullname) has hash: $hash"
    }

    $image = $("$destination\" + $workimage[0].name)

    Write-SubStatus -moduleName $moduleName $("Moving image file to destination: $image")
    Move-item -Path $workimage[0].fullname -Destination $image -Confirm:$false

    return $image
}

function Get-Base64Hash
{
    <#
    .DESCRIPTION
        Obtain the base64 byte hash of the specified file. Used to verify SFS binaries

    .PARAMETER file
        File to generate the base64 hash for

    .PARAMETER algorithm
        Hashing algorithm to use
    #>

    param (
        [Parameter(Mandatory=$True)]
        [string] $file,

        [Parameter(Mandatory=$True)]
        [string] $algorithm
    )

    Write-SubStatus -moduleName $moduleName $("Generating base64 hash of $file using algorithm $algorithm")
    $diskhash_hex = Get-FileHash -algo $algorithm -path $file

    [byte[]] $diskhash_bin = for ([int]$i = 0; $i -lt $diskhash_hex.hash.length; $i += 2) {
        [byte]::parse($diskhash_hex.hash.substring($i, 2), [System.Globalization.NumberStyles]::HexNumber)
    }

    return [convert]::ToBase64String($diskhash_bin)
}

function Get-GraphAccessToken
{
    <#
    .DESCRIPTION
        Gets the Graph Access Token
    #>

        $azContext = Get-AzContext

        $graphTokenItemResource = $global:graphEndpointResourceIdAzureCloud

        if($azContext.Environment -eq $global:azureChinaCloud)
        {
            $graphTokenItemResource = $global:graphEndpointResourceIdAzureChinaCloud
        }
        elseif($azContext.Environment -eq $global:azureUSGovernment)
        {
            $graphTokenItemResource = $global:graphEndpointResourceIdAzureUSGovernment
        }
        elseif($azContext.Environment -eq $global:azureGermanCloud)
        {
            $graphTokenItemResource = $global:graphEndpointResourceIdAzureGermancloud
        }
        elseif($azContext.Environment -eq $global:azurePPE)
        {
            $graphTokenItemResource = $global:graphEndpointResourceIdAzurePPE
        }

        return Get-AzAccessToken -ResourceUrl $graphTokenItemResource
}

function New-KvaArcConnection
{
    <#
    .DESCRIPTION
        Helper function to add the arc onboarding agent addon on a cluster.

    .PARAMETER Name
        cluster Name

    .PARAMETER tenantId
       tenant id for azure

    .PARAMETER subscriptionId
        subscription id for azure

    .PARAMETER resourceGroup
        azure resource group for connected cluster

    .PARAMETER credential
        credential for azure service principal

    .PARAMETER location
        azure location

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding(PositionalBinding=$False, DefaultParametersetName='None')]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $tenantId,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $subscriptionId,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $resourceGroup,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [PSCredential] $credential,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $location,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    # because of the parameter set we know that subid can represent the set.
    if ([string]::IsNullOrWhiteSpace($subscriptionId))
    {
        $azContext = Get-AzContext
        $subscriptionid = $azContext.Subscription.Id
        $tenantid = $azContext.Tenant.Id
        $resourcegroup = $global:config[$moduleName]["azureResourceGroup"]
        $location = $global:config[$moduleName]["azureLocation"]
    }

    # Ensure that the cluster exists
    Get-KvaCluster -Name $Name | Out-Null

    $kubeconfig = Get-KvaCredential -activity $activity
    $arguments = "cluster addons arc install"
    $arguments += " --cluster-name $Name"
    $arguments += " --kubeconfig $kubeconfig"
    $arguments += " --subscription-id $subscriptionId"
    $arguments += " --tenant-id $tenantId"
    $arguments += " --resource-group $resourceGroup"
    $arguments += " --location $location"


    Invoke-KvaCtlWithAzureContext -arguments $arguments  -showOutputAsProgress -activity $activity -credential $credential

    Write-SubStatus -moduleName $moduleName  "Arc Onboarding Agent has been installed to the cluster"
    Write-SubStatus -moduleName $moduleName  "To watch progress for the Arc Agents Onboarding run: kubectl logs job/azure-arc-onboarding -n azure-arc-onboarding --follow"

    Write-StatusWithProgress -activity $activity -status "Done" -completed -moduleName $moduleName
}

function Remove-KvaArcConnection
{
   <#
    .DESCRIPTION
        Helper function to add the arc onboarding agent addon on a cluster.

    .PARAMETER Name
        cluster Name

    .PARAMETER tenantId
       tenant id for azure

    .PARAMETER subscriptionId
        subscription id for azure

    .PARAMETER resourceGroup
        azure resource group for connected cluster

    .PARAMETER credential
        credential for azure service principal

    .PARAMETER location
        azure location

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding(PositionalBinding=$False, DefaultParametersetName='None')]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $tenantId,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $subscriptionId,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $resourceGroup,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [PSCredential] $credential,

        [Parameter(Mandatory=$true, ParameterSetName='azureoveride')]
        [String] $location,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name
    )

    Initialize-KvaEnvironment -activity $activity

    # because of the parameter set we know that subid can represent the set.
    if ([string]::IsNullOrWhiteSpace($subscriptionId))
    {
        $azContext = Get-AzContext
        $subscriptionid = $azContext.Subscription.Id
        $tenantid = $azContext.Tenant.Id
        $resourcegroup = $global:config[$moduleName]["azureResourceGroup"]
        $location = $global:config[$moduleName]["azureLocation"]
    }

    # Ensure that the cluster exists
    Get-KvaCluster -Name $Name | Out-Null

    $kubeconfig = Get-KvaCredential -activity $activity

    $arguments = "cluster addons arc uninstall"
    $arguments += " --cluster-name $Name"
    $arguments += " --kubeconfig $kubeconfig"
    $arguments += " --subscription-id $subscriptionId"
    $arguments += " --tenant-id $tenantId"
    $arguments += " --resource-group $resourceGroup"
    $arguments += " --location $location"

    Invoke-KvaCtlWithAzureContext -arguments $arguments  -showOutputAsProgress -activity $activity -credential $credential

    Write-SubStatus -moduleName $moduleName  "Arc Onboarding Agent has been uninstalled from the cluster"

    Write-StatusWithProgress -activity $activity -status "Done" -completed -moduleName $moduleName
}

function Set-KvaCsiSmb
{
    <#
    .DESCRIPTION
        Installs csi smb plugin to an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Installing csi smb to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity

    Invoke-KvaCtl -arguments "cluster addons csismb install --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi smb installation complete"

    Write-Status -moduleName $moduleName  "Done."

}


function Set-KvaCsiNfs
{
    <#
    .DESCRIPTION
        Installs csi nfs plugin to an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Installing csi nfs to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity

    Invoke-KvaCtl -arguments "cluster addons csinfs install --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi nfs installation complete"

    Write-Status -moduleName $moduleName  "Done."
}

function Reset-KvaCsiSmb
{
    <#
    .DESCRIPTION
        Uninstalls csi smb plugin from an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Uninstalling csi smb to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons csismb uninstall --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi smb uninstallation complete"

    Write-Status -moduleName $moduleName  "Done."
}


function Reset-KvaCsiNfs
{
    <#
    .DESCRIPTION
        Uninstalls csi nfs plugin from an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Uninstalling csi nfs to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons csinfs uninstall --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi nfs uninstallation complete"

    Write-Status -moduleName $moduleName  "Done."
}

function Set-KvaCsiSmb
{
    <#
    .DESCRIPTION
        Installs csi smb plugin to an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Installing csi smb to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons csismb install --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi smb installation complete"

    Write-Status -moduleName $moduleName  "Done."

}


function Set-KvaCsiNfs
{
    <#
    .DESCRIPTION
        Installs csi nfs plugin to an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Installing csi nfs to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons csinfs install --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi nfs installation complete"

    Write-Status -moduleName $moduleName  "Done."
}

function Reset-KvaCsiSmb
{
    <#
    .DESCRIPTION
        Uninstalls csi smb plugin from an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Uninstalling csi smb to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity

    Invoke-KvaCtl -arguments "cluster addons csismb uninstall --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi smb uninstallation complete"

    Write-Status -moduleName $moduleName  "Done."
}


function Reset-KvaCsiNfs
{
    <#
    .DESCRIPTION
        Uninstalls csi nfs plugin from an AKS-HCI cluster.

    .PARAMETER ClusterName
        Cluster Name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $ClusterName
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $ClusterName"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $ClusterName -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Uninstalling csi nfs to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons csinfs uninstall --cluster-name $ClusterName --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Csi nfs uninstallation complete"

    Write-Status -moduleName $moduleName  "Done."
}
function Set-KvaHciMonitoring
{
    <#
    .DESCRIPTION
        Installs monitoring infrastructure on AKS-HCI cluster.

    .PARAMETER Name
        Cluster Name

    .PARAMETER storageSizeGB
        Amount of storage for Prometheus in GB

    .PARAMETER retentionTimeHours
        metrics retention time in hours. (min 2 hours, max 876000 hours(100 years))

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter(Mandatory=$true)]
        [int] $storageSizeGB,

        [Parameter(Mandatory=$true)]
        [int] $retentionTimeHours,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }
    
    $hundredYearsInHours = 876000

    if (($retentionTimeHours -lt 2) -or ($retentionTimeHours -gt $hundredYearsInHours))
    {
        throw "Please provide retentionTimeHours in range"
    }
    if ($storageSizeGB -lt 1)
    {
        throw "storageSizeGB value should be greater than zero"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $Name -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Installing monitoring to cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons akshci-monitoring install --cluster-name $Name --kubeconfig $kubeconfig --storageSizeGB $storageSizeGB --retentionTimeHours $retentionTimeHours" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Monitoring installation complete"

    Write-Status -moduleName $moduleName  "Done."
}
function Reset-KvaHciMonitoring
{
    <#
    .DESCRIPTION
        Uninstalls monitoring infrastructure from AKS-HCI cluster.

    .PARAMETER Name
        Cluster Name

    .PARAMETER activity
        Activity name to use when updating progress
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String] $Name,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $Name"
    }

    Initialize-KvaEnvironment -activity $activity

    Get-KvaCluster -Name $Name -activity $activity | Out-Null

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Uninstalling monitoring from cluster..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "cluster addons akshci-monitoring uninstall --cluster-name $Name --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-SubStatus -moduleName $moduleName  "Monitoring Uninstallation complete"

    Write-Status -moduleName $moduleName  "Done."
}
function Test-KvaAzureConnection
{
     <#
    .DESCRIPTION
       Tests the connection to Azure.
    #>


    $azContext = Get-AzContext

    if([string]::IsNullOrEmpty($azContext.Subscription.Id))
    {
        throw "Connection to Azure failed. Please run 'Set-AksHciRegistration' and try again." 
    }
     
    try 
    {
        Get-AzSubscription -SubscriptionId $azContext.Subscription.Id -ErrorAction Stop | Out-Null
    }
    catch [Exception]
    {
        throw "Connection to Azure failed. Please run 'Set-AksHciRegistration' and try again." 
    }


    $aksHciRegistration = Get-AksHciRegistration
    if ([string]::IsNullOrWhiteSpace($aksHciRegistration.azureResourceGroup))
    {
        throw "No Azure Resource Group Found. Please run 'Set-AksHciRegistration' and try again." 
    }

}

#endregion

#region Cluster Networking commands
function New-KvaClusterNetwork
{
    <#
    .DESCRIPTION
        Create network settings to be used for the target clusters.

    .PARAMETER name
        The name of the vnet

    .PARAMETER vswitchName
        The name of the vswitch

    .PARAMETER vlanID
        The VLAN ID for the vnet

    .PARAMETER ipaddressprefix
        The address prefix to use for static IP assignment

    .PARAMETER gateway
        The gateway to use when using static IP

    .PARAMETER dnsservers
        The dnsservers to use when using static IP

    .PARAMETER vippoolstart
        The starting ip address to use for the vip pool.
        The vip pool addresses will be used by the k8s API server and k8s services'

    .PARAMETER vippoolend
        The ending ip address to use for the vip pool.
        The vip pool addresses will be used by the k8s API server and k8s services

    .PARAMETER k8snodeippoolstart
        The starting ip address to use for VM's in the cluster.

    .PARAMETER k8snodeippoolend
        The ending ip address to use for VM's in the cluster.

    .OUTPUTS
        VirtualNetwork object

    .NOTES
        The cmdlet will throw an exception if the mgmt cluster is not up.

    .EXAMPLE
        $clusterVNetDHCP = New-AksHciClusterNetwork -name e1 -vswitchName External -vippoolstart 172.16.0.0 -vippoolend 172.16.0.240

    .EXAMPLE
        $clusterVNetStatic = New-AksHciClusterNetwork -name e1 -vswitchName External -ipaddressprefix 172.16.0.0/24 -gateway 172.16.0.1 -dnsservers 4.4.4.4, 8.8.8.8 -vippoolstart 172.16.0.0 -vippoolend 172.16.0.240
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $name,

        [Parameter(Mandatory=$true)]
        [string] $vswitchName,

        [Parameter(Mandatory=$false)]
        [int] $vlanID = $global:defaultVlanID,

        [Parameter(Mandatory=$false)]
        [String] $ipaddressprefix,

        [Parameter(Mandatory=$false)]
        [String] $gateway,

        [Parameter(Mandatory=$false)]
        [String[]] $dnsservers,

        [Parameter(Mandatory=$true)]
        [String] $vippoolstart,

        [Parameter(Mandatory=$true)]
        [String] $vippoolend,

        [Parameter(Mandatory=$false)]
        [String] $k8snodeippoolstart,

        [Parameter(Mandatory=$false)]
        [String] $k8snodeippoolend,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $name"
    }

    Initialize-KvaEnvironment -activity $activity

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Validating network configuration..."

    $vnet = New-VirtualNetwork -name $name -vswitchName $vswitchName -vlanID $vlanID -ipaddressprefix $ipaddressprefix -gateway $gateway -dnsservers $dnsservers -vippoolstart $vippoolstart -vippoolend $vippoolend -k8snodeippoolstart $k8snodeippoolstart -k8snodeippoolend $k8snodeippoolend

    $dnsserver = ""
    if(-Not [string]::IsNullOrEmpty($vnet.IpAddressPrefix))
    {
        foreach ($dns in $vnet.DnsServers)
        {
            if(-Not [string]::IsNullOrEmpty($dns))
            {
                $dnsserver += "`n - " +  $dns
            }
        }
    }

    $yaml = @"
virtualnetwork:
  name: "$($vnet.Name)"
  vswitchname: "$($vnet.VswitchName)"
  type: "Transparent"
  vlanid: $($vnet.VlanID)
  ipaddressprefix: $($vnet.IpAddressPrefix)
  gateway: $($vnet.Gateway)
  dnsservers: $dnsserver
  vippoolstart: $($vnet.VipPoolStart)
  vippoolend: $($vnet.VipPoolEnd)
  k8snodeippoolstart: $($vnet.K8snodeIPPoolStart)
  k8snodeippoolend: $($vnet.K8snodeIPPoolEnd)
"@


    $yamlFile = $($global:config[$modulename]["installationPackageDir"]+"\"+$global:yamlDirectoryName+"\$name.yaml")
    Set-Content -Path $yamlFile -Value $yaml -ErrorVariable err
    if ($null -ne $err -and $err.count -gt 0)
    {
        throw $err
    }
    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Creating network..."
    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "network create --networkconfig $yamlFile --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-Status -moduleName $moduleName  "Done."
    return $vnet
}

function Remove-KvaClusterNetwork
{
    <#
    .DESCRIPTION
        Remove a virtual network object for a target cluster

    .PARAMETER name
        The name of the vnet

    .NOTES
        The cmdlet will throw an exception if the network is still being used.
        The cmdlet will throw an exception if the mgmt cluster is not up.

    .EXAMPLE
        Remove-AksHciClusterNetwork -name e1
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $name,

        [Parameter()]
        [String] $activity
    )


    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $name"
    }

    Initialize-KvaEnvironment -activity $activity

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status "Removing network..."
    Write-SubStatus -moduleName $moduleName  "Netowrk removal is in progress..."

    $kubeconfig = Get-KvaCredential -activity $activity
    Invoke-KvaCtl -arguments "network delete --networkname $name --kubeconfig $kubeconfig" -showOutput -activity $activity

    Write-Status -moduleName $moduleName  "Done."
}

function Get-KvaClusterNetwork
{
    <#
    .DESCRIPTION
        Gets the VirtualNetwork object for a target cluster given either the vnet name or the cluster name. If no parameter is given, all vnet's are returned.

    .PARAMETER name
        The name of the vnet

    .PARAMETER clusterName
        The name of the cluster (NOTE: This is P2 -- but we really want to add this functionality for Ben)

    .OUTPUTS
        If name is specified, the VirtualNetwork object will be returned.
        If clusterName is specified, the VirtualNetwork object that the cluster is using will be returned.
        If no parameters are specified all VirtualNetwork objects will be returned.

    .NOTES
        The cmdlet will throw an exception if the mgmt cluster is not up.

    .EXAMPLE
        $clusterVNet = Get-AksHciClusterNetwork -name e1

    .EXAMPLE
        $clusterVNet = Get-AksHciClusterNetwork -clusterName myTargetCluster

    .EXAMPLE
        $allClusterVNets = Get-AksHciClusterNetwork
    #>


    param (
        [Parameter(Mandatory=$false)]
        [string] $name,

        [Parameter(Mandatory=$false)]
        [string] $clusterName,

        [Parameter()]
        [String] $activity
    )

    if (-not $activity)
    {
        $activity = "$($MyInvocation.MyCommand.Name) - $name"
    }

    Initialize-KvaEnvironment -activity $activity

    Write-StatusWithProgress -activity $activity -status "Retrieving network..." -moduleName $moduleName

    Write-Status $("Retrieving configuration for network '$name'") -moduleName $global:KvaModule
    $retrievedVnet = Get-KvaClusterNetworkInternal -name $name -clusterName $clusterName -activity $activity
    Write-SubStatus "Successfully retrieved network information." -moduleName $global:KvaModule
    return $retrievedVnet
}

function Get-KvaClusterNetworkInternal
{
    <#
    .DESCRIPTION
        This function is similar to Get-KvaNetworkCluster excpet it does not update the powershell
        status. It is intended to be called by other cmdlets that do update the status

    .PARAMETER name
        The name of the vnet

    .PARAMETER clusterName
        The name of the cluster (NOTE: This is P2 -- but we really want to add this functionality for Ben)

    .OUTPUTS
        If name is specified, the VirtualNetwork object will be returned.
        If clusterName is specified, the VirtualNetwork object that the cluster is using will be returned.
        If no parameters are specified all VirtualNetwork objects will be returned.

    .NOTES
        The cmdlet will throw an exception if the mgmt cluster is not up.

    .EXAMPLE
        $clusterVNet = Get-AksHciClusterNetwork -name e1

    .EXAMPLE
        $clusterVNet = Get-AksHciClusterNetwork -clusterName myTargetCluster

    .EXAMPLE
        $allClusterVNets = Get-AksHciClusterNetwork
    #>


    param (
        [Parameter(Mandatory=$false)]
        [string] $name,

        [Parameter(Mandatory=$false)]
        [string] $clusterName,

        [Parameter()]
        [String] $activity
    )

    $kubeconfig = Get-KvaCredential -activity $activity
    $queryParam = ""
    if ($name) {
        $queryParam = "--networkname $name"
    } elseif ($clusterName) {
        $queryParam = "--clustername $clusterName"
    }

    $kvaVnets = Invoke-KvaCtl -arguments "network get $queryParam --kubeconfig $kubeconfig --outputformat json" -activity $activity | ConvertFrom-Json
    $vnet = @()
    foreach ($kvaVnet in $kvaVnets) {
       $vnet += [VirtualNetwork]::new($kvaVnet.virtualnetwork.name, $kvaVnet.virtualnetwork.vswitchname, $kvaVnet.virtualnetwork.ipaddressprefix, $kvaVnet.virtualnetwork.gateway, $kvaVnet.virtualnetwork.dnsservers, $kvaVnet.virtualnetwork.macpoolname, $kvaVnet.virtualnetwork.vlanid, $kvaVnet.virtualnetwork.vippoolstart, $kvaVnet.virtualnetwork.vippoolend, $kvaVnet.virtualnetwork.k8snodeippoolstart, $kvaVnet.virtualnetwork.k8snodeippoolend)
    }
    return $vnet
}

#endregion

# SIG # Begin signature block
# MIIjhQYJKoZIhvcNAQcCoIIjdjCCI3ICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB5vhYIWCr7ftax
# 05z3NkpwUCZGAkqE6zCw+JYq8gGP5aCCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVWjCCFVYCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgkL0D9B4J
# C8glMNz0oR5pvrM6Ic/j38hNtEKl6WmG6MMwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQACPfqjF5JlSZCVfvl2Vm+dZ4IeVurXAqRz4ZxZwlpa
# GK314TlX65WUWVeeQ/Unoro2lR5ljwks2jViopRD/CkUfEhie96SsuBkQhUdPdoc
# /TQCIyCl58pT/+JE1ay0UGbo8gq+fnX8bg7oNuajkeF644IxXuQ17AvyBMjoVXrR
# F2sF5AJi7UNaGYTbfW3aGJQX1HcSBVUpeTN9xx6ir1CEBq23Z1LNHaXo5ob0raJ/
# H58m2ca9Gr28oPb67+2Ir6cH+tC1qYjkJLXY54ardf36ZTrRmrgwDYl+GBf1fEdZ
# j+T27awwWTlI7U1oFxCMcSkKpCvksIzrjA+6+HAsAvnLoYIS5DCCEuAGCisGAQQB
# gjcDAwExghLQMIISzAYJKoZIhvcNAQcCoIISvTCCErkCAQMxDzANBglghkgBZQME
# AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIOEbfEMLP7VHcBX6B4D2xGG3ab4rUDyPjmd2QrU5
# bilvAgZgichXoWQYEzIwMjEwNTIxMjAzMDQ0LjQzOFowBIACAfSggdCkgc0wgcox
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1p
# Y3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOkU1QTYtRTI3Qy01OTJFMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNloIIOOzCCBPEwggPZoAMCAQICEzMAAAFHnY/x5t4xg1kAAAAAAUcw
# DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN
# MjAxMTEyMTgyNTU1WhcNMjIwMjExMTgyNTU1WjCByjELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046RTVBNi1FMjdDLTU5
# MkUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtBQNM6X32KFk/BJ8YaprfzEt6Lj34
# G+VLjzgfEgOGSVd1Mu7nCphK0K4oyPrzItgNRjB4gUiKq6GzgxdDHgZPgTEvm57z
# sascyGrybWkf3VVr8bqf2PIgGvwKDNEgVcygsEbuWwXz9Li6M7AOoD4TB8fl4ATm
# +L7b4+lYDUMJYMLzpiJzM745a0XHiriUaOpYWfkwO9Hz6uf+k2Hq7yGyguH8naPL
# MnYfmYIt2PXAwWVvG4MD4YbjXBVZ14ueh7YlqZTMua3n9kT1CZDsHvz+o58nsoam
# XRwRFOb7LDjVV++cZIZLO29usiI0H79tb3fSvh9tU7QC7CirNCBYagNJAgMBAAGj
# ggEbMIIBFzAdBgNVHQ4EFgQUtPjcb95koYZXGy9DPxN49dSCsLowHwYDVR0jBBgw
# FoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov
# L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENB
# XzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAx
# MC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDAN
# BgkqhkiG9w0BAQsFAAOCAQEAUMQOyjV+ea2kEtXqD0cOfD2Z2PFUIy5kLkGU53RD
# GcfhlzIR9QlTgZLqTEhgLLuCSy6jcma+nPg7e5Xg1oqCZcZJRwtRPzS1F6/M6YR3
# 5H3brN0maVnPrmrQ91kkfsNqDTtuWDiAIBfkNEgCpQZCb4OV3HMu5L8eZzg5dUaJ
# 7XE+LBuphJSLFJtabxYt4fkCQxnTD2z50Y32ZuXiNmFFia7qVq+3Yc3mmW02+/KW
# H8P1HPiobJG8crGYgSEkxtkUXGdoutwGWW88KR9RRcM/4GKLqt2OQ8AWEQb7shgM
# 8pxNvu30TxejRApa4WAfOAejTG4+KzBm67XjVZ2IlXAPkjCCBnEwggRZoAMCAQIC
# CmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRp
# ZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcwMTIx
# NDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUKAIKF
# ++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCIhFRD
# DNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5FfgVSx
# z5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx898Fd1
# rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg16Hgc
# sOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHmMIIB
# 4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8UzaFqF
# bVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
# EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYD
# VR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwv
# cHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEB
# BE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j
# ZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSBlTCB
# kjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwICMDQe
# MiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBuAHQA
# LiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+umzPUx
# vs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM9GAS
# inbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84Dxf1
# L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+tuWO
# M7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32WapB4
# pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUPei45
# V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAeb73x
# 4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4hQEe
# gPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanblrKn
# QqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLFVeNp
# 3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLraNtvT
# X4/edIhJEqGCAs0wggI2AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP
# cGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpFNUE2LUUyN0MtNTky
# RTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcG
# BSsOAwIaAxUAq6fBtEENocNASMqL03zGJS0wZd2ggYMwgYCkfjB8MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg
# VGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAORR78UwIhgPMjAy
# MTA1MjExNjM3MjVaGA8yMDIxMDUyMjE2MzcyNVowdjA8BgorBgEEAYRZCgQBMS4w
# LDAKAgUA5FHvxQIBADAIAgEAAgMAgtIwCAIBAAIDAVwQMAoCBQDkU0FFAgEAMDYG
# CisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEA
# AgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAolbZnYJjGVQjjEkJdiZ9Mp4/sNjVA/FY
# Xl6wI9fQP+CHdJaTwG/SV7T6wEDt369Z/omZOuX3tQPp8t+prGOW4VhUqwRWFbjn
# YHPy0Bkd3Q956Z3YDN2thJV5I4AyzyxbPV56bNrZ317osIFB3xP7ztL3qzarL3mj
# GQaPTMOxMLoxggMNMIIDCQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
# MjAxMAITMwAAAUedj/Hm3jGDWQAAAAABRzANBglghkgBZQMEAgEFAKCCAUowGgYJ
# KoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCA6qN+DGv6z
# Y91rwZ+bJt6c+1+Y9BEz8cV9NCvbRj/FizCB+gYLKoZIhvcNAQkQAi8xgeowgecw
# geQwgb0EIHvbPBIDlM+6BsiJk7/YfWGuKwBUi3DMOxxvRaqKGOmFMIGYMIGApH4w
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAFHnY/x5t4xg1kAAAAA
# AUcwIgQgp/uc1xBrIue3RFiedzfEHhNxybK46o64VHo1r241HG0wDQYJKoZIhvcN
# AQELBQAEggEAFNT2wb+8Z7/ZMikR5ysQXhKXj+1gBeMo+bCTuExIG6sVkyTN4w5e
# qvIg/eJothw79BCX9ZJlRp0iPQP+kSyEMxY+g2FFJEv+B5GNlZ0YV5y+6BFDOYNT
# kYdFrsGpgXynUKXup2U0CzvYOtI4oMmCqHwZBmSWktgnot8lrnQTP3oVHDxyGrLd
# FrhNI8BLO0aNu0imhMUj3LkD0SmWKmpRc3SG5CcvRNRLvF0S8cTJ1CcYTVwIDto7
# AtSoqcjEhuqtYW/qC1UHDLBN3nanZ9ATEaS1zuF2BQsd2xhBWKH2cRoVnSpeczAT
# w6D2uDk0l++KgRHU5hPOJi2zSwelgznZUg==
# SIG # End signature block