diff --git a/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml b/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml index 99f9267941..0f4de64345 100644 --- a/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml +++ b/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml @@ -105,6 +105,7 @@ jobs: @{ Name = 'Az.CognitiveServices' }, @{ Name = 'Az.Compute' }, @{ Name = 'Az.KeyVault' }, + @{ Name = 'Az.MachineLearningServices' }, @{ Name = 'Az.Monitor' }, @{ Name = 'Az.OperationalInsights' }, @{ Name = 'Az.RecoveryServices' } diff --git a/.github/actions/templates/setEnvironment/action.yml b/.github/actions/templates/setEnvironment/action.yml index 7000ba3245..e7e59cf28d 100644 --- a/.github/actions/templates/setEnvironment/action.yml +++ b/.github/actions/templates/setEnvironment/action.yml @@ -62,6 +62,7 @@ runs: @{ Name = 'Az.CognitiveServices' }, @{ Name = 'Az.Compute' }, @{ Name = 'Az.KeyVault' }, + @{ Name = 'Az.MachineLearningServices' }, @{ Name = 'Az.Monitor' }, @{ Name = 'Az.OperationalInsights' }, @{ Name = 'Az.RecoveryServices' } diff --git a/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceLockRemoval.ps1 b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceLockRemoval.ps1 new file mode 100644 index 0000000000..ccd41cbceb --- /dev/null +++ b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceLockRemoval.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS +Remove resource locks from a resource or a specific resource lock. + +.DESCRIPTION +Remove resource locks from a resource or a specific resource lock. + +.PARAMETER ResourceId +Mandatory. The resourceID of the resource to check, and remove a resource lock from. + +.PARAMETER Type +Optional. The type of the resource. If the resource is a lock, the lock itself will be removed. If the resource is a resource, all locks on the resource will be removed. If not specified, the resource will be checked for locks, and if any are found, all locks will be removed. + +.PARAMETER RetryLimit +Optional. The number of times to retry checking if the lock is removed. + +.PARAMETER RetryInterval +Optional. The number of seconds to wait between each retry. + +.EXAMPLE +Invoke-ResourceLockRemoval -ResourceId '/subscriptions/.../resourceGroups/validation-rg/.../resource-name' + +Check if the resource 'resource-name' is locked. If it is, remove the lock. +#> +function Invoke-ResourceLockRemoval { + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceId, + + [Parameter(Mandatory = $false)] + [string] $Type, + + [Parameter(Mandatory = $false)] + [int] $RetryLimit = 10, + + [Parameter(Mandatory = $false)] + [int] $RetryInterval = 10 + ) + # Load functions + . (Join-Path $PSScriptRoot 'Invoke-ResourceLockRetrieval.ps1') + + $resourceLock = Invoke-ResourceLockRetrieval -ResourceId $ResourceId -Type $Type + + $isLocked = $resourceLock.count -gt 0 + if (-not $isLocked) { + return + } + + $resourceLock | ForEach-Object { + Write-Warning (' [-] Removing lock [{0}] on [{1}] of type [{2}].' -f $_.Name, $_.ResourceName, $_.ResourceType) + if ($PSCmdlet.ShouldProcess(('Lock [{0}] on resource [{1}] of type [{2}].' -f $_.Name, $_.ResourceName, $_.ResourceType ), 'Remove')) { + $null = $_ | Remove-AzResourceLock -Force + } + } + + $retryCount = 0 + do { + $retryCount++ + if ($retryCount -ge $RetryLimit) { + Write-Warning (' [!] Lock was not removed after {1} seconds. Continuing with resource removal.' -f ($retryCount * $RetryInterval)) + break + } + Write-Verbose ' [⏱️] Waiting for lock to be removed.' -Verbose + Start-Sleep -Seconds $RetryInterval + + # Rechecking the resource locks to see if they have been removed. + $resourceLock = Invoke-ResourceLockRetrieval -ResourceId $ResourceId -Type $Type + $isLocked = $resourceLock.count -gt 0 + } while ($isLocked) + + Write-Verbose (' [-] [{0}] resource lock(s) removed.' -f $resourceLock.count) -Verbose +} diff --git a/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceLockRetrieval.ps1 b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceLockRetrieval.ps1 new file mode 100644 index 0000000000..90c35dd598 --- /dev/null +++ b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceLockRetrieval.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS +Gets resource locks on a resource or a specific resource lock. + +.DESCRIPTION +Gets resource locks on a resource or a specific resource lock. + +.PARAMETER ResourceId +Mandatory. The resourceID of the resource to check or the resource lock to check. + +.PARAMETER Type +Optional. The type of the resource. +If the resource is a lock, the lock itself will be returned. +If the resource is not a lock, all locks on the resource will be returned. + +.EXAMPLE +Invoke-ResourceLockRetrieval -ResourceId '/subscriptions/.../resourceGroups/validation-rg/.../resource-name' + +Check if the resource 'resource-name' is locked. If it is, return the lock. + +.EXAMPLE +Invoke-ResourceLockRetrieval -ResourceId '/subscriptions/.../resourceGroups/validation-rg/.../resource-name/providers/Microsoft.Authorization/locks/lock-name' -Type 'Microsoft.Authorization/locks' + +Return the lock 'lock-name' on the resource 'resource-name'. + +.NOTES +Needed as the AzPwsh cmdlet Get-AzResourceLock does not support getting a specific lock by LockId. +#> +function Invoke-ResourceLockRetrieval { + [OutputType([System.Management.Automation.PSCustomObject])] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceId, + + [Parameter(Mandatory = $false)] + [string] $Type = '' + ) + if ($Type -eq 'Microsoft.Authorization/locks') { + $lockName = ($ResourceId -split '/')[-1] + $lockScope = ($ResourceId -split '/providers/Microsoft.Authorization/locks')[0] + return Get-AzResourceLock -LockName $lockName -Scope $lockScope -ErrorAction SilentlyContinue + } else { + return Get-AzResourceLock -Scope $ResourceId -ErrorAction SilentlyContinue + } +} diff --git a/utilities/pipelines/resourceRemoval/helper/Invoke-ResourcePostRemoval.ps1 b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourcePostRemoval.ps1 index d158ba6b94..7a4178a3e0 100644 --- a/utilities/pipelines/resourceRemoval/helper/Invoke-ResourcePostRemoval.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourcePostRemoval.ps1 @@ -27,9 +27,9 @@ function Invoke-ResourcePostRemoval { [string] $Type ) - switch ($type) { + switch ($Type) { 'Microsoft.AppConfiguration/configurationStores' { - $subscriptionId = $resourceId.Split('/')[2] + $subscriptionId = $ResourceId.Split('/')[2] $resourceName = Split-Path $ResourceId -Leaf # Fetch service in soft-delete @@ -38,7 +38,7 @@ function Invoke-ResourcePostRemoval { Method = 'GET' Path = $getPath } - $softDeletedConfigurationStore = ((Invoke-AzRestMethod @getRequestInputObject).Content | ConvertFrom-Json).value | Where-Object { $_.properties.configurationStoreId -eq $resourceId } + $softDeletedConfigurationStore = ((Invoke-AzRestMethod @getRequestInputObject).Content | ConvertFrom-Json).value | Where-Object { $_.properties.configurationStoreId -eq $ResourceId } if ($softDeletedConfigurationStore) { # Purge service @@ -47,6 +47,7 @@ function Invoke-ResourcePostRemoval { Method = 'POST' Path = $purgePath } + Write-Verbose ('[*] Purging resource [{0}] of type [{1}]' -f $resourceName, $Type) -Verbose if ($PSCmdlet.ShouldProcess(('App Configuration Store with ID [{0}]' -f $softDeletedConfigurationStore.properties.configurationStoreId), 'Purge')) { $response = Invoke-AzRestMethod @purgeRequestInputObject if ($response.StatusCode -ne 200) { @@ -61,7 +62,7 @@ function Invoke-ResourcePostRemoval { $matchingKeyVault = Get-AzKeyVault -InRemovedState | Where-Object { $_.resourceId -eq $ResourceId } if ($matchingKeyVault -and -not $matchingKeyVault.EnablePurgeProtection) { - Write-Verbose ("Purging key vault [$resourceName]") -Verbose + Write-Verbose ('[*] Purging resource [{0}] of type [{1}]' -f $resourceName, $Type) -Verbose if ($PSCmdlet.ShouldProcess(('Key Vault with ID [{0}]' -f $matchingKeyVault.Id), 'Purge')) { try { $null = Remove-AzKeyVault -ResourceId $matchingKeyVault.Id -InRemovedState -Force -Location $matchingKeyVault.Location -ErrorAction 'Stop' @@ -77,11 +78,12 @@ function Invoke-ResourcePostRemoval { break } 'Microsoft.CognitiveServices/accounts' { - $resourceGroupName = $resourceId.Split('/')[4] + $resourceGroupName = $ResourceId.Split('/')[4] $resourceName = Split-Path $ResourceId -Leaf $matchingAccount = Get-AzCognitiveServicesAccount -InRemovedState | Where-Object { $_.AccountName -eq $resourceName } if ($matchingAccount) { + Write-Verbose ('[*] Purging resource [{0}] of type [{1}]' -f $resourceName, $Type) -Verbose if ($PSCmdlet.ShouldProcess(('Cognitive services account with ID [{0}]' -f $matchingAccount.Id), 'Purge')) { $null = Remove-AzCognitiveServicesAccount -InRemovedState -Force -Location $matchingAccount.Location -ResourceGroupName $resourceGroupName -Name $matchingAccount.AccountName } @@ -89,7 +91,7 @@ function Invoke-ResourcePostRemoval { break } 'Microsoft.ApiManagement/service' { - $subscriptionId = $resourceId.Split('/')[2] + $subscriptionId = $ResourceId.Split('/')[2] $resourceName = Split-Path $ResourceId -Leaf # Fetch service in soft-delete @@ -98,7 +100,7 @@ function Invoke-ResourcePostRemoval { Method = 'GET' Path = $getPath } - $softDeletedService = ((Invoke-AzRestMethod @getRequestInputObject).Content | ConvertFrom-Json).value | Where-Object { $_.properties.serviceId -eq $resourceId } + $softDeletedService = ((Invoke-AzRestMethod @getRequestInputObject).Content | ConvertFrom-Json).value | Where-Object { $_.properties.serviceId -eq $ResourceId } if ($softDeletedService) { # Purge service @@ -107,6 +109,7 @@ function Invoke-ResourcePostRemoval { Method = 'DELETE' Path = $purgePath } + Write-Verbose ('[*] Purging resource [{0}] of type [{1}]' -f $resourceName, $Type) -Verbose if ($PSCmdlet.ShouldProcess(('API management service with ID [{0}]' -f $softDeletedService.properties.serviceId), 'Purge')) { $null = Invoke-AzRestMethod @purgeRequestInputObject } @@ -116,7 +119,7 @@ function Invoke-ResourcePostRemoval { 'Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems' { # Remove protected VM # Required if e.g. a VM was listed in an RSV and only that VM is removed - $vaultId = $resourceId.split('/backupFabrics/')[0] + $vaultId = $ResourceId.split('/backupFabrics/')[0] $resourceName = Split-Path $ResourceId -Leaf $softDeleteStatus = (Get-AzRecoveryServicesVaultProperty -VaultId $vaultId).SoftDeleteFeatureState if ($softDeleteStatus -ne 'Disabled') { @@ -132,7 +135,7 @@ function Invoke-ResourcePostRemoval { Name = $resourceName } if ($backupItem = Get-AzRecoveryServicesBackupItem @backupItemInputObject -ErrorAction 'SilentlyContinue') { - Write-Verbose ('Removing Backup item [{0}] from RSV [{1}]' -f $backupItem.Name, $vaultId) -Verbose + Write-Verbose (' [-] Removing Backup item [{0}] from RSV [{1}]' -f $backupItem.Name, $vaultId) -Verbose if ($backupItem.DeleteState -eq 'ToBeDeleted') { if ($PSCmdlet.ShouldProcess('Soft-deleted backup data removal', 'Undo')) { diff --git a/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceRemoval.ps1 b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceRemoval.ps1 index 3885febb63..b0065c67a4 100644 --- a/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceRemoval.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Invoke-ResourceRemoval.ps1 @@ -26,10 +26,18 @@ function Invoke-ResourceRemoval { [Parameter(Mandatory = $true)] [string] $Type ) + # Load functions + . (Join-Path $PSScriptRoot 'Invoke-ResourceLockRemoval.ps1') - switch ($type) { + # 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] + $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 @@ -37,28 +45,29 @@ function Invoke-ResourceRemoval { break } 'Microsoft.Authorization/locks' { - $lockName = ($resourceId -split '/')[-1] - $lockScope = ($resourceId -split '/providers/Microsoft.Authorization/locks')[0] - - $null = Remove-AzResourceLock -LockName $lockName -Scope $lockScope -Force - Write-Verbose "Removed lock [$resourceName]. Waiting 10 seconds for propagation." -Verbose - Start-Sleep 10 + if ($PSCmdlet.ShouldProcess("Lock with ID [$ResourceId]", 'Remove')) { + Invoke-ResourceLockRemoval -ResourceId $ResourceId -Type $Type + } break } 'Microsoft.KeyVault/vaults/keys' { - Write-Verbose ('Skip resource removal for type [{0}]. Reason: handled by different logic.' -f $type) -Verbose + $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' { - Write-Verbose ('Skip resource removal for type [{0}]. Reason: handled by different logic.' -f $type) -Verbose + $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 ('Skip resource removal for type [{0}]. Reason: The Service Bus''s default authorization key [RootManageSharedAccessKey] cannot be removed.' -f $type) -Verbose + Write-Verbose ('[/] Skipping resource [RootManageSharedAccessKey] of type [{0}]. Reason: The Service Bus''s default authorization key cannot be removed' -f $Type) -Verbose } else { - $null = Remove-AzResource -ResourceId $resourceId -Force -ErrorAction 'Stop' + if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) { + $null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop' + } } break } @@ -66,23 +75,23 @@ function Invoke-ResourceRemoval { # Pre-Removal # ----------- # Remove access policies on key vault - $resourceGroupName = $resourceId.Split('/')[4] - $resourceName = Split-Path $resourceId -Leaf + $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 - Write-Verbose ('keyVaultResourceId [{0}]' -f $keyVaultResourceId) -Verbose - Write-Verbose ('objectId [{0}]' -f $objectId) -Verbose if ($PSCmdlet.ShouldProcess(('Access policy [{0}] from key vault [{1}]' -f $objectId, $keyVaultName), 'Remove')) { $null = Remove-AzKeyVaultAccessPolicy -VaultName $keyVaultName -ObjectId $objectId } # Actual removal # -------------- - $null = Remove-AzResource -ResourceId $resourceId -Force -ErrorAction 'Stop' + if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) { + $null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop' + } break } 'Microsoft.RecoveryServices/vaults/backupstorageconfig' { @@ -100,74 +109,87 @@ function Invoke-ResourceRemoval { # 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' + 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 + $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 + 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 + $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 + 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 # -------------- - $null = Remove-AzResource -ResourceId $resourceId -Force -ErrorAction 'Stop' + if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) { + $null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop' + } break } 'Microsoft.OperationalInsights/workspaces' { - $resourceGroupName = $resourceId.Split('/')[4] - $resourceName = Split-Path $resourceId -Leaf + $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.DevTestLab/labs' { - $resourceGroupName = $resourceId.Split('/')[4] - $resourceName = Split-Path $resourceId -Leaf - if ($PSCmdlet.ShouldProcess("DevTestLab Lab [$resourceName]", 'Remove')) { - Get-AzResourceLock -ResourceGroupName $resourceGroupName | - Where-Object -FilterScript { $PSItem.properties.notes -eq "Reserved resource locked by '$resourceName' lab." } | - ForEach-Object { - $null = Remove-AzResourceLock -LockId $PSItem.LockId -Force - Write-Verbose "Removed lock [$($PSItem.Name)] created by the DevTest Lab [$resourceName] on resource [$($PSItem.ResourceName)]. Waiting 10 seconds for propagation." -Verbose - Start-Sleep 10 - } - $null = Remove-AzResource -ResourceId $resourceId -Force -ErrorAction 'Stop' - } - break - } 'Microsoft.MachineLearningServices/workspaces' { - $subscriptionId = $resourceId.Split('/')[2] - $resourceGroupName = $resourceId.Split('/')[4] - $resourceName = Split-Path $resourceId -Leaf - $purgePath = 'subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.MachineLearningServices/workspaces/{2}?api-version=2023-04-01-preview&forceToPurge={3}' -f $subscriptionId, $resourceGroupName, $resourceName, $true + $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 } - if ($PSCmdlet.ShouldProcess("Machine Learning Workspace [$resourceName]", 'Remove')) { - $null = Invoke-AzRestMethod @purgeRequestInputObject + 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 } - ### CODE LOCATION: Add custom removal action here Default { - $null = Remove-AzResource -ResourceId $resourceId -Force -ErrorAction 'Stop' + if ($PSCmdlet.ShouldProcess("Resource with ID [$ResourceId]", 'Remove')) { + $null = Remove-AzResource -ResourceId $ResourceId -Force -ErrorAction 'Stop' + } } } } diff --git a/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 b/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 index 75f9fd31fa..a964a30298 100644 --- a/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 @@ -15,12 +15,6 @@ Optional. The resource group of the resource to remove .PARAMETER ManagementGroupId Optional. The ID of the management group to fetch deployments from. Relevant for management-group level deployments. -.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 - .PARAMETER DeploymentNames Optional. The deployment names to use for the removal @@ -52,13 +46,7 @@ function Remove-Deployment { [string] $TemplateFilePath, [Parameter(Mandatory = $false)] - [string[]] $RemovalSequence = @(), - - [Parameter(Mandatory = $false)] - [int] $SearchRetryLimit = 40, - - [Parameter(Mandatory = $false)] - [int] $SearchRetryInterval = 60 + [string[]] $RemovalSequence = @() ) begin { @@ -80,7 +68,7 @@ function Remove-Deployment { $deploymentScope = Get-ScopeOfTemplateFile -TemplateFilePath $TemplateFilePath # Fundamental checks - if ($deploymentScope -eq 'resourcegroup' -and -not (Get-AzResourceGroup -Name $resourceGroupName -ErrorAction 'SilentlyContinue')) { + if ($deploymentScope -eq 'resourcegroup' -and -not (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction 'SilentlyContinue')) { Write-Verbose "Resource group [$ResourceGroupName] does not exist (anymore). Skipping removal of its contained resources" -Verbose return } @@ -94,8 +82,8 @@ function Remove-Deployment { Name = $deploymentName Scope = $deploymentScope } - if (-not [String]::IsNullOrEmpty($resourceGroupName)) { - $deploymentsInputObject['resourceGroupName'] = $resourceGroupName + if (-not [String]::IsNullOrEmpty($ResourceGroupName)) { + $deploymentsInputObject['resourceGroupName'] = $ResourceGroupName } if (-not [String]::IsNullOrEmpty($ManagementGroupId)) { $deploymentsInputObject['ManagementGroupId'] = $ManagementGroupId @@ -114,7 +102,7 @@ function Remove-Deployment { # Pre-Filter & order items # ======================== $rawTargetResourceIdsToRemove = $deployedTargetResources | Sort-Object -Property { $_.Split('/').Count } -Descending | Select-Object -Unique - Write-Verbose ('Total number of deployment target resources after pre-filtering (duplicates) & ordering items [{0}]' -f $rawTargetResourceIdsToRemove.Count) -Verbose + Write-Verbose ('Total number of deployment target resources after pre-filtering (duplicates) & ordering items [{0}]' -f $rawTargetResourceIdsToRemove.Count) -Verbose # Format items # ============ @@ -142,7 +130,7 @@ function Remove-Deployment { '/subscriptions/{0}/providers/Microsoft.Security/securityContacts/' -f $azContext.Subscription.Id '/subscriptions/{0}/providers/Microsoft.Security/workspaceSettings/' -f $azContext.Subscription.Id ) - [regex] $ignorePrefix_regex = '(?i)^(' + (($resourceIdPrefixesToIgnore | ForEach-Object { [regex]::escape($_) }) –join '|') + ')' + [regex] $ignorePrefix_regex = '(?i)^(' + (($resourceIdPrefixesToIgnore | ForEach-Object { [regex]::escape($_) }) -join '|') + ')' if ($resourcesToIgnore = $resourcesToRemove | Where-Object { $_.resourceId -in $resourceIdsToIgnore -or $_.resourceId -match $ignorePrefix_regex }) { diff --git a/utilities/pipelines/resourceRemoval/helper/Remove-ResourceList.ps1 b/utilities/pipelines/resourceRemoval/helper/Remove-ResourceList.ps1 index 924c308db6..cc35460c8a 100644 --- a/utilities/pipelines/resourceRemoval/helper/Remove-ResourceList.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Remove-ResourceList.ps1 @@ -40,7 +40,7 @@ function Remove-ResourceListInner { if ($alreadyProcessed) { # Skipping - Write-Verbose ('[/] Skipping resource [{0}] of type [{1}] as a parent resource was already processed' -f $resourceName, $resource.type) -Verbose + Write-Verbose ('[/] Skipping resource [{0}] of type [{1}]. Reason: Its parent resource was already processed' -f $resourceName, $resource.type) -Verbose [array]$processedResources += $resource.resourceId [array]$resourcesToRetry = $resourcesToRetry | Where-Object { $_.resourceId -notmatch $resource.resourceId } } else { @@ -54,7 +54,7 @@ function Remove-ResourceListInner { [array]$processedResources += $resource.resourceId [array]$resourcesToRetry = $resourcesToRetry | Where-Object { $_.resourceId -notmatch $resource.resourceId } } catch { - Write-Warning ('Removal moved back for re-try. Reason: [{0}]' -f $_.Exception.Message) + Write-Warning ('[!] Removal moved back for retry. Reason: [{0}]' -f $_.Exception.Message) [array]$resourcesToRetry += $resource } } @@ -115,7 +115,7 @@ function Remove-ResourceList { if (-not $resourcesToRetry) { break } - Write-Verbose ('Re-try removal of remaining [{0}] resources. Waiting [{1}] seconds. Round [{2}|{3}]' -f (($resourcesToRetry -is [array]) ? $resourcesToRetry.Count : 1), $removalRetryInterval, $removalRetryCount, $removalRetryLimit) + Write-Verbose ('Retry removal of remaining [{0}] resources. Waiting [{1}] seconds. Round [{2}|{3}]' -f (($resourcesToRetry -is [array]) ? $resourcesToRetry.Count : 1), $removalRetryInterval, $removalRetryCount, $removalRetryLimit) $removalRetryCount++ Start-Sleep $removalRetryInterval } while ($removalRetryCount -le $removalRetryLimit)