diff --git a/.azuredevops/modulePipelines/ms.management.managementgroups.yml b/.azuredevops/modulePipelines/ms.management.managementgroups.yml index 0da08a5a47..365c23e333 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/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml b/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml index 28bf6c7497..66901fb038 100644 --- a/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml +++ b/.azuredevops/pipelineTemplates/jobs.validateModuleDeployment.yml @@ -325,6 +325,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 7e87b017d1..956635b4d9 100644 --- a/.github/actions/templates/validateModuleDeployment/action.yml +++ b/.github/actions/templates/validateModuleDeployment/action.yml @@ -189,6 +189,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/.github/workflows/ms.management.managementgroups.yml b/.github/workflows/ms.management.managementgroups.yml index b7b2d1ee14..4f4740ec58 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]' 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-DeploymentTargetResourceList.ps1 b/utilities/pipelines/resourceRemoval/helper/Get-DeploymentTargetResourceList.ps1 index d810f99735..38a91cbe8f 100644 --- a/utilities/pipelines/resourceRemoval/helper/Get-DeploymentTargetResourceList.ps1 +++ b/utilities/pipelines/resourceRemoval/helper/Get-DeploymentTargetResourceList.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-DeploymentTargetResourceListInner -Name 'keyvault-12356' -Scope 'resourcegro 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-DeploymentTargetResourceListInner { [Parameter(Mandatory = $false)] [string] $ResourceGroupName, + [Parameter(Mandatory = $false)] + [string] $ManagementGroupId, + [Parameter(Mandatory)] [ValidateSet( 'resourcegroup', @@ -65,6 +76,7 @@ function Get-DeploymentTargetResourceListInner { # 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 } @@ -84,23 +96,25 @@ function Get-DeploymentTargetResourceListInner { [array]$resultSet += Get-DeploymentTargetResourceListInner -name (Split-Path $deployment -Leaf) -Scope 'subscription' } } + break } '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 deployed resource [{0}]' -f $deployment) -Verbose [array]$resultSet += $deployment } 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-DeploymentTargetResourceListInner -Name (Split-Path $deployment -Leaf) -Scope 'subscription' } else { # Management Group Level Deployments - [array]$resultSet += Get-DeploymentTargetResourceListInner -name (Split-Path $deployment -Leaf) -scope 'managementgroup' + [array]$resultSet += Get-DeploymentTargetResourceListInner -name (Split-Path $deployment -Leaf) -scope 'managementgroup' -ManagementGroupId $ManagementGroupId } } + break } 'tenant' { [array]$deploymentTargets = (Get-AzTenantDeploymentOperation -DeploymentName $name).TargetResource | Where-Object { $_ -ne $null } @@ -110,14 +124,15 @@ function Get-DeploymentTargetResourceListInner { } 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-DeploymentTargetResourceListInner -Name (Split-Path $deployment -Leaf) -scope 'managementgroup' + [array]$resultSet += Get-DeploymentTargetResourceListInner -Name (Split-Path $deployment -Leaf) -scope 'managementgroup' -ManagementGroupId $ManagementGroupId } else { # Tenant Level Deployments [array]$resultSet += Get-DeploymentTargetResourceListInner -name (Split-Path $deployment -Leaf) } } + break } } return $resultSet @@ -132,7 +147,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 +168,12 @@ Optional. The time to wait in between the search for resources via their remove Get-DeploymentTargetResourceList -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-DeploymentTargetResourceList { @@ -158,6 +182,9 @@ function Get-DeploymentTargetResourceList { [Parameter(Mandatory = $false)] [string] $ResourceGroupName, + [Parameter(Mandatory = $false)] + [string] $ManagementGroupId, + [Parameter(Mandatory = $true)] [string] $Name, @@ -179,7 +206,18 @@ function Get-DeploymentTargetResourceList { $searchRetryCount = 1 do { - [array]$targetResources = Get-DeploymentTargetResourceListInner -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]$targetResources = Get-DeploymentTargetResourceListInner @innerInputObject if ($targetResources) { break } 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 } diff --git a/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 b/utilities/pipelines/resourceRemoval/helper/Remove-Deployment.ps1 index ec31941f24..f3ed6d5bf2 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] $deployedTargetResources = Get-DeploymentTargetResourceList @deploymentsInputObject -Verbose Write-Verbose ('Total number of deployment target resources after fetching deployments [{0}]' -f $deployedTargetResources.Count) -Verbose 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