utilities/pipelines/e2eValidation/resourceRemoval/helper/Get-DeploymentTargetResourceList.ps1 (355 lines of code) (raw):
#region helper
<#
.SYNOPSIS
Get all deployment operations at a given scope
.DESCRIPTION
Get all deployment oeprations at a given scope. By default, the results are filtered down to 'create' operations (i.e., excluding 'read' operations that would correspond to 'existing' resources).
.PARAMETER Name
Mandatory. The deployment name to search for
.PARAMETER ResourceGroupName
Optional. The name of the resource group for scope 'resourcegroup'. Relevant for resource-group-level deployments.
.PARAMETER SubscriptionId
Optional. The ID of the subscription to fetch deployments from. Relevant for subscription- & resource-group-level deployments.
.PARAMETER ManagementGroupId
Optional. The ID of the management group to fetch deployments from. Relevant for management-group-level deployments.
.PARAMETER Scope
Mandatory. The scope to search in
.PARAMETER ProvisioningOperationsToInclude
Optional. The provisioning operations to include in the result set. By default, only 'create' operations are included.
.EXAMPLE
Get-DeploymentOperationAtScope -Scope 'subscription' -Name 'v73rhp24d7jya-test-apvmiaiboaai'
Get all deployment operations for a deployment with name 'v73rhp24d7jya-test-apvmiaiboaai' at scope 'subscription'
.NOTES
This function is a standin for the Get-AzDeploymentOperation cmdlet, which does not provide the ability to filter by provisioning operation.
As such, it was also returning 'existing' resources (i.e., with provisioningOperation=Read).
#>
function Get-DeploymentOperationAtScope {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[Alias('DeploymentName')]
[string] $Name,
[Parameter(Mandatory = $false)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $false)]
[string] $SubscriptionId,
[Parameter(Mandatory = $false)]
[string] $ManagementGroupId,
[Parameter(Mandatory = $false)]
[ValidateSet(
'Create', # any resource creation
'Read', # E.g., 'existing' resources
'EvaluateDeploymentOutput' # Nobody knows
)]
[string[]] $ProvisioningOperationsToInclude = @('Create'),
[Parameter(Mandatory)]
[ValidateSet(
'resourcegroup',
'subscription',
'managementgroup',
'tenant'
)]
[string] $Scope
)
switch ($Scope) {
'resourcegroup' {
$path = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Resources/deployments/{2}/operations?api-version=2021-04-01' -f $SubscriptionId, $ResourceGroupName, $name
break
}
'subscription' {
$path = '/subscriptions/{0}/providers/Microsoft.Resources/deployments/{1}/operations?api-version=2021-04-01' -f $SubscriptionId, $name
break
}
'managementgroup' {
$path = '/providers/Microsoft.Management/managementGroups/{0}/providers/Microsoft.Resources/deployments/{1}/operations?api-version=2021-04-01' -f $ManagementGroupId, $name
break
}
'tenant' {
$path = '/providers/Microsoft.Resources/deployments/{0}/operations?api-version=2021-04-01' -f $name
break
}
}
##############################################
# Get all deployment children based on scope #
##############################################
$response = Invoke-AzRestMethod -Method 'GET' -Path $path
if ($response.StatusCode -ne 200) {
Write-Error ('Failed to fetch deployment operations for deployment [{0}] in scope [{1}]' -f $name, $scope)
return
} else {
$deploymentOperations = ($response.content | ConvertFrom-Json).value.properties
$deploymentOperationsFiltered = $deploymentOperations | Where-Object { $_.provisioningOperation -in $ProvisioningOperationsToInclude }
return $deploymentOperationsFiltered ?? $true # Returning true to indicate that the deployment was found, but did not contain any relevant operations
}
}
<#
.SYNOPSIS
Get all deployments that match a given deployment name in a given scope
.DESCRIPTION
Get all deployments that match a given deployment name in a given scope. Works recursively through the deployment tree.
.PARAMETER Name
Mandatory. The deployment name to search for
.PARAMETER ResourceGroupName
Optional. The name of the resource group for scope 'resourcegroup'
.PARAMETER ManagementGroupId
Optional. The ID of the management group to fetch deployments from. Relevant for management-group level deployments.
.PARAMETER Scope
Mandatory. The scope to search in
.PARAMETER DoThrow
Optional. Throw an exception if a deployment cannot be found. If not set, a warning is returned instead.
.EXAMPLE
Get-DeploymentTargetResourceListInner -Name 'keyvault-12356' -Scope 'resourcegroup'
Get all deployments that match name 'keyvault-12356' in scope 'resourcegroup'
.EXAMPLE
Get-ResourceIdsOfDeploymentInner -Name 'mgmtGroup-12356' -Scope 'managementGroup' -ManagementGroupId 'af760cf5-3c9e-4804-a59a-a51741daa350'
Get all deployments that match name 'mgmtGroup-12356' in scope 'managementGroup'
.NOTES
Works after the principal:
- Find all deployments for the given deployment name
- If any of them are not a deployments, add their target resource to the result set (as they are e.g. a resource)
- If any of them is are deployments, recursively invoke this function for them to get their contained target resources
#>
function Get-DeploymentTargetResourceListInner {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string] $Name,
[Parameter(Mandatory = $false)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $false)]
[string] $ManagementGroupId,
[Parameter(Mandatory)]
[ValidateSet(
'resourcegroup',
'subscription',
'managementgroup',
'tenant'
)]
[string] $Scope,
[Parameter(Mandatory = $false)]
[switch] $DoThrow
)
$resultSet = [System.Collections.ArrayList]@()
$currentContext = Get-AzContext
##############################################
# Get all deployment children based on scope #
##############################################
$baseInputObject = @{
Scope = $Scope
DeploymentName = $Name
}
switch ($Scope) {
'resourcegroup' {
if (Get-AzResourceGroup -Name $resourceGroupName -ErrorAction 'SilentlyContinue') {
if ($op = Get-DeploymentOperationAtScope @baseInputObject -ResourceGroupName $resourceGroupName -SubscriptionId $currentContext.Subscription.Id) {
[array]$deploymentTargets = $op.TargetResource.id | Where-Object { $_ -ne $null } | Select-Object -Unique
} else {
$message = "Not found deployment [$Name] in scope [$Scope] of Resource Group [$ResourceGroupName]."
if ($DoThrow) {
throw $message
} else {
Write-Warning "$message Ignoring, as nested deployment."
return
}
}
} else {
# In case the resource group itself was already deleted, there is no need to try and fetch deployments from it
# In case we already have any such resources in the list, we should remove them
return $resultSet | Where-Object { $_ -notmatch "\/resourceGroups\/$resourceGroupName\/" } | Select-Object -Unique
}
break
}
'subscription' {
if ($op = Get-DeploymentOperationAtScope @baseInputObject -SubscriptionId $currentContext.Subscription.Id) {
[array]$deploymentTargets = $op.TargetResource.id | Where-Object { $_ -ne $null } | Select-Object -Unique
} else {
$message = "Not found deployment [$Name] in scope [$Scope]."
if ($DoThrow) {
throw $message
} else {
Write-Warning "$message Ignoring, as nested deployment."
return
}
}
break
}
'managementgroup' {
if ($op = Get-DeploymentOperationAtScope @baseInputObject -ManagementGroupId $ManagementGroupId) {
[array]$deploymentTargets = $op.TargetResource.id | Where-Object { $_ -ne $null } | Select-Object -Unique
} else {
$message = "Not found deployment [$Name] in scope [$Scope]."
if ($DoThrow) {
throw $message
} else {
Write-Warning "$message Ignoring, as nested deployment."
return
}
}
break
}
'tenant' {
if ($op = Get-DeploymentOperationAtScope @baseInputObject) {
[array]$deploymentTargets = $op.TargetResource.id | Where-Object { $_ -ne $null } | Select-Object -Unique
} else {
$message = "Not found deployment [$Name] in scope [$Scope]."
if ($DoThrow) {
throw $message
} else {
Write-Warning "$message Ignoring, as nested deployment."
return
}
}
break
}
}
###########################
# Manage nested resources #
###########################
foreach ($deployment in ($deploymentTargets | Where-Object { $_ -notmatch '\/Microsoft\.Resources\/deployments\/' } )) {
Write-Verbose ('Found deployed resource [{0}]' -f $deployment)
[array]$resultSet += $deployment
}
#############################
# Manage nested deployments #
#############################
foreach ($deployment in ($deploymentTargets | Where-Object { $_ -match '\/Microsoft\.Resources\/deployments\/' } )) {
$name = Split-Path $deployment -Leaf
if ($deployment -match '/resourceGroups/') {
# Resource Group Level Child Deployments #
##########################################
if ($deployment -match '^\/subscriptions\/([0-9a-zA-Z-]+?)\/') {
$subscriptionId = $Matches[1]
if ($currentContext.Subscription.Id -ne $subscriptionId) {
$null = Set-AzContext -Subscription $subscriptionId
}
}
Write-Verbose ('Found [resource group] deployment [{0}]' -f $deployment)
$resourceGroupName = $deployment.split('/resourceGroups/')[1].Split('/')[0]
[array]$resultSet += Get-DeploymentTargetResourceListInner -Name $name -Scope 'resourcegroup' -ResourceGroupName $ResourceGroupName
} elseif ($deployment -match '/subscriptions/') {
# Subscription Level Child Deployments #
########################################
if ($deployment -match '^\/subscriptions\/([0-9a-zA-Z-]+?)\/') {
$subscriptionId = $Matches[1]
if ($currentContext.Subscription.Id -ne $subscriptionId) {
$null = Set-AzContext -Subscription $subscriptionId
}
}
Write-Verbose ('Found [subscription] deployment [{0}]' -f $deployment)
[array]$resultSet += Get-DeploymentTargetResourceListInner -Name $name -Scope 'subscription'
} elseif ($deployment -match '/managementgroups/') {
# Management Group Level Child Deployments #
############################################
Write-Verbose ('Found [management group] deployment [{0}]' -f $deployment)
[array]$resultSet += Get-DeploymentTargetResourceListInner -Name $name -Scope 'managementgroup' -ManagementGroupId $ManagementGroupId
} else {
# Tenant Level Child Deployments #
##################################
Write-Verbose ('Found [tenant] deployment [{0}]' -f $deployment)
[array]$resultSet += Get-DeploymentTargetResourceListInner -Name $name -Scope 'tenant'
}
}
return $resultSet | Select-Object -Unique
}
#endregion
<#
.SYNOPSIS
Get all deployments that match a given deployment name in a given scope using a retry mechanic
.DESCRIPTION
Get all deployments that match a given deployment name in a given scope using a retry mechanic.
.PARAMETER ResourceGroupName
Optional. The name of the resource group for scope 'resourcegroup'
.PARAMETER ManagementGroupId
Optional. The ID of the management group to fetch deployments from. Relevant for management-group level deployments.
.PARAMETER Name
Optional. The deployment name to use for the removal
.PARAMETER Scope
Mandatory. The scope to search in
.PARAMETER SearchRetryLimit
Optional. The maximum times to retry the search for resources via their removal tag
.PARAMETER SearchRetryInterval
Optional. The time to wait in between the search for resources via their remove tags
.EXAMPLE
Get-DeploymentTargetResourceList -name 'KeyVault' -ResourceGroupName 'validation-rg' -scope 'resourcegroup'
Get all deployments that match name 'KeyVault' in scope 'resourcegroup' of resource group 'validation-rg'
.EXAMPLE
Get-ResourceIdsOfDeployment -Name 'mgmtGroup-12356' -Scope 'managementGroup' -ManagementGroupId 'af760cf5-3c9e-4804-a59a-a51741daa350'
Get all deployments that match name 'mgmtGroup-12356' in scope 'managementGroup'
#>
function Get-DeploymentTargetResourceList {
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $false)]
[string] $ManagementGroupId,
[Parameter(Mandatory = $true)]
[Alias('Name', 'Names', 'DeploymentName')]
[string[]] $DeploymentNames,
[Parameter(Mandatory = $true)]
[ValidateSet(
'resourcegroup',
'subscription',
'managementgroup',
'tenant'
)]
[string] $Scope,
[Parameter(Mandatory = $false)]
[int] $SearchRetryLimit = 40,
[Parameter(Mandatory = $false)]
[int] $SearchRetryInterval = 60
)
$searchRetryCount = 1
$resourcesToRemove = @()
$deploymentNameObjects = $DeploymentNames | ForEach-Object {
@{
Name = $_
Resolved = $false
}
}
do {
foreach ($deploymentNameObject in $deploymentNameObjects) {
if ($deploymentNameObject.Resolved) {
# Skip further invocations for this deployment name if deployment was already found
continue
}
$innerInputObject = @{
Name = $deploymentNameObject.Name
Scope = $scope
ErrorAction = 'SilentlyContinue'
}
if (-not [String]::IsNullOrEmpty($resourceGroupName)) {
$innerInputObject['resourceGroupName'] = $resourceGroupName
}
if (-not [String]::IsNullOrEmpty($ManagementGroupId)) {
$innerInputObject['ManagementGroupId'] = $ManagementGroupId
}
try {
$targetResources = Get-DeploymentTargetResourceListInner @innerInputObject -DoThrow # Specifying [-DoThrow] for top-level deployments that we definitely want to resolve
Write-Verbose ('Found & resolved deployment [{0}]. [{1}] resources found to remove.' -f $deploymentNameObject.Name, $targetResources.Count) -Verbose
$deploymentNameObject.Resolved = $true
$resourcesToRemove += $targetResources
} catch {
$remainingDeploymentNames = ($deploymentNameObjects | Where-Object { -not $_.Resolved }).Name
Write-Verbose ('No deployment found by name(s) [{0}] in scope [{1}]. Retrying in [{2}] seconds [{3}/{4}]' -f ($remainingDeploymentNames -join ', '), $scope, $searchRetryInterval, $searchRetryCount, $searchRetryLimit) -Verbose
Start-Sleep $searchRetryInterval
$searchRetryCount++
}
}
# Break check
if ($deploymentNameObjects.Resolved -notcontains $false) {
break
}
} while ($searchRetryCount -le $searchRetryLimit)
if ($searchRetryCount -gt $searchRetryLimit) {
$remainingDeploymentNames = ($deploymentNameObjects | Where-Object { -not $_.Resolved }).Name
# We don't want to outright throw an exception as we want to remove as many resources as possible before failing the script in the calling function
return @{
resolveError = ('No deployment for the deployment name(s) [{0}] found' -f ($remainingDeploymentNames -join ', '))
resourcesToRemove = $resourcesToRemove
}
}
return @{
resourcesToRemove = $resourcesToRemove
}
}