tests/build/Initialize-VstsAgentOnWindowsServerCoreContainer.ps1

##################################################################################################################
# Script Disclaimer
##################################################################################################################
# This script is not supported under any Microsoft standard support program or service.
# This script is provided AS IS without warranty of any kind.
# Microsoft disclaims all implied warranties including, without limitation, any implied warranties of
# merchantability or of fitness for a particular purpose. The entire risk arising out of the use or
# performance of this script and documentation remains with you. In no event shall Microsoft, its authors,
# or anyone else involved in the creation, production, or delivery of this script be liable for any damages
# whatsoever (including, without limitation, damages for loss of business profits, business interruption,
# loss of business information, or other pecuniary loss) arising out of the use of or inability to use
# this script or documentation, even if Microsoft has been advised of the possibility of such damages.

<#
.SYNOPSIS
    This script creates ACI container instance(s) which run the Azure DevOps (formerly VSTS) Agent, alongside with the requested
    PowerShell modules, json2hcl and Terraform to enable Azure resource build automation from Azure DevOps CI/CD pipelines.
.DESCRIPTION
    This script is used as a wrapper of the container configuration script
    ("Install-VstsAgentOnWindowsServerCoreContainer.ps1") to create ACI container instance(s) which run
    the Azure DevOps Agent, alongside with the requested PowerShell modules, json2hcl and Terraform to enable Azure resource build
    automation from Azure DevOps CI/CD pipelines.
     
    It copies the container configuration script to a publicly available storage container of the requested
    Storage Account, it creates a new Resource Group (if one doesn't exist with the provided name), removes
    any pre-existing ACI containers with the same name, within the same Resource Group, then creates new ACI
    container instance(s) based on the provided names and invokes the container configuration script inside the
    container(s).
     
    This script is designed and tested to be run from Azure Cloud Shell.
    Prerequisites:
    - Azure Subscription, with an existing Storage Account
    - You need to have **admin rights** :
    - to create a storage container within the already existing Storage Account *- OR -* you need to have
      a storage container which has its public access type configured to the type of "Blob",
    - to create a new Resource Group *- OR -* an existing Resource Group for the Azure Container Instances,
    - to create resources in the selected Resource Group.
    - Azure DevOps account with the requested Agent Pool has to exist.
    - Permission in the Azure DevOps account to add Agents to the chosen Agent Pool.
    - A PAT token.
.PARAMETER SubscriptionName
    Name of the Subscription.
.PARAMETER ResourceGroupName
    Name of the Resource Group.
.PARAMETER ContainerName
    Name of the ACI container(s).
.PARAMETER ReplaceExistingContainer
    Switch to replace existing container(s) with the same name(s) provided.
.PARAMETER MemoryInGB
    Amount of memory in GBs.
.PARAMETER Cpu
    Number of CPU cores.
.PARAMETER Location
    Region of the Azure resources.
.PARAMETER StorageAccountName
    Name of the Storage Account to upload the script file to, which is then invoke by this wrapper.
.PARAMETER StorageContainerName
    Name of the storage container to upload the script file to be invoked by this wrapper.
.PARAMETER ScriptPublicUrl
    Publicly available URL of the internal script file to be invoked by this wrapper. This can be used optionally, instead of defining the Storage Account name. You need to make the internal script available on this URL as a prerequisite.
.PARAMETER ScriptFileName
    Name of the script file to invoke by this wrapper.
.PARAMETER VSTSAccountName
    Name of the Azure Devops account - formerly Visual Studio Team Services (VSTS) account, e.g. https://<Azure DevOps Account Name>.visualstudio.com - OR - https://dev.azure.com/<Azure DevOps Account Name>/
.PARAMETER PATToken
    PPAT token generated by the user who is configuring the container to be used by Azure Devops.
.PARAMETER PoolName
    Name of the Agent pool. It defaults to the "Default" pool when not defined.
.PARAMETER RequiredPowerShellModules
    List of the required PowerShell modules, e.g. Az, AzureAD, Pester
.PARAMETER ContainerImage
    Fully qualified name of the container image, optionally including tags.
.PARAMETER AcrPassword
    Access password to the Azure Container Registry (ACR)
.PARAMETER InstallAzureCli
    Switch to define whether or not you want to install the Azure CLI on your container.
.PARAMETER InstallPowerShellCore
    Switch to define whether or not you want to install Azure PowerShell Core on your container.
.PARAMETER UseChocolatey
    Switch to define whether or not Chocolatey should be used to install the supported components
.EXAMPLE
    .\Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 -SubscriptionName "<subscription name>" -ResourceGroupName "<resource group name>" -ContainerName "<container 1 name>", "<container 2 name>" -Location "<azure region>" -StorageAccountName "<storage account name>" -VSTSAccountName "<azure devops account name>" -PATToken "<PAT token>"
    This uploads the container configuration script to the default "publicvstsscript" storage container of the requested Storage Account and then creates 2 Azure Container Instances, with the default settings (Default Agent Pool 1 GB RAM, 1 CPU core, PowerShell modules installed: "Az", "AzureAD", "Pester").
.EXAMPLE
    .\Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 -SubscriptionName "<subscription name>" -ResourceGroupName "<resource group name>" -ContainerName "<container 1 name>", "<container 2 name>" -Location "<azure region>" -VSTSAccountName "<azure devops account name>" -PATToken "<PAT token>"
    This downloads the container configuration script directly from its default location on GitHub, and then creates 2 Azure Container Instances, with the default settings (Default Agent Pool 1 GB RAM, 1 CPU core, PowerShell modules installed: "Az", "AzureAD", "Pester").
.EXAMPLE
    .\Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 -SubscriptionName "<subscription name>" -ResourceGroupName "<resource group name>" -ContainerName "<container 1 name>", "<container 2 name>" -Location "<azure region>" -VSTSAccountName "<azure devops account name>" -PATToken "<PAT token>" -ScriptPublicUrl "<public URL of the internal config script>"
    This downloads the container configuration script directly from the provided location (this can be anything, e.g. GitHub, a public Storage Account, or any publicly available URL), and then creates 2 Azure Container Instances, with the default settings (Default Agent Pool 1 GB RAM, 1 CPU core, PowerShell modules installed: "Az", "AzureAD", "Pester").
.EXAMPLE
    .\Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 -SubscriptionName "<subscription name>" -ResourceGroupName "<resource group name>" -ContainerName "<container 1 name>", "<container 2 name>" -Location "<azure region>" -StorageAccountName "<storage account name>" -StorageContainerName "publicvstsscript" -MemoryInGB 1 -Cpu 1 -ScriptFileName "Install-VstsAgentOnWindowsServerCoreContainer.ps1" -VSTSAccountName "<Azure DevOps Account name>" -PATToken "<PAT token>" -PoolName "<Azure DevOps Agent Pool name>" -RequiredPowerShellModules "Az", "AzureAD", "Pester"
    This installs 2 Azure Container Instances with all the possible values manually defined.
.EXAMPLE
    .\Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 -SubscriptionName "<subscription name>" -ResourceGroupName "<resource group name>" -ContainerName "<container 1 name>", "<container 2 name>" -Location "<azure region 2>" -StorageAccountName "<storage account name>" -VSTSAccountName "<azure devops account name>" -PATToken "<PAT token>" -PoolName "<agent pool name>" -ReplaceExistingContainer
    This removes any existing ACI containers with the provided names, then creates new ones with the requested values.
.EXAMPLE
    .\Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 -SubscriptionName "<subscription name>" -ResourceGroupName "<resource group name>" -ContainerName "<container 1 name>", "<container 2 name>" -Location "<azure region>" -StorageAccountName "<storage account name>" -VSTSAccountName "<azure devops account name>" -PATToken "<PAT token>" -ContainerImage <myacr.azurecr.io/myrepo/myimage:v1> -AcrPassword <ACR password>
    This uploads the container configuration script to the default "publicvstsscript" storage container of the requested Storage Account and then creates 2 Azure Container Instances, based on the custom image provided, with the default settings (Default Agent Pool 1 GB RAM, 1 CPU core, PowerShell modules installed: "Az", "AzureAD", "Pester").
 
.INPUTS
    <none>
.OUTPUTS
    <none>
.NOTES
    Version: 1.0
    Author: Mate Barabas
    Creation Date: 2018-08-29
#>


param(

    [Parameter(Mandatory = $true,
        HelpMessage = "Name of the Subscription.")]
    [ValidateNotNullOrEmpty()]
    [string]$SubscriptionName,

    [Parameter(Mandatory = $true,
        HelpMessage = "Name of the Resource Group.")]
    [ValidateNotNullOrEmpty()]
    [string]$ResourceGroupName,

    [Parameter(Mandatory = $false,
        HelpMessage = "Name of the ACI container(s).")]
    [ValidateNotNullOrEmpty()]
    [array]$ContainerName,

    [Parameter(Mandatory = $false,
        HelpMessage = "Switch to replace existing container(s) with the same name(s) provided.")]
    [ValidateNotNullOrEmpty()]
    [switch]$ReplaceExistingContainer,

    [Parameter(Mandatory = $false,
        HelpMessage = "Amount of memory in GBs.")]
    [ValidateNotNullOrEmpty()]
    [int]$MemoryInGB = 1,
    
    [Parameter(Mandatory = $false,
        HelpMessage = "Number of CPU cores.")]
    [ValidateNotNullOrEmpty()]
    [int]$Cpu = 1,

    [Parameter(Mandatory = $true,
        HelpMessage = "Region of the Azure resources.")]
    [ValidateNotNullOrEmpty()]
    [string]$Location,

    [Parameter(Mandatory = $false,
        HelpMessage = "Name of the Storage Account to upload the script file to, which is then invoke by this wrapper.")]
    [ValidateNotNullOrEmpty()]
    [string]$StorageAccountName,

    [Parameter(Mandatory = $false,
        HelpMessage = "Name of the Storage Account Resource Group")]
    [ValidateNotNullOrEmpty()]
    [string]$StorageAccountResourceGroupName,

    [Parameter(Mandatory = $false,
        HelpMessage = "Name of the storage container to upload the scriptfile to be invoked by this wrapper.")]
    [ValidateNotNullOrEmpty()]
    [string]$StorageContainerName = "publicvstsscript",
    
    [Parameter(Mandatory = $false,
        HelpMessage = "Publicly available URL of the internal script file to be invoked by this wrapper. This can be used optionally, instead of defining the Storage Account name. You need to make the internal script available on this URL as a prerequisite.")]
    [ValidateNotNullOrEmpty()]
    [string]$ScriptPublicUrl = "https://raw.githubusercontent.com/matebarabas/azure/master/scripts/AzureDevOpsAgentOnACI/Install-VstsAgentOnWindowsServerCoreContainer.ps1",

    [Parameter(Mandatory = $false,
        HelpMessage = "Name of the script file to invoke by this wrapper.")]
    [ValidateNotNullOrEmpty()]
    [string]$ScriptFileName = "Install-VstsAgentOnWindowsServerCoreContainer.ps1",
    
    [Parameter(Mandatory = $false,
        HelpMessage = "Path to the file - if script not running in same directory")]
    [ValidateNotNullOrEmpty()]
    [string]$ScriptFilePath = ".\",

    [Parameter(Mandatory = $true,
        HelpMessage = "Name of the Visual Studio Team Services Account (VSTS), e.g. https://<VSTSAccountName>.visualstudio.com")]
    [ValidateNotNullOrEmpty()]
    [string]$VSTSAccountName,

    [Parameter(Mandatory = $true,
        HelpMessage = "PAT token generated by the user who is configuring the container to be used by VSTS.")]
    [ValidateNotNullOrEmpty()]
    [string]$PATToken,

    [Parameter(Mandatory = $false,
        HelpMessage = "Name of the Agent pool. It defaults to the ""Default"" pool when not defined.")]
    [ValidateNotNullOrEmpty()]
    [string]$PoolName = "Default",

    [Parameter(Mandatory = $false,
        HelpMessage = "List of the required PowerShell modules, e.g. Az, AzureAD, Pester")]
    [ValidateNotNullOrEmpty()]
    [array]$RequiredPowerShellModules = @("Az", "AzureAD", "Pester"),

    [Parameter(Mandatory = $false,
        HelpMessage = "Access password to the Azure Container Registry (ACR)")]
    [ValidateNotNullOrEmpty()]
    [string]$AcrPassword,

    [Parameter(Mandatory = $false,
        HelpMessage = "Fully qualified name of the container image, optionally including tags.")]
    [ValidateNotNullOrEmpty()]
    [string]$ContainerImage, # e.g. "microsoft/dotnet-framework:4.7.2-runtime-20190212-windowsservercore-ltsc2016" or "microsoft/windowsservercore:10.0.14393.2791" or "mcr.microsoft.com/windows/servercore:ltsc2016"

    [Parameter(Mandatory = $false,
        HelpMessage = "Switch to define whether or not you want to install the Azure CLI on your container.")]
    [ValidateNotNullOrEmpty()]
    [bool]$InstallAzureCli = $false,

    [Parameter(Mandatory = $false,
        HelpMessage = "Switch to define whether or not you want to install Azure PowerShell Core on your container.")]
    [ValidateNotNullOrEmpty()]
    [bool]$InstallPowerShellCore = $false,

    [Parameter(Mandatory = $false,
        HelpMessage = "Switch to define whether or not Chocolatey should be used to install the supported components")]
    [ValidateNotNullOrEmpty()]
    [bool]$UseChocolatey = $false

)

#region Functions
function Set-AzureContext
{

    param (

        [Parameter(Mandatory = $false)][string]$SubscriptionName

    )

    # Select the desired Subscription based on the Subscription name provided
    if ($SubscriptionName)
    {
        $Subscription = (Get-AzureRmSubscription | Where-Object { $_.Name -eq $SubscriptionName })
            
        if (-not $Subscription)
        {
            Write-Error "There's no Subscription available with the provided name."
            return
        }
        else
        {
            $SubscriptionId = $Subscription.Id
            Select-AzureRmSubscription -SubscriptionId $SubscriptionId | Out-Null
            Write-Output "The following subscription was selected: ""$SubscriptionName"""
        }
    }
    # If no Subscription name was provided select the active Subscription based on the existing context
    else
    {
        $SubscriptionName = (Get-AzureRmContext).Subscription.Name
        $Subscription = (Get-AzureRmSubscription | Where-Object { $_.Name -eq $SubscriptionName })
        Write-Output "The following subscription was selected: ""$SubscriptionName"""
    }

    if ($Subscription.Count -gt 1)
    {
        Write-Error "You have more then 1 Subscription with the same name. Exiting..."
        return
    }
}

if(-not $StorageAccountResourceGroupName){
    $StorageAccountResourceGroupName = $ResourceGroupName
}

function Copy-ScriptToStorageAccount
{

    param (

        [Parameter(Mandatory = $true)][string]$StorageAccountName,
        [Parameter(Mandatory = $true)][string]$StorageAccountResourceGroupName,
        [Parameter(Mandatory = $false)][string]$StorageContainerName = "publicvstsscript",
        [Parameter(Mandatory = $false)][string]$ScriptFileName = "Install-VstsAgentOnWindowsServerCoreContainer.ps1"

    )

    # Check if the Install-VstsAgentOnWindowsServerCoreContainer.ps1 script exists within the same folder
    if (-not (Get-Item -Path "$ScriptFilePath\$ScriptFileName" -ErrorAction SilentlyContinue))
    {
        Write-Error "The script to be uploaded to the Storage Account ($ScriptFilePath\$ScriptFileName) does not exist in the same folder. Make sure that it is copied to the same folder along with the Initialize-VstsAgentOnWindowsServerCoreContainer.ps1 script. Exiting..."
        
        break
    }

    if(-not (Get-AzureRmStorageAccount -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -ErrorAction SilentlyContinue)){
        New-AzureRmStorageAccount -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -Location "West Europe" -SkuName "Standard_GRS"
    }

    # Getting Storage Account Key
    Write-Output "Getting Storage Account Key..."
    $StorageKey = Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName

    # Setting storage context
    Write-Output "Setting storage context..."
    $ctx = New-AzureStorageContext -StorageAccountName  $StorageAccountName -StorageAccountKey $StorageKey[0].Value

    # Checking if the container exists, creating if it doesn't
    $StorageContainer = Get-AzureStorageContainer -Context $ctx -Name $StorageContainerName -ErrorAction SilentlyContinue
    if (-not $StorageContainer)
    {
        Write-Output "Storage container ""$StorageContainerName"" does not exist in the ""$StorageAccountName"" Storage Account. Creating storage container with public access..."
        New-AzureStorageContainer -Context $ctx -Name $StorageContainerName -Permission Blob | Out-Null
        Write-Output "Wait 10 seconds..."
        Start-Sleep -Seconds 10
    }
    else
    {
        Write-Output "Storage container ""$StorageContainerName"" exists in the ""$StorageAccountName"" Storage Account."
        $PublicAccess = (Get-AzureStorageContainerAcl -Name $StorageContainerName -Context $ctx).PublicAccess
        if ($PublicAccess -ne "Blob")
        {
            Write-Output "Public Access was configured to $PublicAccess. Resetting it to ""Blob"" (public access)."
            Set-AzureStorageContainerAcl -Name $StorageContainerName -Permission Blob
            Write-Output "Wait 10 seconds..."
            Start-Sleep -Seconds 10
        }
    }

    # Check if container creation was successful
    $StorageContainer = Get-AzureStorageContainer -Context $ctx -Name $StorageContainerName -ErrorAction SilentlyContinue
    if (-not $StorageContainer)
    {
        Write-Error "Storage container ($StorageContainerName) could not be created in the $StorageAccountName Storage Account. Exiting..."
        break
    }

    # Uploading CSV file
    Write-Output "Uploading the Install-VstsAgentOnWindowsServerCoreContainer.ps1 script to the Storage Account..."
    Set-AzureStorageBlobContent -Container $StorageContainerName -File "$ScriptFilePath\$ScriptFileName" -Blob $ScriptFileName -context $ctx -Force | Out-Null

    # Checking success
    $Blob = Get-AzureStorageBlob -Context $ctx -Container $StorageContainerName -Blob $ScriptFileName -ErrorAction SilentlyContinue
    if ($Blob)
    {
        Write-Output "The script file ($ScriptFileName) was successfully uploaded to the ""$StorageContainerName"" container of the ""$StorageAccountName"" Storage Account in the ""$StorageAccountResourceGroupName"" Resource Group."
    }
    else
    {
        Write-Error "The script file ($ScriptFileName) could not be uploaded to the ""$StorageContainerName"" container of the ""$StorageAccountName"" Storage Account in the ""$StorageAccountResourceGroupName"" Resource Group. Exiting..."
        break
    }

}

function New-Container
{
    param (
        [Parameter(Mandatory = $false)][string]$AcrPassword,
        [Parameter(Mandatory = $true)][string]$ContainerImage
    )

    # Create & Install containers

    if ($StorageAccountName) #if you want to upload the internal script to a Storage Account
    {
        $ScriptURL = "https://$StorageAccountName.blob.core.windows.net/$StorageContainerName/$ScriptFileName"
    }
    else # if you want to use the internal script from a public location (e.g. GitHub)
    {
        $ScriptURL = $ScriptPublicUrl
    }

    foreach ($Name in $ContainerName)
    {

        $CanCreateContainerWithProvidedName = $true
        $ExistingContainer = Get-AzureRmContainerGroup -ResourceGroupName $ResourceGroupName -Name $Name -ErrorAction SilentlyContinue
        if ($ExistingContainer)
        {
            Write-Warning "ACI container with the requested name ($Name) already exists in the given Resource Group ($ResourceGroupName)."
            $CanCreateContainerWithProvidedName = $false
            if ($ReplaceExistingContainer.IsPresent)
            {
                Write-Warning "Deleting the existing container instance ($Name)..."
                Remove-AzureRmContainerGroup -ResourceGroupName $ResourceGroupName -Name $Name -Confirm:$false
                Write-Warning "Unregistering existing container instance ($Name) from Agent pool ($PoolName)..."
                Remove-AzureDevOpsAgentFromPool -PatToken $PatToken -AzureDevOpsAccountName $VSTSAccountName -AgentPoolName $PoolName -ContainerName $ContainerName

                $ContainerDeletetionWasRequired = $true
                $CanCreateContainerWithProvidedName = $true
                Write-Output "Waiting 10 seconds..."
                Start-Sleep -Seconds 10
            }
            else
            {
                Write-Output "Existing container with the name of $Name is being kept."
            }
        }
        if ($CanCreateContainerWithProvidedName)
        {
            $CreateAtLeastOneContainer = $true
            Write-Output "Creating ACI container ($Name) with the image of $ContainerImage..."
            Write-Output "Instantiating a container can take a few minutes, depending on the image size and whether or not the container image is cached in the ACI platform."

            if ($RequiredPowerShellModules.Count -gt 1)
            {
                $RequiredPowerShellModules = $RequiredPowerShellModules -join ","
            }

            if ($AcrPassword)
            {
                $SecPasswd = ConvertTo-SecureString $AcrPassword -AsPlainText -Force
                $RegistryName = $ContainerImage.Split(".")[0]
                $MyCred = New-Object System.Management.Automation.PSCredential ($RegistryName, $SecPasswd)
                    
                if ($PSVersionTable.PSEdition -eq "Core")
                {
                    # The AZ CLI has to be used, as the required -Command parameter is not available in the core version of the related AzureRM PowerShell module in Azure Cloud Shell.
                    # When running in Cloud Shell, login is not required (has already happened)
                    if ($null -ne $env:ACC_CLOUD)
                    {
                        az container create --resource-group $ResourceGroupName --name $Name --image $ContainerImage --registry-password $AcrPassword --location $Location --os-type Windows --cpu $Cpu --memory $MemoryInGB --restart-policy Always --command-line "powershell Start-Sleep -Seconds 20; Invoke-WebRequest -Uri $ScriptURL -OutFile $ScriptFileName -UseBasicParsing; & .\\$ScriptFileName -VSTSAccountName $VSTSAccountName -PATToken $PATToken -AgentNamePrefix $Name -PoolName $PoolName -InstallPowerShellCore $InstallPowerShellCore -InstallAzureCli $InstallAzureCli -UseChocolatey $UseChocolatey -RequiredPowerShellModules $RequiredPowerShellModules" --subscription $SubscriptionName --output none
                    }
                }
                elseif ($PSVersionTable.PSEdition -eq "Desktop")
                {
                    # Alternative option, using AzureRM PowerShell commandlet (this doesn't work in Azure Cloud Shell, as the -Command parameter is not available in the core version of this cmdlet)
                    New-AzureRmContainerGroup -ResourceGroupName $ResourceGroupName `
                        -Name $Name `
                        -Image $ContainerImage `
                        -Location $Location `
                        -OsType Windows `
                        -Cpu $Cpu `
                        -MemoryInGB $MemoryInGB `
                        -RestartPolicy Always `
                        -RegistryCredential $MyCred `
                        -Command "powershell Start-Sleep -Seconds 20; Invoke-WebRequest -Uri $ScriptURL -OutFile $ScriptFileName -UseBasicParsing; & .\$ScriptFileName -VSTSAccountName $VSTSAccountName -PATToken $PATToken -AgentNamePrefix $Name -PoolName $PoolName -RequiredPowerShellModules $RequiredPowerShellModules -InstallAzureCli $InstallAzureCli -UseChocolatey $UseChocolatey -InstallPowerShellCore $InstallPowerShellCore" | Out-null
                }
                else 
                {
                    Write-Error "PowerShell version could not be defined. Exiting..."
                    return
                }
            }
            else 
            {
                if ($PSVersionTable.PSEdition -eq "Core")
                {
                    # The AZ CLI has to be used, as the required -Command parameter is note available in the core version of the related AzureRM PowerShell module in Azure Cloud Shell.
                    # When running in Cloud Shell, login is not required (has already happened)
                    if ($null -ne $env:ACC_CLOUD)
                    {
                        az container create --resource-group $ResourceGroupName --name $Name --image $ContainerImage --location $Location --os-type Windows --cpu $Cpu --memory $MemoryInGB --restart-policy Always --command-line "powershell Start-Sleep -Seconds 20; Invoke-WebRequest -Uri $ScriptURL -OutFile $ScriptFileName -UseBasicParsing; & .\\$ScriptFileName -VSTSAccountName $VSTSAccountName -PATToken $PATToken -AgentNamePrefix $Name -PoolName $PoolName -RequiredPowerShellModules $RequiredPowerShellModules -InstallAzureCli $InstallAzureCli -UseChocolatey $UseChocolatey -InstallPowerShellCore $InstallPowerShellCore" --subscription $SubscriptionName --output none
                    }
                }
                elseif ($PSVersionTable.PSEdition -eq "Desktop")
                {
                    # Alternative option, using AzureRM PowerShell commandlet (this doesn't work in Azure Cloud Shell, as the -Command parameter is not available in the core version of this cmdlet)
                    New-AzureRmContainerGroup -ResourceGroupName $ResourceGroupName `
                        -Name $Name `
                        -Image $ContainerImage `
                        -Location $Location `
                        -OsType Windows `
                        -Cpu $Cpu `
                        -MemoryInGB $MemoryInGB `
                        -RestartPolicy Always `
                        -Command "powershell Start-Sleep -Seconds 20; Invoke-WebRequest -Uri $ScriptURL -OutFile $ScriptFileName -UseBasicParsing; & .\$ScriptFileName -VSTSAccountName $VSTSAccountName -PATToken $PATToken -AgentNamePrefix $Name -PoolName $PoolName -RequiredPowerShellModules $RequiredPowerShellModules -InstallAzureCli $InstallAzureCli -UseChocolatey $UseChocolatey -InstallPowerShellCore $InstallPowerShellCore" | Out-null
                }
                else 
                {
                    Write-Error "PowerShell version could not be defined. Exiting..."
                    return
                }
            }
        }
        else
        {
            Write-Warning "ACI container could not be created, as there's another container instance with the same name in the same Resource Group. If you want to remove the existing intance first, run the script again by using the -ReplaceExistingContainer switch."
        }
    }

    if ($CreateAtLeastOneContainer)
    {
        Write-Output "ACI container creation tasks have been submitted. When using a cached image, it usually takes about 10 minutes to fully provision a container."
        Write-Output "New ACI container(s) are being built..."

        # Periodically check if all containers have been configured
        $ConfiguredContainers = @()
        $NotificationTracker = @{ }
        while ($ConfiguredContainers.Count -ne $ContainerName.Count)
        {
            Start-Sleep -Seconds 10
            foreach ($Name in $ContainerName)
            {
                if ($ConfiguredContainers -notcontains $Name)
                {
                    $LogEntries = Get-AzureRmContainerInstanceLog -ResourceGroupName $ResourceGroupName -ContainerGroupName $Name -ErrorAction SilentlyContinue
                    $RestartCount = (Get-AzureRmContainerGroup -ResourceGroupName $ResourceGroupName -Name $Name).Containers.restartcount
                        
                    # Show restart counts, avoid repeating the same entry
                    if ($RestartCount -gt 0)
                    {
                        $Key = "$Name-$RestartCount"
                        if ($NotificationTracker[$Key] -ne "MessageAlreadyShown")
                        {
                            Write-Output "The creation of the ACI container ""$Name"" was restarted $RestartCount time(s). Note that a few retries are acceptable, however each iteration increases the overall creation time."
                            $NotificationTracker.Add($Key, "MessageAlreadyShown")
                        }
                            
                    }

                    # Trigger on success
                    if ($LogEntries)
                    {
                        # Check if the last line of the log is "Container successfully configured."
                        $Success = $LogEntries.Split("`n")[-4] -eq "Container successfully configured." # Do NOT change this value, as the wrapper script is triggered based on this.
                        if ($Success)
                        {
                            $ConfiguredContainers += $Name
                            Write-Output "ACI container ""$Name"" successfully configured"
                        }
                    }
                }
            }
        }

        # Print results
        if ($ConfiguredContainers.Count -eq $ContainerName.Count)
        {
            Write-Output "All requested containers have been successfully deployed and configured."
            Write-Output "Check if VSTS agents have been registered under the requested Agent Pool on the VSTS portal."
            $TimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date))
            Write-Output "Finished at $(Get-Date)"
            Write-Output "It took $($TimeSpan.Minutes) minutes and $($TimeSpan.Seconds) seconds to initialize the requested containers."
            if ($ContainerDeletetionWasRequired)
            {
                Write-Warning "One or more containers have been deleted. "
            }
        }
    }
    else
    {
        Write-Output "No ACI containers have been created, as there were conflicts with existing intances and the -ReplaceExistingContainer was not used."
    }
}

function Get-LatestCachedImageVersion
{
    # This function returns the name of the latest version of the
    # microsoft/windowsservercore image that is cached in the ACI platform
    # This is needed, because Microsoft no longer uses the ":latest" tag on their images
    # See more details here: https://techcommunity.microsoft.com/t5/Containers/Removing-the-latest-Tag-An-Update-on-MCR/ba-p/393045

    # Authenticate to invoke Azure REST API
    $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
    $currentAzureContext = Get-AzureRmContext
    $profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient($azProfile)
    $token = $profileClient.AcquireAccessToken($currentAzureContext.Tenant.TenantId)
    $accessToken = $token.AccessToken

    $RestCall = @{
        Method  = "Get"
        Uri     = "https://management.azure.com/subscriptions/$($currentAzureContext.Subscription)/providers/Microsoft.ContainerInstance/locations/$Location/cachedImages?api-version=2018-10-01"
        Headers = @{
            Authorization = "Bearer " + $AccessToken
        }
    }

    $result = Invoke-RestMethod @RestCall

    $LatestCachedImage = (($result.value | Where-Object { $_.image -like "microsoft/*dotnet*windowsservercore*" } | Sort-Object image)[-1]).image
    return $LatestCachedImage
}

function Remove-AzureDevOpsAgentFromPool
{
    Param(
        [string]$PatToken, #Personal access token
        [string]$AzureDevOpsAccountName, #Azure DevOps account name
        [string]$AgentPoolName, #Azure DevOps Agent pool name
        [array]$ContainerName  #Azure DevOps Agent pool name
    )
        
    $base64AuthInfo = [System.Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PatToken)"))
    $Header = @{Authorization = ("Basic $base64AuthInfo") }

    # Get Agent Pool
    Write-Output "Getting Agent Pool ($AgentPoolName)..."
    $uri = "https://dev.azure.com/$AzureDevOpsAccountName/_apis/distributedtask/pools"
    $result = Invoke-RestMethod -Uri $uri -Method GET -ContentType "application/json" -Headers $Header
    $AgentPool = $result.value | Where-Object { $_.Name -eq "$AgentPoolName" }
    $AgentPoolId = $AgentPool.id

    if (-not $AgentPoolId)
    {
        Write-Error "The Agent Pool ($AgentPoolName) doesn't exist!"
    }
    else
    {
        # Get Agents
        Write-Output "Getting Agents from Pool..."
        $uri = "https://dev.azure.com/$AzureDevOpsAccountName/_apis/distributedtask/pools/$AgentPoolId/agents?includeCapabilities=false&includeAssignedRequest=true"
        $result = Invoke-RestMethod -Uri $uri -Method GET -ContentType "application/json" -Headers $Header
        $Agents = $result.value

        if (-not $Agents)
        {
            Write-Output "There are no Agents in this Agent Pool"
        }
        else
        {
            Write-Output "The following agents were found:"
            foreach ($Agent in $Agents)
            {
                $AgentName = $Agent.name
                $AgentStatus = $Agent.status
                Write-Output "$AgentName ($AgentStatus)"
            }

            # Delete Agent(s) from Pool
            foreach ($Name in $ContainerName)
            {
                Write-Output "Attempting to remove any Agent(s) that belonged to this ACI container: $Name..."
                foreach ($Agent in $Agents)
                {
                    $AgentName = $Agent.name

                    if ($AgentName -match "^$Name-")
                    {
                        # Delete Agent
                        $AgentIds = $Agent.id
                        foreach ($AgentId in $AgentIds)
                        {
                            Write-Output "Deleting Agent ($AgentName) - (Agent ID: $AgentId)..."
                            $uri = "https://dev.azure.com/$AzureDevOpsAccountName/_apis/distributedtask/pools/$AgentPoolId/agents/$($AgentId)?api-version=5.0"
                            $result = Invoke-RestMethod -Uri $uri -Method DELETE -ContentType "application/json" -Headers $Header

                            # Check success
                            Write-Output "Checking if the Agent ($AgentName) - (Agent ID: $AgentId) is still there..."
                            $uri = "https://dev.azure.com/$AzureDevOpsAccountName/_apis/distributedtask/pools/$AgentPoolId/agents?includeCapabilities=false&includeAssignedRequest=true"
                            $result = Invoke-RestMethod -Uri $uri -Method GET -ContentType "application/json" -Headers $Header
                            $AgentStillThere = $result.value.id -contains $AgentId
                            if ($AgentStillThere)
                            {
                                Write-Warning "Agent ($AgentName) could not be deleted. Don't forget to clean your Agent pool in Azure Devops (remove any agents that were created in a previous iteration and are now offline)!"
                            }
                            else
                            {
                                Write-Output "Agent ($AgentName) successfully deleted."    
                            }
                        }
                    }
                }
            }
        }
    }
}

#endregion

#region Main
# Report start time
$StartDate = Get-Date
Write-Output "Started at $StartDate..."

# Login to Azure and select Subscription
Set-AzureContext -SubscriptionName $SubscriptionName

if ($StorageAccountName)
{
    # Upload the configuration script to a Storage Account
    Copy-ScriptToStorageAccount -StorageAccountResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -StorageContainerName $StorageContainerName -ScriptFileName $ScriptFileName
}

Write-Output "Ready For creating Resource Group for containers"

<#
# Create Resource Group for containers
if (-not (Get-AzureRmResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue))
{
    Write-Output "Resource Group ""$ResourceGroupName"" does not exist for ACI containers. Creating..."
    New-AzureRmResourceGroup -Name $ResourceGroupName -Location $Location | Out-Null
}
#>

Write-Output "Ready For creating containers"
# Create containers
if (-not $ContainerImage)
{
    $ContainerImage = Get-LatestCachedImageVersion
    Write-Output "Container Image = $ContainerImage"
}
New-Container -ContainerImage $ContainerImage

#endregion
# SIG # Begin signature block
# MIINEAYJKoZIhvcNAQcCoIINATCCDP0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUag+tQogpqd0EKtkbuuzLmtQU
# xrSgggpSMIIFGjCCBAKgAwIBAgIQAsF1KHTVwoQxhSrYoGRpyjANBgkqhkiG9w0B
# AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz
# c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTE3MDUwOTAwMDAwMFoXDTIwMDUx
# MzEyMDAwMFowVzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMQ8wDQYD
# VQQHEwZWaWVubmExETAPBgNVBAoTCGRiYXRvb2xzMREwDwYDVQQDEwhkYmF0b29s
# czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI8ng7JxnekL0AO4qQgt
# Kr6p3q3SNOPh+SUZH+SyY8EA2I3wR7BMoT7rnZNolTwGjUXn7bRC6vISWg16N202
# 1RBWdTGW2rVPBVLF4HA46jle4hcpEVquXdj3yGYa99ko1w2FOWzLjKvtLqj4tzOh
# K7wa/Gbmv0Si/FU6oOmctzYMI0QXtEG7lR1HsJT5kywwmgcjyuiN28iBIhT6man0
# Ib6xKDv40PblKq5c9AFVldXUGVeBJbLhcEAA1nSPSLGdc7j4J2SulGISYY7ocuX3
# tkv01te72Mv2KkqqpfkLEAQjXgtM0hlgwuc8/A4if+I0YtboCMkVQuwBpbR9/6ys
# Z+sCAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5LfZldQ5Y
# MB0GA1UdDgQWBBRcxSkFqeA3vvHU0aq2mVpFRSOdmjAOBgNVHQ8BAf8EBAMCB4Aw
# EwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0cDovL2Ny
# bDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAzoDGGL2h0
# dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEuY3JsMEwG
# A1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3
# LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYwJAYI
# KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcwAoZC
# aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJ
# RENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQAD
# ggEBANuBGTbzCRhgG0Th09J0m/qDqohWMx6ZOFKhMoKl8f/l6IwyDrkG48JBkWOA
# QYXNAzvp3Ro7aGCNJKRAOcIjNKYef/PFRfFQvMe07nQIj78G8x0q44ZpOVCp9uVj
# sLmIvsmF1dcYhOWs9BOG/Zp9augJUtlYpo4JW+iuZHCqjhKzIc74rEEiZd0hSm8M
# asshvBUSB9e8do/7RhaKezvlciDaFBQvg5s0fICsEhULBRhoyVOiUKUcemprPiTD
# xh3buBLuN0bBayjWmOMlkG1Z6i8DUvWlPGz9jiBT3ONBqxXfghXLL6n8PhfppBhn
# daPQO8+SqF5rqrlyBPmRRaTz2GQwggUwMIIEGKADAgECAhAECRgbX9W7ZnVTQ7Vv
# lVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0Rp
# Z2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBaFw0yODEw
# MjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNI
# QTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUA
# A4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/lqJ3bMtdx
# 6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fTeyOU5JEj
# lpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqHCN8M9eJN
# YBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+bMt+dDk2
# DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLoLFH3c7y9
# hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIByTASBgNV
# HRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF
# BQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
# Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDig
# NoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwAAgQwKjAo
# BggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAKBghghkgB
# hv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0jBBgwFoAU
# Reuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7sDVoks/Mi
# 0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGSdQ9RtG6l
# jlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6r7VRwo0k
# riTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo+MUSaJ/P
# QMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qzsIzV6Q3d
# 9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHqaGxEMrJm
# oecYpJpkUe8xggIoMIICJAIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM
# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQD
# EyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBAhACwXUo
# dNXChDGFKtigZGnKMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgACh
# AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM
# BgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBQodToNMwK9wGq1pq5gEafK/pkI
# 6TANBgkqhkiG9w0BAQEFAASCAQA1cWGaoOtYK7Aj0+voFm8NBPFXUyBfFFFMBUBK
# np8giAMSvASbzXSUPR/hae6LR/UzZFdXqH3ZhAbA//BItAjM5EC8fhbZMHGzqXmx
# IxU7YxzuXJvrLd7yVjLRRhu+kclMVGSGELBFIfEAm8nMtrhWAKGhXPlzFTJtwOiD
# qREulLuvtEBb/LBk/mT/vw8499Jzj9fE5oXYkqcJiodToaagGEpe2fZC03s8rbm9
# kl3n3H39/j7y4PwJHt0BEhtT4koNVod5d3Jx42hpJIN77vLCmJ3DtVhckArrkftc
# UPdWInKb/H47AJCuP3+chRgiGmEGdlnVVM13FJfzIkHxDxpA
# SIG # End signature block