From f6b498e54251c224c5bfd2bdb2214515aebbae43 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Wed, 19 Jan 2022 10:05:58 +0100 Subject: [PATCH 1/5] Added managementgroup handling to removal --- .../pipelineTemplates/module.jobs.deploy.yml | 1 + .../validateModuleDeployment/action.yml | 1 + .../Initialize-DeploymentRemoval.ps1 | 21 ++++++--- .../helper/Get-ResourceIdsOfDeployment.ps1 | 44 ++++++++++++++++--- .../helper/Remove-Deployment.ps1 | 19 ++++++-- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/.azuredevops/pipelineTemplates/module.jobs.deploy.yml b/.azuredevops/pipelineTemplates/module.jobs.deploy.yml index 56faf474cd..1aac5ee930 100644 --- a/.azuredevops/pipelineTemplates/module.jobs.deploy.yml +++ b/.azuredevops/pipelineTemplates/module.jobs.deploy.yml @@ -320,6 +320,7 @@ jobs: DeploymentName = '$(deploymentName)' TemplateFilePath = $templateFilePath ResourceGroupName = '${{ parameters.resourceGroupName }}' + ManagementGroupId = '${{ parameters.managementGroupId }}' Verbose = $true } diff --git a/.github/actions/templates/validateModuleDeployment/action.yml b/.github/actions/templates/validateModuleDeployment/action.yml index 8a6ae87cf1..91106d16d5 100644 --- a/.github/actions/templates/validateModuleDeployment/action.yml +++ b/.github/actions/templates/validateModuleDeployment/action.yml @@ -183,6 +183,7 @@ runs: DeploymentName = '${{ steps.deploy_step.outputs.deploymentName }}' TemplateFilePath = Join-Path $env:GITHUB_WORKSPACE '${{ inputs.templateFilePath }}' ResourceGroupName = '${{ inputs.resourceGroupName }}' + ManagementGroupId = '${{ inputs.managementGroupId }}' Verbose = $true } diff --git a/utilities/pipelines/resourceRemoval/Initialize-DeploymentRemoval.ps1 b/utilities/pipelines/resourceRemoval/Initialize-DeploymentRemoval.ps1 index 1c06f2c681..fce2a12284 100644 --- a/utilities/pipelines/resourceRemoval/Initialize-DeploymentRemoval.ps1 +++ b/utilities/pipelines/resourceRemoval/Initialize-DeploymentRemoval.ps1 @@ -14,6 +14,9 @@ Mandatory. The path to the template used for the deployment. Used to determine t .PARAMETER ResourceGroupName Optional. The name of the resource group the deployment was happening in. Relevant for resource-group level deployments. +.PARAMETER ManagementGroupId +Optional. The ID of the management group to fetch deployments from. Relevant for management-group level deployments. + .EXAMPLE Initialize-DeploymentRemoval -DeploymentName 'virtualWans-20211204T1812029146Z' -TemplateFilePath "$home/ResourceModules/arm/Microsoft.Network/virtualWans/deploy.bicep" -resourceGroupName 'test-virtualWan-parameters.json-rg' @@ -31,7 +34,10 @@ function Initialize-DeploymentRemoval { [string] $TemplateFilePath, [Parameter(Mandatory = $false)] - [string] $ResourceGroupName = 'validation-rg' + [string] $ResourceGroupName, + + [Parameter(Mandatory = $false)] + [string] $ManagementGroupId ) begin { @@ -84,10 +90,15 @@ function Initialize-DeploymentRemoval { # Invoke removal $inputObject = @{ - DeploymentName = $deploymentName - ResourceGroupName = $resourceGroupName - TemplateFilePath = $templateFilePath - RemovalSequence = $removalSequence + DeploymentName = $deploymentName + TemplateFilePath = $templateFilePath + RemovalSequence = $removalSequence + } + if (-not [String]::IsNullOrEmpty($resourceGroupName)) { + $inputObject['resourceGroupName'] = $resourceGroupName + } + if (-not [String]::IsNullOrEmpty($ManagementGroupId)) { + $inputObject['ManagementGroupId'] = $ManagementGroupId } Remove-Deployment @inputObject -Verbose } diff --git a/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 b/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 index 4c7a389e9f..30ee721f2e 100644 --- a/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 @@ -12,6 +12,9 @@ Mandatory. The deployment name to search for .PARAMETER ResourceGroupName Optional. The name of the resource group for scope 'resourcegroup' +.PARAMETER ManagementGroupId +Optional. The ID of the management group to fetch deployments from. Relevant for management-group level deployments. + .PARAMETER Scope Mandatory. The scope to search in @@ -20,6 +23,11 @@ Get-ResourceIdsOfDeploymentInner -Name 'keyvault-12356' -Scope 'resourcegroup' Get all deployments that match name 'keyvault-12356' in scope 'resourcegroup' +.EXAMPLE +Get-ResourceIdsOfDeploymentInner -Name 'mgmtGroup-12356' -Scope 'managementGroup' -ManagementGroupId 'af760cf5-3c9e-4804-a59a-a51741daa350' + +Get all deployments that match name 'mgmtGroup-12356' in scope 'managementGroup' + .NOTES Works after the principal: - Find all deployments for the given deployment name @@ -36,6 +44,9 @@ function Get-ResourceIdsOfDeploymentInner { [Parameter(Mandatory = $false)] [string] $ResourceGroupName, + [Parameter(Mandatory = $false)] + [string] $ManagementGroupId, + [Parameter(Mandatory)] [ValidateSet( 'resourcegroup', @@ -86,7 +97,7 @@ function Get-ResourceIdsOfDeploymentInner { } } 'managementgroup' { - [array]$deploymentTargets = (Get-AzManagementGroupDeploymentOperation -DeploymentName $name).TargetResource | Where-Object { $_ -ne $null } + [array]$deploymentTargets = (Get-AzManagementGroupDeploymentOperation -DeploymentName $name -ManagementGroupId $ManagementGroupId).TargetResource | Where-Object { $_ -ne $null } foreach ($deployment in ($deploymentTargets | Where-Object { $_ -notmatch '/deployments/' } )) { Write-Verbose ('Found deployment [{0}]' -f $deployment) -Verbose [array]$resultSet += $deployment @@ -98,7 +109,7 @@ function Get-ResourceIdsOfDeploymentInner { [array]$resultSet += Get-ResourceIdsOfDeploymentInner -Name (Split-Path $deployment -Leaf) -Scope 'subscription' } else { # Management Group Level Deployments - [array]$resultSet += Get-ResourceIdsOfDeploymentInner -name (Split-Path $deployment -Leaf) -scope 'managementgroup' + [array]$resultSet += Get-ResourceIdsOfDeploymentInner -name (Split-Path $deployment -Leaf) -scope 'managementgroup' -ManagementGroupId $ManagementGroupId } } } @@ -112,7 +123,7 @@ function Get-ResourceIdsOfDeploymentInner { [array]$resultSet = $resultSet | Where-Object { $_ -ne $deployment } if ($deployment -match '/tenant/') { # Management Group Level Child Deployments - [array]$resultSet += Get-ResourceIdsOfDeploymentInner -Name (Split-Path $deployment -Leaf) -scope 'managementgroup' + [array]$resultSet += Get-ResourceIdsOfDeploymentInner -Name (Split-Path $deployment -Leaf) -scope 'managementgroup' -ManagementGroupId $ManagementGroupId } else { # Tenant Level Deployments [array]$resultSet += Get-ResourceIdsOfDeploymentInner -name (Split-Path $deployment -Leaf) @@ -132,7 +143,10 @@ Get all deployments that match a given deployment name in a given scope using a Get all deployments that match a given deployment name in a given scope using a retry mechanic. .PARAMETER ResourceGroupName -Mandatory. The resource group of the resource to remove +Optional. The name of the resource group for scope 'resourcegroup' + +.PARAMETER ManagementGroupId +Optional. The ID of the management group to fetch deployments from. Relevant for management-group level deployments. .PARAMETER Name Optional. The deployment name to use for the removal @@ -150,6 +164,12 @@ Optional. The time to wait in between the search for resources via their remove Get-ResourceIdsOfDeployment -name 'KeyVault' -ResourceGroupName 'validation-rg' -scope 'resourcegroup' Get all deployments that match name 'KeyVault' in scope 'resourcegroup' of resource group 'validation-rg' + +.EXAMPLE +Get-ResourceIdsOfDeployment -Name 'mgmtGroup-12356' -Scope 'managementGroup' -ManagementGroupId 'af760cf5-3c9e-4804-a59a-a51741daa350' + +Get all deployments that match name 'mgmtGroup-12356' in scope 'managementGroup' + #> function Get-ResourceIdsOfDeployment { @@ -158,6 +178,9 @@ function Get-ResourceIdsOfDeployment { [Parameter(Mandatory = $false)] [string] $ResourceGroupName, + [Parameter(Mandatory = $false)] + [string] $ManagementGroupId, + [Parameter(Mandatory = $true)] [string] $Name, @@ -179,7 +202,18 @@ function Get-ResourceIdsOfDeployment { $searchRetryCount = 1 do { - [array]$deployments = Get-ResourceIdsOfDeploymentInner -Name $name -Scope $scope -ResourceGroupName $resourceGroupName -ErrorAction 'SilentlyContinue' + $innerInputObject = @{ + Name = $name + Scope = $scope + ErrorAction = 'SilentlyContinue' + } + if (-not [String]::IsNullOrEmpty($resourceGroupName)) { + $innerInputObject['resourceGroupName'] = $resourceGroupName + } + if (-not [String]::IsNullOrEmpty($ManagementGroupId)) { + $innerInputObject['ManagementGroupId'] = $ManagementGroupId + } + [array]$deployments = Get-ResourceIdsOfDeploymentInner @innerInputObject if ($deployments) { break } diff --git a/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 b/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 index f4959c50c6..af325a271c 100644 --- a/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 @@ -10,7 +10,10 @@ Requires the resource in question to be tagged with 'removeModule = Mandatory. The name of the module to remove .PARAMETER ResourceGroupName -Mandatory. The resource group of the resource to remove +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 @@ -39,6 +42,9 @@ function Remove-Deployment { [Parameter(Mandatory = $false)] [string] $ResourceGroupName, + [Parameter(Mandatory = $false)] + [string] $ManagementGroupId, + [Parameter(Mandatory = $true)] [string] $DeploymentName, @@ -81,9 +87,14 @@ function Remove-Deployment { # Fetch deployments # ================= $deploymentsInputObject = @{ - Name = $deploymentName - Scope = $deploymentScope - ResourceGroupName = $resourceGroupName + Name = $deploymentName + Scope = $deploymentScope + } + if (-not [String]::IsNullOrEmpty($resourceGroupName)) { + $deploymentsInputObject['resourceGroupName'] = $resourceGroupName + } + if (-not [String]::IsNullOrEmpty($ManagementGroupId)) { + $deploymentsInputObject['ManagementGroupId'] = $ManagementGroupId } [array] $deploymentResourceIds = Get-ResourceIdsOfDeployment @deploymentsInputObject -Verbose Write-Verbose ('Total number of deployments after fetching deployments [{0}]' -f $deploymentResourceIds.Count) -Verbose From e9c6663a71bb5a40cc435298f5dce25b581ce0d3 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Wed, 19 Jan 2022 10:17:23 +0100 Subject: [PATCH 2/5] Further bugfixes --- .../helper/Get-ResourceIdsOfDeployment.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 b/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 index 30ee721f2e..ea46d4019e 100644 --- a/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsOfDeployment.ps1 @@ -76,6 +76,7 @@ function Get-ResourceIdsOfDeploymentInner { # In case we already have any such resources in the list, we should remove them [array]$resultSet = $resultSet | Where-Object { $_ -notmatch "/resourceGroups/$resourceGroupName/" } } + break } 'subscription' { [array]$deploymentTargets = (Get-AzDeploymentOperation -DeploymentName $name).TargetResource | Where-Object { $_ -ne $null } @@ -95,6 +96,7 @@ function Get-ResourceIdsOfDeploymentInner { [array]$resultSet += Get-ResourceIdsOfDeploymentInner -name (Split-Path $deployment -Leaf) -Scope 'subscription' } } + break } 'managementgroup' { [array]$deploymentTargets = (Get-AzManagementGroupDeploymentOperation -DeploymentName $name -ManagementGroupId $ManagementGroupId).TargetResource | Where-Object { $_ -ne $null } @@ -104,7 +106,7 @@ function Get-ResourceIdsOfDeploymentInner { } foreach ($deployment in ($deploymentTargets | Where-Object { $_ -match '/deployments/' } )) { [array]$resultSet = $resultSet | Where-Object { $_ -ne $deployment } - if ($deployment -match '/managementGroup/') { + if ($deployment -match '/subscriptions/') { # Subscription Level Child Deployments [array]$resultSet += Get-ResourceIdsOfDeploymentInner -Name (Split-Path $deployment -Leaf) -Scope 'subscription' } else { @@ -112,6 +114,7 @@ function Get-ResourceIdsOfDeploymentInner { [array]$resultSet += Get-ResourceIdsOfDeploymentInner -name (Split-Path $deployment -Leaf) -scope 'managementgroup' -ManagementGroupId $ManagementGroupId } } + break } 'tenant' { [array]$deploymentTargets = (Get-AzTenantDeploymentOperation -DeploymentName $name).TargetResource | Where-Object { $_ -ne $null } @@ -121,7 +124,7 @@ function Get-ResourceIdsOfDeploymentInner { } foreach ($deployment in ($deploymentTargets | Where-Object { $_ -match '/deployments/' } )) { [array]$resultSet = $resultSet | Where-Object { $_ -ne $deployment } - if ($deployment -match '/tenant/') { + if ($deployment -match '/managementgroups/') { # Management Group Level Child Deployments [array]$resultSet += Get-ResourceIdsOfDeploymentInner -Name (Split-Path $deployment -Leaf) -scope 'managementgroup' -ManagementGroupId $ManagementGroupId } else { @@ -129,6 +132,7 @@ function Get-ResourceIdsOfDeploymentInner { [array]$resultSet += Get-ResourceIdsOfDeploymentInner -name (Split-Path $deployment -Leaf) } } + break } } return $resultSet From 3daa1937ab8af9217836200f28323dd191309e61 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Wed, 19 Jan 2022 10:44:57 +0100 Subject: [PATCH 3/5] Fixed formatting --- .../Get-ResourceIdsAsFormattedObjectList.ps1 | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsAsFormattedObjectList.ps1 b/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsAsFormattedObjectList.ps1 index 2136afbe0a..3c5f7d2452 100644 --- a/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsAsFormattedObjectList.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Get-ResourceIdsAsFormattedObjectList.ps1 @@ -36,10 +36,18 @@ function Get-ResourceIdsAsFormattedObjectList { switch ($idElements.Count) { { $PSItem -eq 5 } { - # subscription level resource group - $formattedResources += @{ - resourceId = $resourceId - type = 'Microsoft.Resources/resourceGroups' + if ($idElements[3] -eq 'managementGroups') { + # management-group level management group (e.g. '/providers/Microsoft.Management/managementGroups/testMG') + $formattedResources += @{ + resourceId = $resourceId + type = $idElements[2, 3] -join '/' + } + } else { + # subscription level resource group (e.g. '/subscriptions//resourceGroups/myRG') + $formattedResources += @{ + resourceId = $resourceId + type = 'Microsoft.Resources/resourceGroups' + } } break } From 615a12a841acc3aef5570632f027888a2cb2a130 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Wed, 19 Jan 2022 12:30:23 +0100 Subject: [PATCH 4/5] Updated & enabled mgmt group removal --- .azuredevops/modulePipelines/ms.management.managementgroups.yml | 2 +- .github/workflows/ms.management.managementgroups.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.azuredevops/modulePipelines/ms.management.managementgroups.yml b/.azuredevops/modulePipelines/ms.management.managementgroups.yml index 007a7ea0ed..e321fa070f 100644 --- a/.azuredevops/modulePipelines/ms.management.managementgroups.yml +++ b/.azuredevops/modulePipelines/ms.management.managementgroups.yml @@ -4,7 +4,7 @@ parameters: - name: removeDeployment displayName: Remove deployed module type: boolean - default: false # Deployment does not support tags + default: true - name: versioningOption displayName: The mode to handle the version increments [major|minor|patch] type: string diff --git a/.github/workflows/ms.management.managementgroups.yml b/.github/workflows/ms.management.managementgroups.yml index b10520e08e..c2bb16da0c 100644 --- a/.github/workflows/ms.management.managementgroups.yml +++ b/.github/workflows/ms.management.managementgroups.yml @@ -7,7 +7,7 @@ on: type: boolean description: 'Remove deployed module' required: false - default: 'false' # Deployment does not support tags + default: 'true' versioningOption: type: choice description: 'The mode to handle the version increments [major|minor|patch]' From 3bf1116ef6f060f88dd9ee1327e248d70f16c9b1 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Wed, 19 Jan 2022 12:45:27 +0100 Subject: [PATCH 5/5] Minor update to error output --- .../resourceValidation/Test-TemplateWithParameterFile.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utilities/pipelines/resourceValidation/Test-TemplateWithParameterFile.ps1 b/utilities/pipelines/resourceValidation/Test-TemplateWithParameterFile.ps1 index a9e8f4a9d3..dd7eee6f35 100644 --- a/utilities/pipelines/resourceValidation/Test-TemplateWithParameterFile.ps1 +++ b/utilities/pipelines/resourceValidation/Test-TemplateWithParameterFile.ps1 @@ -124,7 +124,8 @@ function Test-TemplateWithParameterFile { } } if ($ValidationErrors) { - Write-Warning ($res.Details | ConvertTo-Json -Depth 10 | Out-String) + if ($res.Details) { Write-Warning ($res.Details | ConvertTo-Json -Depth 10 | Out-String) } + if ($res.Message) { Write-Warning $res.Message } Write-Error 'Template is not valid.' } else { Write-Verbose 'Template is valid' -Verbose