From 174d56edcf902cd2609cb8bfd86c3b2c45c4e099 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Sat, 15 Nov 2025 14:43:07 +0000 Subject: [PATCH 1/4] feat: add missing features to destroy script --- src/ALZ/Public/Remove-PlatformLandingZone.ps1 | 247 ++++++++++++++++-- 1 file changed, 221 insertions(+), 26 deletions(-) diff --git a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 index 1ff8228..8976a97 100644 --- a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 +++ b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 @@ -15,8 +15,12 @@ function Remove-PlatformLandingZone { 4. Removes subscriptions from management groups and optionally moves them to a target management group 5. Discovers subscriptions from management groups (if not explicitly provided) 6. Deletes management groups in reverse depth order (children before parents) - 7. Deletes all resource groups in the discovered/specified subscriptions (excluding retention patterns) - 8. Resets Microsoft Defender for Cloud plans to Free tier + 7. Deletes management group-level deployments from target management groups (if not being deleted) + 8. Deletes orphaned role assignments from target management groups (if not being deleted) + 9. Deletes all resource groups in the discovered/specified subscriptions (excluding retention patterns) + 10. Resets Microsoft Defender for Cloud plans to Free tier + 11. Deletes all subscription-level deployments + 12. Deletes orphaned role assignments from subscriptions CRITICAL WARNING: This is a highly destructive operation that will permanently delete Azure resources. By default, ALL resource groups in the subscriptions will be deleted unless they match retention patterns. @@ -77,6 +81,18 @@ function Remove-PlatformLandingZone { executing the actual cleanup. Default: $false (execute actual deletions) + .PARAMETER skipDefenderPlanReset + A switch parameter that skips the Microsoft Defender for Cloud plan reset operation. When specified, the + function will not attempt to reset Defender plans to Free tier. This is useful when you want to preserve + existing Defender configurations or when you don't have the necessary permissions. + Default: $false (reset Defender plans) + + .PARAMETER skipDeploymentDeletion + A switch parameter that skips deployment deletion operations at both the management group and subscription + levels. When specified, the function will not delete deployment history records from management groups or + subscriptions. This is useful when you want to preserve deployment records for audit or compliance purposes. + Default: $false (delete deployments) + .EXAMPLE Remove-PlatformLandingZone -managementGroups @("alz-platform", "alz-landingzones") @@ -133,6 +149,12 @@ function Remove-PlatformLandingZone { Skips management group processing entirely and only deletes resource groups from the specified subscription. This is useful when you want to clean subscriptions without touching the management group structure. + .EXAMPLE + Remove-PlatformLandingZone -managementGroups @("alz-test") -skipDefenderPlanReset -skipDeploymentDeletion + + Removes management groups and resource groups but skips resetting Microsoft Defender plans and deleting + deployment history. Useful for faster cleanup when Defender configuration and audit trails should be preserved. + .NOTES This function uses Azure CLI commands and requires: - Azure CLI to be installed and available in the system path @@ -189,7 +211,9 @@ function Remove-PlatformLandingZone { [switch]$bypassConfirmation, [int]$bypassConfirmationTimeoutSeconds = 30, [int]$throttleLimit = 11, - [switch]$planMode + [switch]$planMode, + [switch]$skipDefenderPlanReset, + [switch]$skipDeploymentDeletion ) function Write-ToConsoleLog { @@ -310,6 +334,8 @@ function Remove-PlatformLandingZone { if ($PSCmdlet.ShouldProcess("Delete Management Groups and Clean Subscriptions", "delete")) { + $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString() + if($bypassConfirmation) { Write-ToConsoleLog "Bypass confirmation enabled, proceeding without prompts..." -IsWarning Write-ToConsoleLog "This is a highly destructive operation that will permanently delete Azure resources!" -IsWarning @@ -401,7 +427,6 @@ function Remove-PlatformLandingZone { } $funcGetManagementGroupChildrenRecursive = ${function:Get-ManagementGroupChildrenRecursive}.ToString() - $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString() if(-not $subscriptionsProvided) { Write-ToConsoleLog "No subscriptions provided, they will be discovered from the target management group hierarchy..." @@ -494,6 +519,104 @@ function Remove-PlatformLandingZone { } } -ThrottleLimit $throttleLimit } + + # Delete deployments from target management groups that are not being deleted + if($managementGroupsFound.Count -ne 0 -and -not $skipDeploymentDeletion) { + $managementGroupsFound | ForEach-Object -Parallel { + $managementGroupId = $_.Name + $managementGroupDisplayName = $_.DisplayName + $deleteTargetManagementGroups = $using:deleteTargetManagementGroups + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + # Only delete deployments if this management group is not being deleted + if(-not $deleteTargetManagementGroups) { + Write-ToConsoleLog "Checking for management group level deployments to delete in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + $deployments = (az deployment mg list --management-group-id $managementGroupId --query "[].name" -o json) | ConvertFrom-Json + + if ($deployments -and $deployments.Count -gt 0) { + Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + + $deployments | ForEach-Object -Parallel { + $deploymentName = $_ + $managementGroupId = $using:managementGroupId + $managementGroupDisplayName = $using:managementGroupDisplayName + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + Write-ToConsoleLog "Deleting deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + $result = $null + if($using:planMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az deployment mg delete --management-group-id $managementGroupId --name $deploymentName" -NoNewLine -Color Gray + } else { + $result = az deployment mg delete --management-group-id $managementGroupId --name $deploymentName 2>&1 + } + + if (!$result) { + Write-ToConsoleLog "Deleted deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:throttleLimit + + Write-ToConsoleLog "All deployments processed in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + } else { + Write-ToConsoleLog "No deployments found in management group: $managementGroupId ($managementGroupDisplayName), skipping." -NoNewLine + } + } else { + Write-ToConsoleLog "Skipping deployment deletion for management group: $managementGroupId ($managementGroupDisplayName) as it is being deleted" -NoNewLine + } + } -ThrottleLimit $throttleLimit + } + + # Delete orphaned role assignments from target management groups that are not being deleted + if($managementGroupsFound.Count -ne 0) { + $managementGroupsFound | ForEach-Object -Parallel { + $managementGroupId = $_.Name + $managementGroupDisplayName = $_.DisplayName + $deleteTargetManagementGroups = $using:deleteTargetManagementGroups + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + # Only delete role assignments if this management group is not being deleted + if(-not $deleteTargetManagementGroups) { + Write-ToConsoleLog "Checking for orphaned role assignments to delete in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + $roleAssignments = (az role assignment list --scope "/providers/Microsoft.Management/managementGroups/$managementGroupId" --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json + + if ($roleAssignments -and $roleAssignments.Count -gt 0) { + Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + + $roleAssignments | ForEach-Object -Parallel { + $roleAssignment = $_ + $managementGroupId = $using:managementGroupId + $managementGroupDisplayName = $using:managementGroupDisplayName + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + $result = $null + if($using:planMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray + } else { + $result = az role assignment delete --ids $roleAssignment.id 2>&1 + } + + if (!$result) { + Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from management group: $managementGroupId ($managementGroupDisplayName)" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:throttleLimit + + Write-ToConsoleLog "All orphaned role assignments processed in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine + } else { + Write-ToConsoleLog "No orphaned role assignments found in management group: $managementGroupId ($managementGroupDisplayName), skipping." -NoNewLine + } + } else { + Write-ToConsoleLog "Skipping orphaned role assignment deletion for management group: $managementGroupId ($managementGroupDisplayName) as it is being deleted" -NoNewLine + } + } -ThrottleLimit $throttleLimit + } } if($subscriptionsProvided) { @@ -501,8 +624,8 @@ function Remove-PlatformLandingZone { foreach($subscription in $subscriptions) { $subscriptionObject = @{ - Id = Test-IsGuid -StringGuid $subscription ? $subscription : (az account list --all --query "[?name=='$subscription'].id" -o tsv) - Name = Test-IsGuid -StringGuid $subscription ? (az account list --all --query "[?id=='$subscription'].name" -o tsv) : $subscription + Id = (Test-IsGuid -StringGuid $subscription) ? $subscription : (az account list --all --query "[?name=='$subscription'].id" -o tsv) + Name = (Test-IsGuid -StringGuid $subscription) ? (az account list --all --query "[?id=='$subscription'].name" -o tsv) : $subscription } if(-not $subscriptionObject.Id -or -not $subscriptionObject.Name) { Write-ToConsoleLog "Subscription not found, skipping: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning @@ -612,35 +735,107 @@ function Remove-PlatformLandingZone { } } - Write-ToConsoleLog "Checking for Microsoft Defender for Cloud Plans to reset in subscription: $($subscription.Name) (ID: $($subscription.Id))" - $defenderPlans = (az security pricing list --subscription $subscription.Id) | ConvertFrom-Json + if(-not $using:skipDefenderPlanReset) { + Write-ToConsoleLog "Checking for Microsoft Defender for Cloud Plans to reset in subscription: $($subscription.Name) (ID: $($subscription.Id))" + $defenderPlans = (az security pricing list --subscription $subscription.Id) | ConvertFrom-Json - $defenderPlans.value | Where-Object { -not $_.deprecated } | ForEach-Object -Parallel { - $subscription = $using:subscription - $funcWriteToConsoleLog = $using:funcWriteToConsoleLog - ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + $defenderPlans.value | Where-Object { -not $_.deprecated } | ForEach-Object -Parallel { + $subscription = $using:subscription + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog - if ($_.pricingTier -ne "Free") { - Write-ToConsoleLog "Resetting Microsoft Defender for Cloud Plan to Free for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $result = $null - if($using:planMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az security pricing create --name $($_.name) --tier `"Free`" --subscription $($subscription.Id)" -NoNewLine -Color Gray + if ($_.pricingTier -ne "Free") { + Write-ToConsoleLog "Resetting Microsoft Defender for Cloud Plan to Free for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + $result = $null + if($using:planMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az security pricing create --name $($_.name) --tier `"Free`" --subscription $($subscription.Id)" -NoNewLine -Color Gray + } else { + $result = (az security pricing create --name $_.name --tier "Free" --subscription $subscription.Id 2>&1) + } + if ($result -like "*must be 'Standard'*") { + Write-ToConsoleLog "Resetting Microsoft Defender for Cloud Plan to Standard as Free is not supported for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + if($using:planMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az security pricing create --name $($_.name) --tier `"Standard`" --subscription $($subscription.Id)" -NoNewLine -Color Gray + } else { + $result = az security pricing create --name $_.name --tier "Standard" --subscription $subscription.Id + } + } + Write-ToConsoleLog "Microsoft Defender for Cloud Plan reset for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } else { - $result = (az security pricing create --name $_.name --tier "Free" --subscription $subscription.Id 2>&1) + Write-ToConsoleLog "Microsoft Defender for Cloud Plan is already set to Free for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine } - if ($result -like "*must be 'Standard'*") { - Write-ToConsoleLog "Resetting Microsoft Defender for Cloud Plan to Standard as Free is not supported for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } -ThrottleLimit $using:throttleLimit + } else { + Write-ToConsoleLog "Skipping Microsoft Defender for Cloud Plans reset in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } + + if(-not $using:skipDeploymentDeletion) { + Write-ToConsoleLog "Checking for subscription level deployments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" + $deployments = (az deployment sub list --subscription $subscription.Id --query "[].name" -o json) | ConvertFrom-Json + + if ($deployments -and $deployments.Count -gt 0) { + Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in subscription: $($subscription.Name) (ID: $($subscription.Id))" + + $deployments | ForEach-Object -Parallel { + $deploymentName = $_ + $subscription = $using:subscription + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + Write-ToConsoleLog "Deleting deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + $result = $null if($using:planMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az security pricing create --name $($_.name) --tier `"Standard`" --subscription $($subscription.Id)" -NoNewLine -Color Gray + Write-ToConsoleLog "(Plan Mode) Would run: az deployment sub delete --name $deploymentName --subscription $($subscription.Id)" -NoNewLine -Color Gray } else { - $result = az security pricing create --name $_.name --tier "Standard" --subscription $subscription.Id + $result = az deployment sub delete --name $deploymentName --subscription $subscription.Id 2>&1 } - } - Write-ToConsoleLog "Microsoft Defender for Cloud Plan reset for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + + if (!$result) { + Write-ToConsoleLog "Deleted deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:throttleLimit + + Write-ToConsoleLog "All deployments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } else { - Write-ToConsoleLog "Microsoft Defender for Cloud Plan is already set to Free for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine + Write-ToConsoleLog "No deployments found in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine } - } -ThrottleLimit $using:throttleLimit + } else { + Write-ToConsoleLog "Skipping subscription level deployment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } + + Write-ToConsoleLog "Checking for orphaned role assignments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + $roleAssignments = (az role assignment list --subscription $subscription.Id --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json + + if ($roleAssignments -and $roleAssignments.Count -gt 0) { + Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + + $roleAssignments | ForEach-Object -Parallel { + $roleAssignment = $_ + $subscription = $using:subscription + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + $result = $null + if($using:planMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray + } else { + $result = az role assignment delete --ids $roleAssignment.id 2>&1 + } + + if (!$result) { + Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:throttleLimit + + Write-ToConsoleLog "All orphaned role assignments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } else { + Write-ToConsoleLog "No orphaned role assignments found in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine + } } -ThrottleLimit $throttleLimit From f319f87bb60db67b44ea6111f20446af52e4fc31 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Sat, 15 Nov 2025 14:45:23 +0000 Subject: [PATCH 2/4] add switch --- src/ALZ/Public/Remove-PlatformLandingZone.ps1 | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 index 8976a97..a672833 100644 --- a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 +++ b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 @@ -93,6 +93,12 @@ function Remove-PlatformLandingZone { subscriptions. This is useful when you want to preserve deployment records for audit or compliance purposes. Default: $false (delete deployments) + .PARAMETER skipOrphanedRoleAssignmentDeletion + A switch parameter that skips orphaned role assignment deletion operations at both the management group and + subscription levels. When specified, the function will not delete role assignments where the principal no + longer exists. This is useful when you want to preserve role assignment records or lack the necessary permissions. + Default: $false (delete orphaned role assignments) + .EXAMPLE Remove-PlatformLandingZone -managementGroups @("alz-platform", "alz-landingzones") @@ -155,6 +161,12 @@ function Remove-PlatformLandingZone { Removes management groups and resource groups but skips resetting Microsoft Defender plans and deleting deployment history. Useful for faster cleanup when Defender configuration and audit trails should be preserved. + .EXAMPLE + Remove-PlatformLandingZone -subscriptions @("Sub-Test-001") -skipOrphanedRoleAssignmentDeletion + + Cleans up the subscription but skips orphaned role assignment deletion. Useful when you want to preserve + role assignments for review or lack the necessary permissions to delete them. + .NOTES This function uses Azure CLI commands and requires: - Azure CLI to be installed and available in the system path @@ -213,7 +225,8 @@ function Remove-PlatformLandingZone { [int]$throttleLimit = 11, [switch]$planMode, [switch]$skipDefenderPlanReset, - [switch]$skipDeploymentDeletion + [switch]$skipDeploymentDeletion, + [switch]$skipOrphanedRoleAssignmentDeletion ) function Write-ToConsoleLog { @@ -570,7 +583,7 @@ function Remove-PlatformLandingZone { } # Delete orphaned role assignments from target management groups that are not being deleted - if($managementGroupsFound.Count -ne 0) { + if($managementGroupsFound.Count -ne 0 -and -not $skipOrphanedRoleAssignmentDeletion) { $managementGroupsFound | ForEach-Object -Parallel { $managementGroupId = $_.Name $managementGroupDisplayName = $_.DisplayName @@ -616,6 +629,8 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Skipping orphaned role assignment deletion for management group: $managementGroupId ($managementGroupDisplayName) as it is being deleted" -NoNewLine } } -ThrottleLimit $throttleLimit + } elseif($managementGroupsFound.Count -ne 0) { + Write-ToConsoleLog "Skipping orphaned role assignment deletion for all management groups as requested" -NoNewLine } } @@ -805,36 +820,40 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Skipping subscription level deployment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } - Write-ToConsoleLog "Checking for orphaned role assignments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $roleAssignments = (az role assignment list --subscription $subscription.Id --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json + if(-not $using:skipOrphanedRoleAssignmentDeletion) { + Write-ToConsoleLog "Checking for orphaned role assignments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + $roleAssignments = (az role assignment list --subscription $subscription.Id --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json - if ($roleAssignments -and $roleAssignments.Count -gt 0) { - Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + if ($roleAssignments -and $roleAssignments.Count -gt 0) { + Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $roleAssignments | ForEach-Object -Parallel { - $roleAssignment = $_ - $subscription = $using:subscription - $funcWriteToConsoleLog = $using:funcWriteToConsoleLog - ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + $roleAssignments | ForEach-Object -Parallel { + $roleAssignment = $_ + $subscription = $using:subscription + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog - Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $result = $null - if($using:planMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray - } else { - $result = az role assignment delete --ids $roleAssignment.id 2>&1 - } + Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + $result = $null + if($using:planMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray + } else { + $result = az role assignment delete --ids $roleAssignment.id 2>&1 + } - if (!$result) { - Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - } else { - Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine - } - } -ThrottleLimit $using:throttleLimit + if (!$result) { + Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:throttleLimit - Write-ToConsoleLog "All orphaned role assignments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + Write-ToConsoleLog "All orphaned role assignments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine + } else { + Write-ToConsoleLog "No orphaned role assignments found in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine + } } else { - Write-ToConsoleLog "No orphaned role assignments found in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine + Write-ToConsoleLog "Skipping orphaned role assignment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } } -ThrottleLimit $throttleLimit From 251c3e9094896ced8264fabf5bffc89d5b80e22a Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Sat, 15 Nov 2025 16:27:45 +0000 Subject: [PATCH 3/4] refactor orphaned role assignments function --- src/ALZ/Public/Remove-PlatformLandingZone.ps1 | 353 +++++++++--------- 1 file changed, 179 insertions(+), 174 deletions(-) diff --git a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 index a672833..f86336c 100644 --- a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 +++ b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 @@ -26,143 +26,143 @@ function Remove-PlatformLandingZone { By default, ALL resource groups in the subscriptions will be deleted unless they match retention patterns. Use with extreme caution and ensure you have appropriate backups and authorization before executing. - .PARAMETER managementGroups + .PARAMETER ManagementGroups An array of management group IDs or names to process. By default, the function deletes child management groups - one level below these target groups (not the target groups themselves). Use -deleteTargetManagementGroups to + one level below these target groups (not the target groups themselves). Use -DeleteTargetManagementGroups to delete the target groups as well. Subscriptions under these management groups will be discovered unless - subscriptions are explicitly provided via the -subscriptions parameter. + subscriptions are explicitly provided via the -Subscriptions parameter. - .PARAMETER deleteTargetManagementGroups - A switch parameter that causes the target management groups specified in -managementGroups to be deleted along + .PARAMETER DeleteTargetManagementGroups + A switch parameter that causes the target management groups specified in -ManagementGroups to be deleted along with all their children. By default, only management groups one level below the targets are deleted, preserving the target management groups themselves. Default: $false (preserve target management groups) - .PARAMETER subscriptionsTargetManagementGroup + .PARAMETER SubscriptionsTargetManagementGroup The management group ID or name where subscriptions should be moved after being removed from their current management groups. If not specified, subscriptions are removed from management groups without being reassigned. This is useful for maintaining subscription organization during cleanup operations. Default: $null (subscriptions are not reassigned) - .PARAMETER subscriptions + .PARAMETER Subscriptions An optional array of subscription IDs or names to process for resource group deletion. If provided, the function will only delete resource groups from these specific subscriptions and will not discover additional subscriptions from management groups. If omitted, subscriptions will be discovered from the management groups being processed. Accepts both subscription IDs (GUIDs) and subscription names. Default: Empty array (discover from management groups) - .PARAMETER resourceGroupsToRetainNamePatterns + .PARAMETER ResourceGroupsToRetainNamePatterns An array of regex patterns for resource group names that should be retained (not deleted). Resource groups matching any of these patterns will be skipped during the deletion process. This is useful for preserving critical infrastructure or billing-related resource groups. Default: @("VisualStudioOnline-") - Retains Azure DevOps billing resource groups - .PARAMETER bypassConfirmation + .PARAMETER BypassConfirmation A switch parameter that bypasses the interactive confirmation prompts. When specified, the function waits - for the duration specified in -bypassConfirmationTimeoutSeconds before proceeding, allowing time to cancel. + for the duration specified in -BypassConfirmationTimeoutSeconds before proceeding, allowing time to cancel. During this timeout, pressing any key will cancel the operation. WARNING: Use this parameter with extreme caution as it reduces safety checks. Default: $false (confirmation required) - .PARAMETER bypassConfirmationTimeoutSeconds - The number of seconds to wait before proceeding when -bypassConfirmation is used. During this timeout, + .PARAMETER BypassConfirmationTimeoutSeconds + The number of seconds to wait before proceeding when -BypassConfirmation is used. During this timeout, pressing any key will cancel the operation. This provides a safety window to prevent accidental deletions. Default: 30 seconds - .PARAMETER throttleLimit + .PARAMETER ThrottleLimit The maximum number of parallel operations to execute simultaneously. This controls the degree of parallelism when processing management groups and resource groups. Higher values may improve performance but increase API throttling risk and resource consumption. Default: 11 "These go to eleven." - .PARAMETER planMode + .PARAMETER PlanMode A switch parameter that enables "dry run" mode. When specified, the function displays what actions would be taken without actually making any changes. This is useful for validating the scope of operations before executing the actual cleanup. Default: $false (execute actual deletions) - .PARAMETER skipDefenderPlanReset + .PARAMETER SkipDefenderPlanReset A switch parameter that skips the Microsoft Defender for Cloud plan reset operation. When specified, the function will not attempt to reset Defender plans to Free tier. This is useful when you want to preserve existing Defender configurations or when you don't have the necessary permissions. Default: $false (reset Defender plans) - .PARAMETER skipDeploymentDeletion + .PARAMETER SkipDeploymentDeletion A switch parameter that skips deployment deletion operations at both the management group and subscription levels. When specified, the function will not delete deployment history records from management groups or subscriptions. This is useful when you want to preserve deployment records for audit or compliance purposes. Default: $false (delete deployments) - .PARAMETER skipOrphanedRoleAssignmentDeletion + .PARAMETER SkipOrphanedRoleAssignmentDeletion A switch parameter that skips orphaned role assignment deletion operations at both the management group and subscription levels. When specified, the function will not delete role assignments where the principal no longer exists. This is useful when you want to preserve role assignment records or lack the necessary permissions. Default: $false (delete orphaned role assignments) .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-platform", "alz-landingzones") + Remove-PlatformLandingZone -ManagementGroups @("alz-platform", "alz-landingzones") Removes all child management groups one level below "alz-platform" and "alz-landingzones", discovers subscriptions from those management groups, prompts for confirmation, then deletes all resource groups in the discovered subscriptions (except those matching retention patterns). .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-test") -deleteTargetManagementGroups + Remove-PlatformLandingZone -ManagementGroups @("alz-test") -DeleteTargetManagementGroups Deletes the "alz-test" management group itself along with all its children, rather than just deleting one level below it. .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("mg-dev") -subscriptions @("Sub-Dev-001", "Sub-Dev-002") + Remove-PlatformLandingZone -ManagementGroups @("mg-dev") -Subscriptions @("Sub-Dev-001", "Sub-Dev-002") Processes the "mg-dev" management group hierarchy and deletes resource groups only from the two explicitly specified subscriptions. No additional subscriptions will be discovered from the management group. .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-test") -subscriptionsTargetManagementGroup "mg-tenant-root" + Remove-PlatformLandingZone -ManagementGroups @("alz-test") -SubscriptionsTargetManagementGroup "mg-tenant-root" Removes child management groups and moves all discovered subscriptions to the "mg-tenant-root" management group instead of leaving them orphaned. .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-dev") -planMode + Remove-PlatformLandingZone -ManagementGroups @("alz-dev") -PlanMode Runs in plan mode (dry run) to show what would be deleted without making any actual changes. Useful for validating the scope before executing. .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-test") -bypassConfirmation -bypassConfirmationTimeoutSeconds 60 + Remove-PlatformLandingZone -ManagementGroups @("alz-test") -BypassConfirmation -BypassConfirmationTimeoutSeconds 60 Bypasses interactive confirmation prompts but waits 60 seconds before proceeding, allowing time to cancel by pressing any key. USE WITH EXTREME CAUTION! .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-prod") -resourceGroupsToRetainNamePatterns @("VisualStudioOnline-", "RG-Critical-", "NetworkWatcherRG") + Remove-PlatformLandingZone -ManagementGroups @("alz-prod") -ResourceGroupsToRetainNamePatterns @("VisualStudioOnline-", "RG-Critical-", "NetworkWatcherRG") Removes management group hierarchy but retains resource groups matching any of the specified patterns. This example preserves Azure DevOps billing resources, critical resource groups, and Network Watcher resource groups. .EXAMPLE $subs = @("12345678-1234-1234-1234-123456789012", "87654321-4321-4321-4321-210987654321") - Remove-PlatformLandingZone -managementGroups @("alz-test") -subscriptions $subs -throttleLimit 5 + Remove-PlatformLandingZone -ManagementGroups @("alz-test") -Subscriptions $subs -ThrottleLimit 5 Processes the management group hierarchy and only the specified subscriptions (by GUID) with reduced parallelism to minimize API throttling. .EXAMPLE - Remove-PlatformLandingZone -subscriptions @("Sub-Test-001") + Remove-PlatformLandingZone -Subscriptions @("Sub-Test-001") Skips management group processing entirely and only deletes resource groups from the specified subscription. This is useful when you want to clean subscriptions without touching the management group structure. .EXAMPLE - Remove-PlatformLandingZone -managementGroups @("alz-test") -skipDefenderPlanReset -skipDeploymentDeletion + Remove-PlatformLandingZone -ManagementGroups @("alz-test") -SkipDefenderPlanReset -SkipDeploymentDeletion Removes management groups and resource groups but skips resetting Microsoft Defender plans and deleting deployment history. Useful for faster cleanup when Defender configuration and audit trails should be preserved. .EXAMPLE - Remove-PlatformLandingZone -subscriptions @("Sub-Test-001") -skipOrphanedRoleAssignmentDeletion + Remove-PlatformLandingZone -Subscriptions @("Sub-Test-001") -SkipOrphanedRoleAssignmentDeletion Cleans up the subscription but skips orphaned role assignment deletion. Useful when you want to preserve role assignments for review or lack the necessary permissions to delete them. @@ -213,20 +213,20 @@ function Remove-PlatformLandingZone { #> [CmdletBinding(SupportsShouldProcess = $true)] param ( - [string[]]$managementGroups, - [switch]$deleteTargetManagementGroups, - [string]$subscriptionsTargetManagementGroup = $null, - [string[]]$subscriptions = @(), - [string[]]$resourceGroupsToRetainNamePatterns = @( + [string[]]$ManagementGroups, + [switch]$DeleteTargetManagementGroups, + [string]$SubscriptionsTargetManagementGroup = $null, + [string[]]$Subscriptions = @(), + [string[]]$ResourceGroupsToRetainNamePatterns = @( "VisualStudioOnline-" # By default retain Visual Studio Online resource groups created for Azure DevOps billing purposes ), - [switch]$bypassConfirmation, - [int]$bypassConfirmationTimeoutSeconds = 30, - [int]$throttleLimit = 11, - [switch]$planMode, - [switch]$skipDefenderPlanReset, - [switch]$skipDeploymentDeletion, - [switch]$skipOrphanedRoleAssignmentDeletion + [switch]$BypassConfirmation, + [int]$BypassConfirmationTimeoutSeconds = 30, + [int]$ThrottleLimit = 11, + [switch]$PlanMode, + [switch]$SkipDefenderPlanReset, + [switch]$SkipDeploymentDeletion, + [switch]$SkipOrphanedRoleAssignmentDeletion ) function Write-ToConsoleLog { @@ -276,35 +276,35 @@ function Remove-PlatformLandingZone { function Get-ManagementGroupChildrenRecursive { param ( - [object[]]$managementGroups, - [int]$depth = 0, - [hashtable]$managementGroupsFound = @{} + [object[]]$ManagementGroups, + [int]$Depth = 0, + [hashtable]$ManagementGroupsFound = @{} ) - $managementGroups = $managementGroups | Where-Object { $_.type -eq "Microsoft.Management/managementGroups" } + $ManagementGroups = $ManagementGroups | Where-Object { $_.type -eq "Microsoft.Management/managementGroups" } - foreach($managementGroup in $managementGroups) { - if(!$managementGroupsFound.ContainsKey($depth)) { - $managementGroupsFound[$depth] = @() + foreach($managementGroup in $ManagementGroups) { + if(!$ManagementGroupsFound.ContainsKey($Depth)) { + $ManagementGroupsFound[$Depth] = @() } - $managementGroupsFound[$depth] += $managementGroup.name + $ManagementGroupsFound[$Depth] += $managementGroup.name $children = $managementGroup.children | Where-Object { $_.type -eq "Microsoft.Management/managementGroups" } if ($children -and $children.Count -gt 0) { Write-ToConsoleLog "Management group has children: $($managementGroup.name)" -NoNewLine - if(!$managementGroupsFound.ContainsKey($depth + 1)) { - $managementGroupsFound[$depth + 1] = @() + if(!$ManagementGroupsFound.ContainsKey($Depth + 1)) { + $ManagementGroupsFound[$Depth + 1] = @() } - Get-ManagementGroupChildrenRecursive -managementGroups $children -depth ($depth + 1) -managementGroupsFound $managementGroupsFound + Get-ManagementGroupChildrenRecursive -ManagementGroups $children -Depth ($Depth + 1) -ManagementGroupsFound $ManagementGroupsFound } else { Write-ToConsoleLog "Management group has no children: $($managementGroup.name)" -NoNewLine } } - if($depth -eq 0) { - return $managementGroupsFound + if($Depth -eq 0) { + return $ManagementGroupsFound } } @@ -321,23 +321,23 @@ function Remove-PlatformLandingZone { function Invoke-PromptForConfirmation { param ( - [string]$message, - [string]$initialConfirmationText, - [string]$finalConfirmationText = "YES I CONFIRM" + [string]$Message, + [string]$InitialConfirmationText, + [string]$FinalConfirmationText = "YES I CONFIRM" ) - Write-ToConsoleLog "$message" -IsWarning - Write-ToConsoleLog "If you wish to proceed, type '$initialConfirmationText' to confirm." -IsWarning + Write-ToConsoleLog "$Message" -IsWarning + Write-ToConsoleLog "If you wish to proceed, type '$InitialConfirmationText' to confirm." -IsWarning $confirmation = Read-Host "Enter the confirmation text" - if ($confirmation -ne $initialConfirmationText) { + if ($confirmation -ne $InitialConfirmationText) { Write-ToConsoleLog "Confirmation not received. Exiting without making any changes." -IsError return $false } Write-ToConsoleLog "Initial confirmation received." -IsSuccess Write-ToConsoleLog "WARNING: This operation is permanent cannot be reversed!" -IsWarning - Write-ToConsoleLog "Are you sure you want to proceed? Type '$finalConfirmationText' to perform the highly destructive operation..." -IsWarning + Write-ToConsoleLog "Are you sure you want to proceed? Type '$FinalConfirmationText' to perform the highly destructive operation..." -IsWarning $confirmation = Read-Host "Enter the final confirmation text" - if ($confirmation -ne $finalConfirmationText) { + if ($confirmation -ne $FinalConfirmationText) { Write-ToConsoleLog "Final confirmation not received. Exiting without making any changes." -IsError return $false } @@ -345,21 +345,73 @@ function Remove-PlatformLandingZone { return $true } + function Remove-OrphanedRoleAssignmentsForScope { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [string]$ScopeType, + [string]$ScopeNameForLogs, + [string]$ScopeId, + [int]$ThrottleLimit, + [switch]$PlanMode + ) + + if(-not $PSCmdlet.ShouldProcess("Delete Orphaned Role Assignments", "delete")) { + return + } + + $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString() + $isSubscriptionScope = $ScopeType -eq "subscription" + Write-ToConsoleLog "Checking for orphaned role assignments to delete in $($ScopeType): $ScopeNameForLogs" -NoNewLine + $scopePrefix = $isSubscriptionScope ? "/subscriptions" : "/providers/Microsoft.Management/managementGroups" + $roleAssignments = (az role assignment list --scope "$scopePrefix/$ScopeId" --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json + + if ($roleAssignments -and $roleAssignments.Count -gt 0) { + Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in $($ScopeType): $ScopeNameForLogs" -NoNewLine + + $roleAssignments | ForEach-Object -Parallel { + $roleAssignment = $_ + $ScopeType = $using:ScopeType + $ScopeNameForLogs = $using:ScopeNameForLogs + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from $($ScopeType): $ScopeNameForLogs" -NoNewLine + $result = $null + if($using:PlanMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray + } else { + $result = az role assignment delete --ids $roleAssignment.id 2>&1 + } + + if (!$result) { + Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from $($ScopeType): $ScopeNameForLogs" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from $($ScopeType): $ScopeNameForLogs" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:ThrottleLimit + + Write-ToConsoleLog "All orphaned role assignments processed in $($ScopeType): $ScopeNameForLogs" -NoNewLine + } else { + Write-ToConsoleLog "No orphaned role assignments found in $($ScopeType): $ScopeNameForLogs, skipping." -NoNewLine + } + } + if ($PSCmdlet.ShouldProcess("Delete Management Groups and Clean Subscriptions", "delete")) { $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString() + $funcRemoveOrphanedRoleAssignmentsForScope = ${function:Remove-OrphanedRoleAssignmentsForScope}.ToString() - if($bypassConfirmation) { + if($BypassConfirmation) { Write-ToConsoleLog "Bypass confirmation enabled, proceeding without prompts..." -IsWarning Write-ToConsoleLog "This is a highly destructive operation that will permanently delete Azure resources!" -IsWarning - Write-ToConsoleLog "We are waiting $bypassConfirmationTimeoutSeconds seconds to allow for cancellation. Press any key to cancel..." -IsWarning + Write-ToConsoleLog "We are waiting $BypassConfirmationTimeoutSeconds seconds to allow for cancellation. Press any key to cancel..." -IsWarning $keyPressed = $false $secondsRunning = 0 - while((-not $keyPressed) -and ($secondsRunning -lt $bypassConfirmationTimeoutSeconds)){ + while((-not $keyPressed) -and ($secondsRunning -lt $BypassConfirmationTimeoutSeconds)){ $keyPressed = [Console]::KeyAvailable - Write-ToConsoleLog ("Waiting for: $($bypassConfirmationTimeoutSeconds-$secondsRunning) seconds. Press any key to cancel...") -IsWarning -Overwrite + Write-ToConsoleLog ("Waiting for: $($BypassConfirmationTimeoutSeconds-$secondsRunning) seconds. Press any key to cancel...") -IsWarning -Overwrite Start-Sleep -Seconds 1 $secondsRunning++ } @@ -372,8 +424,8 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Thanks for providing the inputs, getting started..." -IsSuccess - $managementGroupsProvided = $managementGroups.Count -gt 0 - $subscriptionsProvided = $subscriptions.Count -gt 0 + $managementGroupsProvided = $ManagementGroups.Count -gt 0 + $subscriptionsProvided = $Subscriptions.Count -gt 0 if(-not $subscriptionsProvided -and -not $managementGroupsProvided) { Write-ToConsoleLog "No management groups or subscriptions provided, nothing to do. Exiting..." -IsError @@ -389,12 +441,12 @@ function Remove-PlatformLandingZone { if($managementGroupsProvided) { $managementGroupsFound = @() - if($subscriptionsTargetManagementGroup) { - Write-ToConsoleLog "Validating target management group for subscriptions: $subscriptionsTargetManagementGroup" + if($SubscriptionsTargetManagementGroup) { + Write-ToConsoleLog "Validating target management group for subscriptions: $SubscriptionsTargetManagementGroup" - $managementGroupObject = (az account management-group show --name $subscriptionsTargetManagementGroup) | ConvertFrom-Json + $managementGroupObject = (az account management-group show --name $SubscriptionsTargetManagementGroup) | ConvertFrom-Json if($null -eq $managementGroupObject) { - Write-ToConsoleLog "Target management group for subscriptions not found: $subscriptionsTargetManagementGroup" -IsError + Write-ToConsoleLog "Target management group for subscriptions not found: $SubscriptionsTargetManagementGroup" -IsError return } @@ -402,7 +454,7 @@ function Remove-PlatformLandingZone { } Write-ToConsoleLog "Validating provided management groups..." - foreach($managementGroup in $managementGroups) { + foreach($managementGroup in $ManagementGroups) { $managementGroupObject = (az account management-group show --name $managementGroup) | ConvertFrom-Json if($null -eq $managementGroupObject) { @@ -421,12 +473,12 @@ function Remove-PlatformLandingZone { return } - if(-not $bypassConfirmation) { + if(-not $BypassConfirmation) { Write-ToConsoleLog "The following Management Groups will be processed for removal:" $managementGroupsFound | ForEach-Object { Write-ToConsoleLog "Management Group: $($_.Name) ($($_.DisplayName))" -NoNewLine } $warningMessage = "ALL THE MANAGEMENT GROUP STRUCTURES ONE LEVEL BELOW THE LISTED MANAGEMENT GROUPS WILL BE PERMANENTLY DELETED" $confirmationText = "I CONFIRM I UNDERSTAND ALL THE MANAGEMENT GROUP STRUCTURES ONE LEVEL BELOW THE LISTED MANAGEMENT GROUPS WILL BE PERMANENTLY DELETED" - if($deleteTargetManagementGroups) { + if($DeleteTargetManagementGroups) { $warningMessage = "ALL THE LISTED MANAGEMENTS GROUPS AND THEIR CHILDREN WILL BE PERMANENTLY DELETED" $confirmationText = "I CONFIRM I UNDERSTAND ALL THE MANAGEMENT GROUPS AND THEIR CHILDREN WILL BE PERMANENTLY DELETED" } @@ -449,8 +501,8 @@ function Remove-PlatformLandingZone { $managementGroupsFound | ForEach-Object -Parallel { $subscriptionsProvided = $using:subscriptionsProvided $subscriptionsFound = $using:subscriptionsFound - $subscriptionsTargetManagementGroup = $using:subscriptionsTargetManagementGroup - $deleteTargetManagementGroups = $using:deleteTargetManagementGroups + $subscriptionsTargetManagementGroup = $using:SubscriptionsTargetManagementGroup + $deleteTargetManagementGroups = $using:DeleteTargetManagementGroups $funcWriteToConsoleLog = $using:funcWriteToConsoleLog ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog @@ -468,15 +520,15 @@ function Remove-PlatformLandingZone { if($hasChildren -or $deleteTargetManagementGroups) { ${function:Get-ManagementGroupChildrenRecursive} = $using:funcGetManagementGroupChildrenRecursive - $managementGroupsToDelete = Get-ManagementGroupChildrenRecursive -managementGroups @($targetManagementGroups) + $managementGroupsToDelete = Get-ManagementGroupChildrenRecursive -ManagementGroups @($targetManagementGroups) } else { Write-ToConsoleLog "Management group has no children: $managementGroupId ($managementGroupDisplayName)" -NoNewLine } $reverseKeys = $managementGroupsToDelete.Keys | Sort-Object -Descending - $throttleLimit = $using:throttleLimit - $planMode = $using:planMode + $throttleLimit = $using:ThrottleLimit + $planMode = $using:PlanMode foreach($depth in $reverseKeys) { $managementGroups = $managementGroupsToDelete[$depth] @@ -485,7 +537,7 @@ function Remove-PlatformLandingZone { $managementGroups | ForEach-Object -Parallel { $subscriptionsFound = $using:subscriptionsFound - $subscriptionsTargetManagementGroup = $using:subscriptionsTargetManagementGroup + $subscriptionsTargetManagementGroup = $using:SubscriptionsTargetManagementGroup $funcWriteToConsoleLog = $using:funcWriteToConsoleLog ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog @@ -505,13 +557,13 @@ function Remove-PlatformLandingZone { if($subscriptionsTargetManagementGroup) { Write-ToConsoleLog "Moving subscription to target management group: $($subscriptionsTargetManagementGroup), subscription: $($subscription.displayName)" -NoNewLine - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az account management-group subscription add --name $($subscriptionsTargetManagementGroup) --subscription $($subscription.name)" -NoNewLine -Color Gray } else { az account management-group subscription add --name $subscriptionsTargetManagementGroup --subscription $subscription.name | Out-Null } } else { - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az account management-group subscription remove --name $_ --subscription $($subscription.name)" -NoNewLine -Color Gray } else { az account management-group subscription remove --name $_ --subscription $subscription.name | Out-Null @@ -523,22 +575,22 @@ function Remove-PlatformLandingZone { } Write-ToConsoleLog "Deleting management group: $_" -NoNewline - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az account management-group delete --name $_" -NoNewline -Color Gray } else { az account management-group delete --name $_ | Out-Null } - } -ThrottleLimit $using:throttleLimit + } -ThrottleLimit $using:ThrottleLimit } - } -ThrottleLimit $throttleLimit + } -ThrottleLimit $ThrottleLimit } # Delete deployments from target management groups that are not being deleted - if($managementGroupsFound.Count -ne 0 -and -not $skipDeploymentDeletion) { + if($managementGroupsFound.Count -ne 0 -and -not $SkipDeploymentDeletion) { $managementGroupsFound | ForEach-Object -Parallel { $managementGroupId = $_.Name $managementGroupDisplayName = $_.DisplayName - $deleteTargetManagementGroups = $using:deleteTargetManagementGroups + $deleteTargetManagementGroups = $using:DeleteTargetManagementGroups $funcWriteToConsoleLog = $using:funcWriteToConsoleLog ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog @@ -559,7 +611,7 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Deleting deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine $result = $null - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az deployment mg delete --management-group-id $managementGroupId --name $deploymentName" -NoNewLine -Color Gray } else { $result = az deployment mg delete --management-group-id $managementGroupId --name $deploymentName 2>&1 @@ -570,7 +622,7 @@ function Remove-PlatformLandingZone { } else { Write-ToConsoleLog "Failed to delete deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -IsWarning -NoNewLine } - } -ThrottleLimit $using:throttleLimit + } -ThrottleLimit $using:ThrottleLimit Write-ToConsoleLog "All deployments processed in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine } else { @@ -579,56 +631,32 @@ function Remove-PlatformLandingZone { } else { Write-ToConsoleLog "Skipping deployment deletion for management group: $managementGroupId ($managementGroupDisplayName) as it is being deleted" -NoNewLine } - } -ThrottleLimit $throttleLimit + } -ThrottleLimit $ThrottleLimit } # Delete orphaned role assignments from target management groups that are not being deleted - if($managementGroupsFound.Count -ne 0 -and -not $skipOrphanedRoleAssignmentDeletion) { + if($managementGroupsFound.Count -ne 0 -and -not $SkipOrphanedRoleAssignmentDeletion) { $managementGroupsFound | ForEach-Object -Parallel { $managementGroupId = $_.Name $managementGroupDisplayName = $_.DisplayName - $deleteTargetManagementGroups = $using:deleteTargetManagementGroups + $deleteTargetManagementGroups = $using:DeleteTargetManagementGroups $funcWriteToConsoleLog = $using:funcWriteToConsoleLog ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + $funcRemoveOrphanedRoleAssignmentsForScope = $using:funcRemoveOrphanedRoleAssignmentsForScope + ${function:Remove-OrphanedRoleAssignmentsForScope} = $funcRemoveOrphanedRoleAssignmentsForScope # Only delete role assignments if this management group is not being deleted if(-not $deleteTargetManagementGroups) { - Write-ToConsoleLog "Checking for orphaned role assignments to delete in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - $roleAssignments = (az role assignment list --scope "/providers/Microsoft.Management/managementGroups/$managementGroupId" --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json - - if ($roleAssignments -and $roleAssignments.Count -gt 0) { - Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - - $roleAssignments | ForEach-Object -Parallel { - $roleAssignment = $_ - $managementGroupId = $using:managementGroupId - $managementGroupDisplayName = $using:managementGroupDisplayName - $funcWriteToConsoleLog = $using:funcWriteToConsoleLog - ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog - - Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - $result = $null - if($using:planMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray - } else { - $result = az role assignment delete --ids $roleAssignment.id 2>&1 - } - - if (!$result) { - Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - } else { - Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from management group: $managementGroupId ($managementGroupDisplayName)" -IsWarning -NoNewLine - } - } -ThrottleLimit $using:throttleLimit - - Write-ToConsoleLog "All orphaned role assignments processed in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - } else { - Write-ToConsoleLog "No orphaned role assignments found in management group: $managementGroupId ($managementGroupDisplayName), skipping." -NoNewLine - } + Remove-OrphanedRoleAssignmentsForScope ` + -ScopeType "management group" ` + -ScopeNameForLogs "$managementGroupId ($managementGroupDisplayName)" ` + -ScopeId $managementGroupId ` + -ThrottleLimit $using:ThrottleLimit ` + -PlanMode:$using:PlanMode } else { Write-ToConsoleLog "Skipping orphaned role assignment deletion for management group: $managementGroupId ($managementGroupDisplayName) as it is being deleted" -NoNewLine } - } -ThrottleLimit $throttleLimit + } -ThrottleLimit $ThrottleLimit } elseif($managementGroupsFound.Count -ne 0) { Write-ToConsoleLog "Skipping orphaned role assignment deletion for all management groups as requested" -NoNewLine } @@ -637,7 +665,7 @@ function Remove-PlatformLandingZone { if($subscriptionsProvided) { Write-ToConsoleLog "Checking the provided subscriptions exist..." - foreach($subscription in $subscriptions) { + foreach($subscription in $Subscriptions) { $subscriptionObject = @{ Id = (Test-IsGuid -StringGuid $subscription) ? $subscription : (az account list --all --query "[?name=='$subscription'].id" -o tsv) Name = (Test-IsGuid -StringGuid $subscription) ? (az account list --all --query "[?id=='$subscription'].name" -o tsv) : $subscription @@ -656,12 +684,12 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "No subscriptions provided or found, skipping resource group deletion..." -IsWarning return } else { - if(-not $bypassConfirmation) { + if(-not $BypassConfirmation) { Write-ToConsoleLog "The following Subscriptions were provided or discovered during management group cleanup:" $subscriptionsFinal | ForEach-Object { Write-ToConsoleLog "Name: $($_.Name), ID: $($_.Id)" -NoNewline } $continue = Invoke-PromptForConfirmation ` - -message "ALL RESOURCE GROUPS IN THE LISTED SUBSCRIPTIONS WILL BE PERMANENTLY DELETED UNLESS THEY MATCH RETENTION PATTERNS" ` - -initialConfirmationText "I CONFIRM I UNDERSTAND ALL SELECTED RESOURCE GROUPS IN THE NAMED SUBSCRIPTIONS WILL BE PERMANENTLY DELETED" + -Message "ALL RESOURCE GROUPS IN THE LISTED SUBSCRIPTIONS WILL BE PERMANENTLY DELETED UNLESS THEY MATCH RETENTION PATTERNS" ` + -InitialConfirmationText "I CONFIRM I UNDERSTAND ALL SELECTED RESOURCE GROUPS IN THE NAMED SUBSCRIPTIONS WILL BE PERMANENTLY DELETED" if(-not $continue) { Write-ToConsoleLog "Exiting..." return @@ -672,6 +700,8 @@ function Remove-PlatformLandingZone { $subscriptionsFinal | ForEach-Object -Parallel { $funcWriteToConsoleLog = $using:funcWriteToConsoleLog ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + $funcRemoveOrphanedRoleAssignmentsForScope = $using:funcRemoveOrphanedRoleAssignmentsForScope + ${function:Remove-OrphanedRoleAssignmentsForScope} = $funcRemoveOrphanedRoleAssignmentsForScope $subscription = $_ Write-ToConsoleLog "Finding resource groups for subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewline @@ -686,7 +716,7 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Found resource groups for subscription: $($subscription.Name) (ID: $($subscription.Id)), count: $($resourceGroups.Count)" -NoNewline $resourceGroupsToDelete = @() - $resourceGroupsToRetainNamePatterns = $using:resourceGroupsToRetainNamePatterns + $resourceGroupsToRetainNamePatterns = $using:ResourceGroupsToRetainNamePatterns foreach ($resourceGroup in $resourceGroups) { $foundMatch = $false @@ -711,8 +741,8 @@ function Remove-PlatformLandingZone { $shouldRetry = $true - $throttleLimit = $using:throttleLimit - $planMode = $using:planMode + $throttleLimit = $using:ThrottleLimit + $planMode = $using:PlanMode while($shouldRetry) { $shouldRetry = $false @@ -725,7 +755,7 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Deleting resource group for subscription: $($subscription.Name) (ID: $($subscription.Id)), resource group: $($ResourceGroupName)" -NoNewLine $result = $null - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az group delete --name $ResourceGroupName --subscription $($subscription.Id) --yes" -NoNewLine -Color Gray } else { $result = az group delete --name $ResourceGroupName --subscription $subscription.Id --yes 2>&1 @@ -739,7 +769,7 @@ function Remove-PlatformLandingZone { $retries = $using:resourceGroupsToRetry $retries.Add($_) } - } -ThrottleLimit $using:throttleLimit + } -ThrottleLimit $using:ThrottleLimit if($resourceGroupsToRetry.Count -gt 0) { Write-ToConsoleLog "Some resource groups failed to delete and will be retried in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine @@ -750,7 +780,7 @@ function Remove-PlatformLandingZone { } } - if(-not $using:skipDefenderPlanReset) { + if(-not $using:SkipDefenderPlanReset) { Write-ToConsoleLog "Checking for Microsoft Defender for Cloud Plans to reset in subscription: $($subscription.Name) (ID: $($subscription.Id))" $defenderPlans = (az security pricing list --subscription $subscription.Id) | ConvertFrom-Json @@ -762,14 +792,14 @@ function Remove-PlatformLandingZone { if ($_.pricingTier -ne "Free") { Write-ToConsoleLog "Resetting Microsoft Defender for Cloud Plan to Free for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine $result = $null - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az security pricing create --name $($_.name) --tier `"Free`" --subscription $($subscription.Id)" -NoNewLine -Color Gray } else { $result = (az security pricing create --name $_.name --tier "Free" --subscription $subscription.Id 2>&1) } if ($result -like "*must be 'Standard'*") { Write-ToConsoleLog "Resetting Microsoft Defender for Cloud Plan to Standard as Free is not supported for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az security pricing create --name $($_.name) --tier `"Standard`" --subscription $($subscription.Id)" -NoNewLine -Color Gray } else { $result = az security pricing create --name $_.name --tier "Standard" --subscription $subscription.Id @@ -779,12 +809,12 @@ function Remove-PlatformLandingZone { } else { Write-ToConsoleLog "Microsoft Defender for Cloud Plan is already set to Free for plan: $($_.name) in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine } - } -ThrottleLimit $using:throttleLimit + } -ThrottleLimit $using:ThrottleLimit } else { Write-ToConsoleLog "Skipping Microsoft Defender for Cloud Plans reset in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } - if(-not $using:skipDeploymentDeletion) { + if(-not $using:SkipDeploymentDeletion) { Write-ToConsoleLog "Checking for subscription level deployments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" $deployments = (az deployment sub list --subscription $subscription.Id --query "[].name" -o json) | ConvertFrom-Json @@ -799,7 +829,7 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Deleting deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine $result = $null - if($using:planMode) { + if($using:PlanMode) { Write-ToConsoleLog "(Plan Mode) Would run: az deployment sub delete --name $deploymentName --subscription $($subscription.Id)" -NoNewLine -Color Gray } else { $result = az deployment sub delete --name $deploymentName --subscription $subscription.Id 2>&1 @@ -810,7 +840,7 @@ function Remove-PlatformLandingZone { } else { Write-ToConsoleLog "Failed to delete deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine } - } -ThrottleLimit $using:throttleLimit + } -ThrottleLimit $using:ThrottleLimit Write-ToConsoleLog "All deployments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } else { @@ -820,43 +850,18 @@ function Remove-PlatformLandingZone { Write-ToConsoleLog "Skipping subscription level deployment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } - if(-not $using:skipOrphanedRoleAssignmentDeletion) { - Write-ToConsoleLog "Checking for orphaned role assignments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $roleAssignments = (az role assignment list --subscription $subscription.Id --query "[?principalName==''].{id:id,principalId:principalId,roleDefinitionName:roleDefinitionName}" -o json) | ConvertFrom-Json - - if ($roleAssignments -and $roleAssignments.Count -gt 0) { - Write-ToConsoleLog "Found $($roleAssignments.Count) orphaned role assignment(s) in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - - $roleAssignments | ForEach-Object -Parallel { - $roleAssignment = $_ - $subscription = $using:subscription - $funcWriteToConsoleLog = $using:funcWriteToConsoleLog - ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog - - Write-ToConsoleLog "Deleting orphaned role assignment: $($roleAssignment.roleDefinitionName) for principal: $($roleAssignment.principalId) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $result = $null - if($using:planMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az role assignment delete --ids $($roleAssignment.id)" -NoNewLine -Color Gray - } else { - $result = az role assignment delete --ids $roleAssignment.id 2>&1 - } - - if (!$result) { - Write-ToConsoleLog "Deleted orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - } else { - Write-ToConsoleLog "Failed to delete orphaned role assignment: $($roleAssignment.roleDefinitionName) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine - } - } -ThrottleLimit $using:throttleLimit - - Write-ToConsoleLog "All orphaned role assignments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - } else { - Write-ToConsoleLog "No orphaned role assignments found in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine - } + if(-not $using:SkipOrphanedRoleAssignmentDeletion) { + Remove-OrphanedRoleAssignmentsForScope ` + -ScopeType "subscription" ` + -ScopeNameForLogs "$($subscription.Name) (ID: $($subscription.Id))" ` + -ScopeId $subscription.Id ` + -ThrottleLimit $using:ThrottleLimit ` + -PlanMode:$using:PlanMode } else { Write-ToConsoleLog "Skipping orphaned role assignment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine } - } -ThrottleLimit $throttleLimit + } -ThrottleLimit $ThrottleLimit Write-ToConsoleLog "Cleanup completed." -IsSuccess } From bccd8303883cf91e77cfcd6c08d67a0d77e65799 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Sat, 15 Nov 2025 16:56:14 +0000 Subject: [PATCH 4/4] refactor deployments into function --- src/ALZ/Public/Remove-PlatformLandingZone.ps1 | 210 ++++++++++++------ 1 file changed, 147 insertions(+), 63 deletions(-) diff --git a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 index f86336c..eb16b3d 100644 --- a/src/ALZ/Public/Remove-PlatformLandingZone.ps1 +++ b/src/ALZ/Public/Remove-PlatformLandingZone.ps1 @@ -274,6 +274,68 @@ function Remove-PlatformLandingZone { Write-Host "$prefix[$timestamp] [$Level] $Message" -ForegroundColor $Color -NoNewline:$Overwrite.IsPresent } + function Test-RequiredTooling { + Write-ToConsoleLog "Checking the software requirements for the Accelerator..." + + $checkResults = @() + $hasFailure = $false + + # Check if Azure CLI is installed + Write-Verbose "Checking Azure CLI installation" + $azCliPath = Get-Command az -ErrorAction SilentlyContinue + if ($azCliPath) { + $checkResults += @{ + message = "Azure CLI is installed." + result = "Success" + } + } else { + $checkResults += @{ + message = "Azure CLI is not installed. Follow the instructions here: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli" + result = "Failure" + } + $hasFailure = $true + } + + # Check if Azure CLI is logged in + Write-Verbose "Checking Azure CLI login status" + $azCliAccount = $(az account show -o json) | ConvertFrom-Json + if ($azCliAccount) { + $checkResults += @{ + message = "Azure CLI is logged in. Tenant ID: $($azCliAccount.tenantId), Subscription: $($azCliAccount.name) ($($azCliAccount.id))" + result = "Success" + } + } else { + $checkResults += @{ + message = "Azure CLI is not logged in. Please login to Azure CLI using 'az login -t `"00000000-0000-0000-0000-000000000000}`"', replacing the empty GUID with your tenant ID." + result = "Failure" + } + $hasFailure = $true + } + + Write-Verbose "Showing check results" + Write-Verbose $(ConvertTo-Json $checkResults -Depth 100) + $checkResults | ForEach-Object {[PSCustomObject]$_} | Format-Table -Property @{ + Label = "Check Result"; Expression = { + switch ($_.result) { + 'Success' { $color = "92"; break } + 'Failure' { $color = "91"; break } + 'Warning' { $color = "93"; break } + default { $color = "0" } + } + $e = [char]27 + "$e[${color}m$($_.result)${e}[0m" + } + }, @{ Label = "Check Details"; Expression = {$_.message} } -AutoSize -Wrap + + if($hasFailure) { + Write-ToConsoleLog "Software requirements have no been met, please review and install the missing software." -IsError + Write-ToConsoleLog "Cannot continue with Deployment..." -IsError + throw "Software requirements have no been met, please review and install the missing software." + } + + Write-ToConsoleLog "All software requirements have been met." -IsSuccess + } + function Get-ManagementGroupChildrenRecursive { param ( [object[]]$ManagementGroups, @@ -396,10 +458,79 @@ function Remove-PlatformLandingZone { } } + function Remove-DeploymentsForScope { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [string]$ScopeType, + [string]$ScopeNameForLogs, + [string]$ScopeId, + [int]$ThrottleLimit, + [switch]$PlanMode + ) + + if(-not $PSCmdlet.ShouldProcess("Delete Deployments", "delete")) { + return + } + + $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString() + $isSubscriptionScope = $ScopeType -eq "subscription" + + Write-ToConsoleLog "Checking for deployments to delete in $($ScopeType): $ScopeNameForLogs" -NoNewLine + + $deployments = @() + if ($isSubscriptionScope) { + $deployments = (az deployment sub list --subscription $ScopeId --query "[].name" -o json) | ConvertFrom-Json + } else { + $deployments = (az deployment mg list --management-group-id $ScopeId --query "[].name" -o json) | ConvertFrom-Json + } + + if ($deployments -and $deployments.Count -gt 0) { + Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in $($ScopeType): $scopeNameForLogs" -NoNewLine + + $deployments | ForEach-Object -Parallel { + $deploymentName = $_ + $scopeId = $using:ScopeId + $scopeNameForLogs = $using:ScopeNameForLogs + $funcWriteToConsoleLog = $using:funcWriteToConsoleLog + ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + + Write-ToConsoleLog "Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -NoNewLine + $result = $null + if($isSubscriptionScope) { + if($using:PlanMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az deployment sub delete --subscription $scopeId --name $deploymentName" -NoNewLine -Color Gray + } else { + $result = az deployment sub delete --subscription $scopeId --name $deploymentName 2>&1 + } + } else { + if($using:PlanMode) { + Write-ToConsoleLog "(Plan Mode) Would run: az deployment mg delete --management-group-id $scopeId --name $deploymentName" -NoNewLine -Color Gray + } else { + $result = az deployment mg delete --management-group-id $scopeId --name $deploymentName 2>&1 + } + } + + if (!$result) { + Write-ToConsoleLog "Deleted deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -NoNewLine + } else { + Write-ToConsoleLog "Failed to delete deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -IsWarning -NoNewLine + } + } -ThrottleLimit $using:ThrottleLimit + + Write-ToConsoleLog "All deployments processed in $($scopeType): $scopeNameForLogs" -NoNewLine + } else { + Write-ToConsoleLog "No deployments found in $($scopeType): $scopeNameForLogs, skipping." -NoNewLine + } + } + + # Main execution starts here if ($PSCmdlet.ShouldProcess("Delete Management Groups and Clean Subscriptions", "delete")) { + Test-RequiredTooling + $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString() $funcRemoveOrphanedRoleAssignmentsForScope = ${function:Remove-OrphanedRoleAssignmentsForScope}.ToString() + $funcRemoveDeploymentsForScope = ${function:Remove-DeploymentsForScope}.ToString() if($BypassConfirmation) { Write-ToConsoleLog "Bypass confirmation enabled, proceeding without prompts..." -IsWarning @@ -593,41 +724,17 @@ function Remove-PlatformLandingZone { $deleteTargetManagementGroups = $using:DeleteTargetManagementGroups $funcWriteToConsoleLog = $using:funcWriteToConsoleLog ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog + $funcRemoveDeploymentsForScope = $using:funcRemoveDeploymentsForScope + ${function:Remove-DeploymentsForScope} = $funcRemoveDeploymentsForScope # Only delete deployments if this management group is not being deleted if(-not $deleteTargetManagementGroups) { - Write-ToConsoleLog "Checking for management group level deployments to delete in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - $deployments = (az deployment mg list --management-group-id $managementGroupId --query "[].name" -o json) | ConvertFrom-Json - - if ($deployments -and $deployments.Count -gt 0) { - Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - - $deployments | ForEach-Object -Parallel { - $deploymentName = $_ - $managementGroupId = $using:managementGroupId - $managementGroupDisplayName = $using:managementGroupDisplayName - $funcWriteToConsoleLog = $using:funcWriteToConsoleLog - ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog - - Write-ToConsoleLog "Deleting deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - $result = $null - if($using:PlanMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az deployment mg delete --management-group-id $managementGroupId --name $deploymentName" -NoNewLine -Color Gray - } else { - $result = az deployment mg delete --management-group-id $managementGroupId --name $deploymentName 2>&1 - } - - if (!$result) { - Write-ToConsoleLog "Deleted deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - } else { - Write-ToConsoleLog "Failed to delete deployment: $deploymentName from management group: $managementGroupId ($managementGroupDisplayName)" -IsWarning -NoNewLine - } - } -ThrottleLimit $using:ThrottleLimit - - Write-ToConsoleLog "All deployments processed in management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine - } else { - Write-ToConsoleLog "No deployments found in management group: $managementGroupId ($managementGroupDisplayName), skipping." -NoNewLine - } + Remove-DeploymentsForScope ` + -ScopeType "management group" ` + -ScopeNameForLogs "$managementGroupId ($managementGroupDisplayName)" ` + -ScopeId $managementGroupId ` + -ThrottleLimit $using:ThrottleLimit ` + -PlanMode:$using:PlanMode } else { Write-ToConsoleLog "Skipping deployment deletion for management group: $managementGroupId ($managementGroupDisplayName) as it is being deleted" -NoNewLine } @@ -702,6 +809,8 @@ function Remove-PlatformLandingZone { ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog $funcRemoveOrphanedRoleAssignmentsForScope = $using:funcRemoveOrphanedRoleAssignmentsForScope ${function:Remove-OrphanedRoleAssignmentsForScope} = $funcRemoveOrphanedRoleAssignmentsForScope + $funcRemoveDeploymentsForScope = $using:funcRemoveDeploymentsForScope + ${function:Remove-DeploymentsForScope} = $funcRemoveDeploymentsForScope $subscription = $_ Write-ToConsoleLog "Finding resource groups for subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewline @@ -815,37 +924,12 @@ function Remove-PlatformLandingZone { } if(-not $using:SkipDeploymentDeletion) { - Write-ToConsoleLog "Checking for subscription level deployments to delete in subscription: $($subscription.Name) (ID: $($subscription.Id))" - $deployments = (az deployment sub list --subscription $subscription.Id --query "[].name" -o json) | ConvertFrom-Json - - if ($deployments -and $deployments.Count -gt 0) { - Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in subscription: $($subscription.Name) (ID: $($subscription.Id))" - - $deployments | ForEach-Object -Parallel { - $deploymentName = $_ - $subscription = $using:subscription - $funcWriteToConsoleLog = $using:funcWriteToConsoleLog - ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog - - Write-ToConsoleLog "Deleting deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - $result = $null - if($using:PlanMode) { - Write-ToConsoleLog "(Plan Mode) Would run: az deployment sub delete --name $deploymentName --subscription $($subscription.Id)" -NoNewLine -Color Gray - } else { - $result = az deployment sub delete --name $deploymentName --subscription $subscription.Id 2>&1 - } - - if (!$result) { - Write-ToConsoleLog "Deleted deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - } else { - Write-ToConsoleLog "Failed to delete deployment: $deploymentName in subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine - } - } -ThrottleLimit $using:ThrottleLimit - - Write-ToConsoleLog "All deployments processed in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine - } else { - Write-ToConsoleLog "No deployments found in subscription: $($subscription.Name) (ID: $($subscription.Id)), skipping." -NoNewLine - } + Remove-DeploymentsForScope ` + -ScopeType "subscription" ` + -ScopeNameForLogs "$($subscription.Name) (ID: $($subscription.Id))" ` + -ScopeId $subscription.Id ` + -ThrottleLimit $using:ThrottleLimit ` + -PlanMode:$using:PlanMode } else { Write-ToConsoleLog "Skipping subscription level deployment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine }