utilities/pipelines/e2eValidation/resourceRemoval/helper/Remove-Deployment.ps1 (144 lines of code) (raw):

<# .SYNOPSIS Invoke the removal of a deployed module .DESCRIPTION Invoke the removal of a deployed module. Requires the resource in question to be tagged with 'removeModule = <moduleName>' .PARAMETER ModuleName Mandatory. The name of the module to remove .PARAMETER ResourceGroupName 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 DeploymentName(s) Optional. The name(s) of the deployment(s). Combined with resources provide via the resource Id(s). .PARAMETER ResourceId(s) Optional. The resource Id(s) of the resources to remove. Combined with resources found via the deployment name(s). .PARAMETER TemplateFilePath Optional. The path to the template used for the deployment(s). Used to determine the level/scope (e.g. subscription). Required if deploymentName(s) are provided. .PARAMETER RemoveFirstSequence Optional. The order of resource types to remove before all others .PARAMETER RemoveLastSequence Optional. The order of resource types to remove after all others .EXAMPLE Remove-Deployment -DeploymentNames @('KeyVault-t1','KeyVault-t2') -TemplateFilePath 'C:/main.json' Remove all resources deployed via the with deployment names 'KeyVault-t1' & 'KeyVault-t2' #> function Remove-Deployment { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $false)] [string] $ResourceGroupName, [Parameter(Mandatory = $false)] [string] $ManagementGroupId, [Parameter(Mandatory = $false)] [string[]] $DeploymentNames = @(), [Parameter(Mandatory = $false)] [string[]] $ResourceIds = @(), [Parameter(Mandatory = $false)] [string] $TemplateFilePath, [Parameter(Mandatory = $false)] [string[]] $RemoveFirstSequence = @(), [Parameter(Mandatory = $false)] [string[]] $RemoveLastSequence = @() ) begin { Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) # Load helper . (Join-Path (Get-Item -Path $PSScriptRoot).parent.parent.parent.FullName 'sharedScripts' 'Get-ScopeOfTemplateFile.ps1') . (Join-Path (Split-Path $PSScriptRoot -Parent) 'helper' 'Get-DeploymentTargetResourceList.ps1') . (Join-Path (Split-Path $PSScriptRoot -Parent) 'helper' 'Get-ResourceIdsAsFormattedObjectList.ps1') . (Join-Path (Split-Path $PSScriptRoot -Parent) 'helper' 'Get-OrderedResourcesList.ps1') . (Join-Path (Split-Path $PSScriptRoot -Parent) 'helper' 'Remove-ResourceList.ps1') } process { $azContext = Get-AzContext $deployedTargetResources = $ResourceIds if ($DeploymentNames.Count -gt 0) { # Prepare data # ============ $deploymentScope = Get-ScopeOfTemplateFile -TemplateFilePath $TemplateFilePath # Fundamental checks 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 } # Fetch deployments # ================= $deploymentsInputObject = @{ DeploymentNames = $DeploymentNames Scope = $deploymentScope } if (-not [String]::IsNullOrEmpty($ResourceGroupName)) { $deploymentsInputObject['resourceGroupName'] = $ResourceGroupName } if (-not [String]::IsNullOrEmpty($ManagementGroupId)) { $deploymentsInputObject['ManagementGroupId'] = $ManagementGroupId } # In case the function also returns an error, we'll throw a corresponding exception at the end of this script (see below) $resolveResult = Get-DeploymentTargetResourceList @deploymentsInputObject $deployedTargetResources += $resolveResult.resourcesToRemove } [array] $deployedTargetResources = $deployedTargetResources | Select-Object -Unique Write-Verbose ('Total number of deployment target resources after fetching deployments [{0}]' -f $deployedTargetResources.Count) -Verbose if (-not $deployedTargetResources) { # Nothing to do return } # Pre-Filter & order items # ======================== $rawTargetResourceIdsToRemove = $deployedTargetResources | Sort-Object -Culture 'en-US' -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 # Format items # ============ [array] $resourcesToRemove = Get-ResourceIdsAsFormattedObjectList -ResourceIds $rawTargetResourceIdsToRemove Write-Verbose ('Total number of deployment target resources after formatting items [{0}]' -f $resourcesToRemove.Count) -Verbose # Filter resources # ================ # Resource IDs in the below list are ignored by the removal $resourceIdsToIgnore = @( '/subscriptions/{0}/resourceGroups/NetworkWatcherRG' -f $azContext.Subscription.Id ) # Resource IDs starting with a prefix in the below list are ignored by the removal $resourceIdPrefixesToIgnore = @( '/subscriptions/{0}/providers/Microsoft.Security/autoProvisioningSettings/' -f $azContext.Subscription.Id '/subscriptions/{0}/providers/Microsoft.Security/deviceSecurityGroups/' -f $azContext.Subscription.Id '/subscriptions/{0}/providers/Microsoft.Security/iotSecuritySolutions/' -f $azContext.Subscription.Id '/subscriptions/{0}/providers/Microsoft.Security/pricings/' -f $azContext.Subscription.Id '/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 '|') + ')' if ($resourcesToIgnore = $resourcesToRemove | Where-Object { $_.resourceId -in $resourceIdsToIgnore -or $_.resourceId -match $ignorePrefix_regex }) { Write-Verbose 'Resources excluded from removal:' -Verbose $resourcesToIgnore | ForEach-Object { Write-Verbose ('- Ignore [{0}]' -f $_.resourceId) -Verbose } } [array] $resourcesToRemove = $resourcesToRemove | Where-Object { $_.resourceId -notin $resourceIdsToIgnore -and $_.resourceId -notmatch $ignorePrefix_regex } Write-Verbose ('Total number of deployments after filtering all dependency resources [{0}]' -f $resourcesToRemove.Count) -Verbose # Order resources # =============== $orderListInputObject = @{ ResourcesToOrder = $resourcesToRemove RemoveFirstSequence = $RemoveFirstSequence RemoveLastSequence = $RemoveLastSequence } [array] $resourcesToRemove = Get-OrderedResourcesList @orderListInputObject Write-Verbose ('Total number of deployments after final ordering of resources [{0}]' -f $resourcesToRemove.Count) -Verbose # Remove resources # ================ if ($resourcesToRemove.Count -gt 0) { if ($PSCmdlet.ShouldProcess(('[{0}] resources' -f (($resourcesToRemove -is [array]) ? $resourcesToRemove.Count : 1)), 'Remove')) { Remove-ResourceList -ResourcesToRemove $resourcesToRemove } } else { Write-Verbose 'Found [0] resources to remove' } # In case any deployment was not resolved as planned we finally want to throw an exception to make this visible in the pipeline if ($resolveResult.resolveError) { throw ('The following error was thrown while resolving the original deployment names: [{0}]' -f $resolveResult.resolveError) } } end { Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) } }