utilities/pipelines/e2eValidation/resourceRemoval/helper/Invoke-ResourceRemoval.ps1 (358 lines of code) (raw):
<#
.SYNOPSIS
Remove a specific resource
.DESCRIPTION
Remove a specific resource. Tries to handle different resource types accordingly
.PARAMETER ResourceId
Mandatory. The resourceID of the resource to remove
.PARAMETER Type
Mandatory. The type of the resource to remove
.EXAMPLE
Invoke-ResourceRemoval -Type 'Microsoft.Insights/diagnosticSettings' -ResourceId '/subscriptions/.../resourceGroups/validation-rg/providers/Microsoft.Network/networkInterfaces/sxx-vm-linux-001-nic-01/providers/Microsoft.Insights/diagnosticSettings/sxx-vm-linux-001-nic-01-diagnosticSettings'
Remove the resource 'sxx-vm-linux-001-nic-01-diagnosticSettings' of type 'Microsoft.Insights/diagnosticSettings' from resource '/subscriptions/.../resourceGroups/validation-rg/providers/Microsoft.Network/networkInterfaces/sxx-vm-linux-001-nic-01'
#>
function Invoke-ResourceRemoval {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory = $true)]
[string] $ResourceId,
[Parameter(Mandatory = $true)]
[string] $Type
)
# Load functions
. (Join-Path $PSScriptRoot 'Invoke-ResourceLockRemoval.ps1')
# Remove unhandled resource locks, for cases when the resource
# collection is incomplete, usually due to previous removal failing.
if ($PSCmdlet.ShouldProcess("Possible locks on resource with ID [$ResourceId]", 'Handle')) {
Invoke-ResourceLockRemoval -ResourceId $ResourceId -Type $Type
}
switch ($Type) {
'Microsoft.Insights/diagnosticSettings' {
$parentResourceId = $ResourceId.Split('/providers/{0}' -f $Type)[0]
$resourceName = Split-Path $ResourceId -Leaf
if ($PSCmdlet.ShouldProcess("Diagnostic setting [$resourceName]", 'Remove')) {
$null = Remove-AzDiagnosticSetting -ResourceId $parentResourceId -Name $resourceName
}
break
}
'Microsoft.Authorization/locks' {
if ($PSCmdlet.ShouldProcess("Lock with ID [$ResourceId]", 'Remove')) {
Invoke-ResourceLockRemoval -ResourceId $ResourceId -Type $Type
}
break
}
'Microsoft.KeyVault/vaults/keys' {
$resourceName = Split-Path $ResourceId -Leaf
Write-Verbose ('[/] Skipping resource [{0}] of type [{1}]. Reason: It is handled by different logic.' -f $resourceName, $Type) -Verbose
# Also, we don't want to accidently remove keys of the dependency key vault
break
}
'Microsoft.KeyVault/vaults/accessPolicies' {
$resourceName = Split-Path $ResourceId -Leaf
Write-Verbose ('[/] Skipping resource [{0}] of type [{1}]. Reason: It is handled by different logic.' -f $resourceName, $Type) -Verbose
break
}
'Microsoft.ServiceBus/namespaces/authorizationRules' {
if ((Split-Path $ResourceId '/')[-1] -eq 'RootManageSharedAccessKey') {
Write-Verbose ('[/] Skipping resource [RootManageSharedAccessKey] of type [{0}]. Reason: The Service Bus''s default authorization key cannot be removed' -f $Type) -Verbose
} else {
if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) {
$null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop'
}
}
break
}
'Microsoft.Compute/diskEncryptionSets' {
# Pre-Removal
# -----------
# Remove access policies on key vault
$resourceGroupName = $ResourceId.Split('/')[4]
$resourceName = Split-Path $ResourceId -Leaf
$diskEncryptionSet = Get-AzDiskEncryptionSet -Name $resourceName -ResourceGroupName $resourceGroupName
$keyVaultResourceId = $diskEncryptionSet.ActiveKey.SourceVault.Id
$keyVaultName = Split-Path $keyVaultResourceId -Leaf
$objectId = $diskEncryptionSet.Identity.PrincipalId
if ($PSCmdlet.ShouldProcess(('Access policy [{0}] from key vault [{1}]' -f $objectId, $keyVaultName), 'Remove')) {
$null = Remove-AzKeyVaultAccessPolicy -VaultName $keyVaultName -ObjectId $objectId
}
# Actual removal
# --------------
if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) {
$null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop'
}
break
}
'Microsoft.RecoveryServices/vaults/backupstorageconfig' {
# Not a 'resource' that can be removed, but represents settings on the RSV. The config is deleted with the RSV
break
}
'Microsoft.Authorization/roleAssignments' {
$idElem = $ResourceId.Split('/')
$scope = $idElem[0..($idElem.Count - 5)] -join '/'
$roleAssignmentsOnScope = Get-AzRoleAssignment -Scope $scope
$null = $roleAssignmentsOnScope | Where-Object { $_.RoleAssignmentId -eq $ResourceId } | Remove-AzRoleAssignment
break
}
'Microsoft.Authorization/roleEligibilityScheduleRequests' {
$idElem = $ResourceId.Split('/')
$scope = $idElem[0..($idElem.Count - 5)] -join '/'
$pimRequestName = $idElem[-1]
$pimRoleAssignment = Get-AzRoleEligibilityScheduleRequest -Scope $scope -Name $pimRequestName
if ($pimRoleAssignment) {
$pimRoleAssignmentPrinicpalId = $pimRoleAssignment.PrincipalId
$pimRoleAssignmentRoleDefinitionId = $pimRoleAssignment.RoleDefinitionId
$guid = New-Guid
# PIM role assignments cannot be removed before 5 minutes from being created. Waiting for 5 minutes
Write-Verbose 'Waiting for 5 minutes before removing PIM role assignment' -Verbose
Start-Sleep -Seconds 300
# The PIM ARM API doesn't support DELETE requests so the only way to delete an assignment is by creating a new assignment with `AdminRemove` type using a new GUID
$removalInputObject = @{
Name = $guid
Scope = $scope
PrincipalId = $pimRoleAssignmentPrinicpalId
RequestType = 'AdminRemove'
RoleDefinitionId = $pimRoleAssignmentRoleDefinitionId
}
$null = New-AzRoleEligibilityScheduleRequest @removalInputObject
}
break
}
'Microsoft.Authorization/roleAssignmentScheduleRequests' {
$idElem = $ResourceId.Split('/')
$scope = $idElem[0..($idElem.Count - 5)] -join '/'
$pimRequestName = $idElem[-1]
$pimRoleAssignment = Get-AzRoleAssignmentScheduleRequest -Scope $scope -Name $pimRequestName
if ($pimRoleAssignment) {
$pimRoleAssignmentPrinicpalId = $pimRoleAssignment.PrincipalId
$pimRoleAssignmentRoleDefinitionId = $pimRoleAssignment.RoleDefinitionId
$guid = New-Guid
# PIM role assignments cannot be removed before 5 minutes from being created. Waiting for 5 minutes
Write-Verbose 'Waiting for 5 minutes before removing PIM role assignment' -Verbose
Start-Sleep -Seconds 300
# The PIM ARM API doesn't support DELETE requests so the only way to delete an assignment is by creating a new assignment with `AdminRemove` type using a new GUID
$removalInputObject = @{
Name = $guid
Scope = $scope
PrincipalId = $pimRoleAssignmentPrinicpalId
RequestType = 'AdminRemove'
RoleDefinitionId = $pimRoleAssignmentRoleDefinitionId
}
$null = New-AzRoleAssignmentScheduleRequest @removalInputObject
}
break
}
'Microsoft.RecoveryServices/vaults' {
# Pre-Removal
# -----------
# Remove protected VMs
if ((Get-AzRecoveryServicesVaultProperty -VaultId $ResourceId).SoftDeleteFeatureState -ne 'Disabled') {
if ($PSCmdlet.ShouldProcess(('Soft-delete on RSV [{0}]' -f $ResourceId), 'Set')) {
$null = Set-AzRecoveryServicesVaultProperty -VaultId $ResourceId -SoftDeleteFeatureState 'Disable'
}
}
$backupItems = Get-AzRecoveryServicesBackupItem -BackupManagementType 'AzureVM' -WorkloadType 'AzureVM' -VaultId $ResourceId
foreach ($backupItem in $backupItems) {
Write-Verbose ('Removing Backup item [{0}] from RSV [{1}]' -f $backupItem.Name, $ResourceId) -Verbose
if ($backupItem.DeleteState -eq 'ToBeDeleted') {
if ($PSCmdlet.ShouldProcess('Soft-deleted backup data removal', 'Undo')) {
$null = Undo-AzRecoveryServicesBackupItemDeletion -Item $backupItem -VaultId $ResourceId -Force
}
}
if ($PSCmdlet.ShouldProcess(('Backup item [{0}] from RSV [{1}]' -f $backupItem.Name, $ResourceId), 'Remove')) {
$null = Disable-AzRecoveryServicesBackupProtection -Item $backupItem -VaultId $ResourceId -RemoveRecoveryPoints -Force
}
}
# Actual removal
# --------------
if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) {
$null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop'
}
break
}
'Microsoft.DataProtection/backupVaults' {
# Note: This Resource Provider does not allow deleting the vault as long as it has nested resources
# Pre-Removal
# -----------
$resourceGroupName = $ResourceId.Split('/')[4]
$resourceName = Split-Path $ResourceId -Leaf
$vault = Get-AzDataProtectionBackupVault -ResourceGroupName $resourceGroupName -VaultName $resourceName
# Disable vault immutability
if ($vault.ImmutabilityState -ne 'Disabled') {
Write-Verbose (' [-] Disabling immutability on vault [{0}]' -f $resourceName) -Verbose
if ($PSCmdlet.ShouldProcess(('Immutability on vault [{0}]' -f $resourceName), 'Update')) {
$null = Update-AzDataProtectionBackupVault -ResourceGroupName $resourceGroupName -VaultName $resourceName -ImmutabilityState Disabled
}
}
# Disable vault soft-deletion
if ($vault.SoftDeleteState -ne 'Off') {
Write-Verbose (' [-] Disabling soft-deletion on vault [{0}]' -f $resourceName) -Verbose
if ($PSCmdlet.ShouldProcess(('Soft-delete on vault [{0}]' -f $resourceName), 'Update')) {
$null = Update-AzDataProtectionBackupVault -ResourceGroupName $resourceGroupName -VaultName $resourceName -SoftDeleteState Off
}
}
# Undo soft-deleted backup instances
$softDeletedBackupInstances = Get-AzDataProtectionSoftDeletedBackupInstance -ResourceGroupName $resourceGroupName -VaultName $resourceName
foreach ($softDeletedBackupInstance in $softDeletedBackupInstances) {
Write-Verbose (' [-] Removing Backup instance soft deletion [{0}] from vault [{1}]' -f $softDeletedBackupInstance.Name, $resourceName) -Verbose
if ($PSCmdlet.ShouldProcess(('Soft deletion on backup instance [{0}] from vault [{1}]' -f $softDeletedBackupInstance.Name, $resourceName), 'Undo')) {
$null = Undo-AzDataProtectionBackupInstanceDeletion -ResourceGroupName $resourceGroupName -VaultName $resourceName -BackupInstanceName $softDeletedBackupInstance.name
}
}
# Actual removal
# --------------
# Remove backup instances
$backupInstances = Get-AzDataProtectionBackupInstance -ResourceGroupName $resourceGroupName -VaultName $resourceName
foreach ($backupInstance in $backupInstances) {
Write-Verbose (' [-] Removing Backup instance [{0}] from vault [{1}]' -f $backupInstance.Name, $resourceName) -Verbose
if ($PSCmdlet.ShouldProcess(('Backup instance [{0}] from vault [{1}]' -f $backupInstance.Name, $resourceName), 'Remove')) {
$null = Remove-AzDataProtectionBackupInstance -ResourceGroupName $resourceGroupName -VaultName $resourceName -Name $backupInstance.name
}
}
# Remove backup policies
$backupPolicies = Get-AzDataProtectionBackupPolicy -ResourceGroupName $resourceGroupName -VaultName $resourceName
foreach ($backupPolicy in $backupPolicies) {
Write-Verbose (' [-] Removing Backup policy [{0}] from vault [{1}]' -f $backupPolicy.Name, $resourceName) -Verbose
if ($PSCmdlet.ShouldProcess(('Backup instance [{0}] from vault [{1}]' -f $backupPolicy.Name, $resourceName), 'Remove')) {
$null = Remove-AzDataProtectionBackupPolicy -ResourceGroupName $resourceGroupName -VaultName $resourceName -Name $backupPolicy.name
}
}
# Remove backup vault
Write-Verbose (' [-] Removing Backup vault [{0}]' -f $resourceName) -Verbose
if ($PSCmdlet.ShouldProcess("Backup vault with ID [$ResourceId]", 'Remove')) {
$null = Remove-AzDataProtectionBackupVault -ResourceGroupName $resourceGroupName -VaultName $resourceName
}
break
}
'Microsoft.OperationalInsights/workspaces' {
$resourceGroupName = $ResourceId.Split('/')[4]
$resourceName = Split-Path $ResourceId -Leaf
# Force delete workspace (cannot be recovered)
if ($PSCmdlet.ShouldProcess("Log Analytics Workspace [$resourceName]", 'Remove')) {
Write-Verbose ('[*] Purging resource [{0}] of type [{1}]' -f $resourceName, $Type) -Verbose
$null = Remove-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName -Name $resourceName -Force -ForceDelete
}
break
}
'Microsoft.VirtualMachineImages/imageTemplates' {
# Note: If you ever run into the issue that you cannot remove the image template because of an issue with the MSI (e.g., because the below logic was not executed in the pipeline), you can follow these manual steps:
# 1. Unassign the existing MSI (az image builder identity remove --resource-group <itRg> --name <itName> --user-assigned <msiResourceId> --yes)
# 2. Trigger image template removal (will fail, but remove the cached 'running' state)
# 3. Assign a new MSI (az image builder identity assign --resource-group <itRg> --name <itName> --user-assigned <msiResourceId>)
# 4. Trigger image template removal again, which removes the resource for good
$resourceGroupName = $ResourceId.Split('/')[4]
$resourceName = Split-Path $ResourceId -Leaf
$subscriptionId = $ResourceId.Split('/')[2]
# Remove resource
if ($PSCmdlet.ShouldProcess("Image Template [$resourceName]", 'Remove')) {
$removeRequestInputObject = @{
Method = 'DELETE'
Path = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.VirtualMachineImages/imageTemplates/{2}?api-version=2022-07-01' -f $subscriptionId, $resourceGroupName, $resourceName
}
$removalResponse = Invoke-AzRestMethod @removeRequestInputObject
if ($removalResponse.StatusCode -notlike '2*') {
$responseContent = $removalResponse.Content | ConvertFrom-Json
throw ('{0} : {1}' -f $responseContent.error.code, $responseContent.error.message)
}
# Wait for template to be removed. If we don't wait, it can happen that its MSI is removed too soon, locking the resource from deletion
$retryCount = 1
$retryLimit = 240
$retryInterval = 15
do {
$getRequestInputObject = @{
Method = 'GET'
Path = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.VirtualMachineImages/imageTemplates/{2}?api-version=2022-07-01' -f $subscriptionId, $resourceGroupName, $resourceName
}
$getResponse = Invoke-AzRestMethod @getRequestInputObject
if ($getResponse.StatusCode -eq 400) {
# Invalid request
throw ($getResponse.Content | ConvertFrom-Json).error.message
} elseif ($getResponse.StatusCode -eq 404) {
# Resource not found, removal was successful
$templateExists = $false
} elseif ($getResponse.StatusCode -eq '200') {
# Resource still around - try again
$templateExists = $true
Write-Verbose (' [⏱️] Waiting {0} seconds for Image Template to be removed. [{1}/{2}]' -f $retryInterval, $retryCount, $retryLimit) -Verbose
Start-Sleep -Seconds $retryInterval
$retryCount++
} else {
throw ('Failed request. Response: [{0}]' -f ($getResponse | Out-String))
}
} while ($templateExists -and $retryCount -lt $retryLimit)
if ($retryCount -ge $retryLimit) {
Write-Warning (' [!] Image Template [{0}] was not removed after {1} seconds. Continuing with resource removal.' -f $resourceName, ($retryCount * $retryInterval))
break
}
}
break
}
'Microsoft.MachineLearningServices/workspaces' {
$subscriptionId = $ResourceId.Split('/')[2]
$resourceGroupName = $ResourceId.Split('/')[4]
$resourceName = Split-Path $ResourceId -Leaf
# Purge service
$purgePath = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.MachineLearningServices/workspaces/{2}?api-version=2023-06-01-preview&forceToPurge=true' -f $subscriptionId, $resourceGroupName, $resourceName
$purgeRequestInputObject = @{
Method = 'DELETE'
Path = $purgePath
}
Write-Verbose ('[*] Purging resource [{0}] of type [{1}]' -f $resourceName, $Type) -Verbose
if ($PSCmdlet.ShouldProcess("Machine Learning Workspace [$resourceName]", 'Purge')) {
$purgeResource = Invoke-AzRestMethod @purgeRequestInputObject
if ($purgeResource.StatusCode -notlike '2*') {
$responseContent = $purgeResource.Content | ConvertFrom-Json
throw ('{0} : {1}' -f $responseContent.error.code, $responseContent.error.message)
}
# Wait for workspace to be purged. If it is not purged it has a chance of being soft-deleted via RG deletion (not purged)
# The consecutive deployments will fail because it is not purged.
$retryCount = 0
$retryLimit = 240
$retryInterval = 15
do {
$retryCount++
if ($retryCount -ge $retryLimit) {
Write-Warning (' [!] Workspace [{0}] was not purged after {1} seconds. Continuing with resource removal.' -f $resourceName, ($retryCount * $retryInterval))
break
}
Write-Verbose (' [⏱️] Waiting {0} seconds for workspace to be purged.' -f $retryInterval) -Verbose
Start-Sleep -Seconds $retryInterval
$workspace = Get-AzMLWorkspace -Name $resourceName -ResourceGroupName $resourceGroupName -SubscriptionId $subscriptionId -ErrorAction SilentlyContinue
$workspaceExists = $workspace.count -gt 0
} while ($workspaceExists)
}
break
}
{ $PSItem -eq 'Microsoft.Subscription/aliases' -and $ResourceId -like '*dep-sub-blzv-tests*ssa*' } {
$subscriptionName = $ResourceId.Split('/')[4]
$subscription = Get-AzSubscription | Where-Object { $_.Name -eq $subscriptionName }
$subscriptionId = $subscription.Id
$subscriptionState = $subscription.State
$null = Select-AzSubscription -SubscriptionId $subscriptionId -WarningAction 'SilentlyContinue'
# Delete NetworkWatcher resource group
if ((Get-AzResourceGroup -Name 'NetworkWatcherRG' -ErrorAction SilentlyContinue)) {
if ($PSCmdlet.ShouldProcess('Resource Group [NetworkWatcherRG]', 'Remove')) {
$null = Remove-AzResourceGroup -Name 'NetworkWatcherRG' -Force
}
}
# Moving Subscription to Management Group: bicep-lz-vending-automation-decom
if (-not (Get-AzManagementGroupSubscription -GroupName 'bicep-lz-vending-automation-decom' -SubscriptionId $subscriptionId -ErrorAction 'SilentlyContinue')) {
if ($PSCmdlet.ShouldProcess("Subscription [$subscriptionName] to Management Group: bicep-lz-vending-automation-decom", 'Move')) {
$null = New-AzManagementGroupSubscription -GroupName 'bicep-lz-vending-automation-decom' -SubscriptionId $subscriptionId
}
}
if ($subscriptionState -eq 'Enabled') {
Write-Verbose ('[*] Disabling resource [{0}] of type [{1}]' -f $subscriptionName, $Type) -Verbose
if ($PSCmdlet.ShouldProcess("Subscription [$subscriptionName]", 'Remove')) {
$null = Disable-AzSubscription -SubscriptionId $subscriptionId -Confirm:$false
}
}
break
}
### CODE LOCATION: Add custom removal action here
Default {
if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) {
$null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop'
}
}
}
}