source/Private/ArmDeploymentProgress.ps1
|
function Invoke-RJArmDeploymentWithProgress { <# .SYNOPSIS Deploys an ARM template as a background job and reports each resource as it completes. .DESCRIPTION Everything a Log Analytics Workspace (or Automation Account) deployment creates - the workspace, custom tables, Data Collection Rules, role assignments - comes from a single ARM deployment, so PowerShell only learns what's happening by polling ARM's own deployment operations. This submits the deployment as a job and polls Get-AzResourceGroupDeploymentOperation while it runs, announcing each resource with a friendly name derived from its ARM type as "Configuring <name>..." then "<name> configured" (or a Write-Warning on failure), recursing into nested deployments (e.g. the DCR bicep module, which ARM represents as its own Microsoft.Resources/deployments resource). .PARAMETER ResourceGroupName The target resource group. .PARAMETER TemplatePath Path to the JSON ARM template file. .PARAMETER DeploymentName Name for the deployment (for tracking in Azure portal). .PARAMETER Parameters Optional hashtable of template parameters. .OUTPUTS The deployment result object, as returned by Receive-Job. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$ResourceGroupName, [Parameter(Mandatory)][string]$TemplatePath, [Parameter(Mandatory)][string]$DeploymentName, [hashtable]$Parameters = @{} ) $job = New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName -Name $DeploymentName ` -TemplateFile $TemplatePath -TemplateParameterObject $Parameters -AsJob -ErrorAction Stop ` -Verbose:($VerbosePreference -eq 'Continue') ` -Debug:($DebugPreference -eq 'Continue') $seen = @{} # resourceId -> 'started' | 'done' function Report([string]$Deployment) { foreach ($op in @(Get-AzResourceGroupDeploymentOperation -ResourceGroupName $ResourceGroupName ` -DeploymentName $Deployment -ErrorAction SilentlyContinue)) { $id = $op.TargetResource if (-not $id) { continue } # Nested deployment (e.g. the DCR module) - recurse into its operations. The nested # deployment itself is never announced, but its failure message is: a failure at the # module level (e.g. template validation) would otherwise show up nowhere until the # final Receive-Job error. if ($id -match '/providers/Microsoft\.Resources/deployments/([^/]+)$') { $nestedDeployment = $Matches[1] if ($op.ProvisioningState -eq 'Failed' -and $seen[$id] -ne 'done') { $seen[$id] = 'done' Write-Warning "Nested deployment '$nestedDeployment' failed: $($op.StatusMessage)" } Report $nestedDeployment continue } # Role assignment IDs end in a GUID that identifies nothing useful - use the # resource they're scoped to instead (e.g. the workspace or DCR being assigned to). if ($id -match '^(.+)/providers/Microsoft\.Authorization/roleAssignments/[^/]+$') { $name = "Role assignment on '$((($Matches[1]) -split '/')[-1])'" } # Friendly names for the small set of resource types this deployment actually # creates, pre-capitalized (used both mid-sentence and standalone below - simpler # than deriving two casings from one string). Anything else (e.g. an Automation # Account permission resource) falls through to the plain quoted-name form below - # unlabelled, but never wrong. # The table pattern must be checked before the workspace pattern, since a table's # ID also contains ".../workspaces/..." as a substring. elseif ($id -match '/providers/Microsoft\.OperationalInsights/workspaces/[^/]+/tables/([^/]+)$') { $name = "Custom table '$($Matches[1])'" } elseif ($id -match '/providers/Microsoft\.OperationalInsights/workspaces/([^/]+)$') { $name = "Log Analytics Workspace '$($Matches[1])'" } elseif ($id -match '/providers/Microsoft\.Insights/dataCollectionRules/([^/]+)$') { $name = "Data collection rule '$($Matches[1])'" } elseif ($id -match '/providers/Microsoft\.Automation/automationAccounts/([^/]+)$') { $name = "Automation account '$($Matches[1])'" } else { $name = "'$(($id -split '/')[-1])'" } if (-not $seen[$id]) { $seen[$id] = 'started' Write-Information " Configuring $name..." } if ($seen[$id] -eq 'done' -or $op.ProvisioningState -notin 'Succeeded', 'Failed') { continue } $seen[$id] = 'done' if ($op.ProvisioningState -eq 'Succeeded') { Write-Information " $name configured" } else { Write-Warning "$name failed: $($op.StatusMessage)" } } } try { # A freshly created job can still be 'NotStarted' for a moment - treat it like 'Running', # otherwise the loop is skipped and no progress is ever reported. while ($job.State -in 'NotStarted', 'Running') { Report $DeploymentName Start-Sleep -Seconds $ArmDeploymentPollIntervalSeconds } Report $DeploymentName # final sweep for anything that finished after the last poll return Receive-Job -Job $job -Wait -ErrorAction Stop } finally { # Only discards the local job object. If the user interrupted (Ctrl+C), the ARM deployment # itself keeps running in Azure - it can be followed in the portal under the resource group's # Deployments blade. Remove-Job -Job $job -Force -ErrorAction SilentlyContinue } } |