internal/functions/New-AzOpsDeployment.ps1
function New-AzOpsDeployment { <# .SYNOPSIS Deploys a full state into azure. .DESCRIPTION Deploys a full state into azure. .PARAMETER DeploymentName Name under which to deploy the state. .PARAMETER TemplateFilePath Path where the ARM templates can be found. .PARAMETER TemplateObject TemplateObject where the templates content is stored in-memory. .PARAMETER TemplateParameterFilePath Path where the parameters of the ARM templates can be found. .PARAMETER Mode Mode in which to process the templates. Defaults to incremental. .PARAMETER StatePath The root folder under which to find the resource json. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatifExcludedChangeTypes Exclude specific change types from WhatIf operations. .PARAMETER WhatIfResultFormat Accepts ResourceIdOnly or FullResourcePayloads. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE > $AzOpsDeploymentList | Select-Object $uniqueProperties -Unique | Sort-Object -Property TemplateParameterFilePath | New-Deployment Deploy all unique deployments provided from $AzOpsDeploymentList Name Value ---- ----- filePath /root/managementgroup/subscription/resourcegroup/template.json parameterFilePath /root/managementgroup/subscription/resourcegroup/template.parameters.json results Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments.PSWhatIfOperationResult deployment Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSDeploymentStack #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [AllowNull()] [string] $DeploymentStackTemplateFilePath, [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [AllowNull()] [object] $DeploymentStackSettings, [Parameter(ValueFromPipelineByPropertyName = $true)] [string] $DeploymentName = "azops-template-deployment", [Parameter(ValueFromPipelineByPropertyName = $true)] [string] $TemplateFilePath = (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate'), [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [AllowNull()] [string] $TemporaryTemplateFilePath, [Parameter(ValueFromPipelineByPropertyName = $true)] [hashtable] $TemplateObject, [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [AllowNull()] [string] $TemplateParameterFilePath, [string] $Mode = "Incremental", [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State'), [string[]] $WhatifExcludedChangeTypes = (Get-PSFConfigValue -FullName 'AzOps.Core.WhatifExcludedChangeTypes'), [string] [ValidateSet("ResourceIdOnly","FullResourcePayloads")] $WhatIfResultFormat ) process { Write-AzOpsMessage -LogLevel Important -LogString 'New-AzOpsDeployment.Processing' -LogStringValues $DeploymentName, $TemplateFilePath, $TemplateParameterFilePath, $Mode -Target $TemplateFilePath #region Resolve Scope try { if ($TemplateParameterFilePath -and $TemplateFilePath -eq (Resolve-Path -Path (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate')).Path) { $scopeObject = New-AzOpsScope -Path $TemplateParameterFilePath -StatePath $StatePath -ErrorAction Stop -WhatIf:$false } else { $scopeObject = New-AzOpsScope -Path $TemplateFilePath -StatePath $StatePath -ErrorAction Stop -WhatIf:$false } $scopeFound = $true } catch { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.Scope.Failed' -LogStringValues $TemplateFilePath, $TemplateParameterFilePath -ErrorRecord $_ return } if (-not $scopeObject) { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.Scope.Empty' -LogStringValues $TemplateFilePath, $TemplateParameterFilePath return } #endregion Resolve Scope #region Parse Content if ($null -eq $TemplateObject) { $TemplateFileContent = [System.IO.File]::ReadAllText($TemplateFilePath) $TemplateObject = ConvertFrom-Json $TemplateFileContent -AsHashtable } if ($TemplateObject.metadata._generator.name -eq 'bicep') { # Detect bicep templates $bicepTemplate = $true } #endregion #region Process Scope # Configure variables/parameters and the WhatIf/Deployment cmdlets to be used per scope $defaultDeploymentRegion = (Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion') if ($null -eq $DeploymentStackSettings) { $parameters = @{ 'TemplateObject' = $TemplateObject 'SkipTemplateParameterPrompt' = $true 'Location' = $defaultDeploymentRegion } } elseif ($TemporaryTemplateFilePath) { $parameters = @{ 'TemplateFile' = $TemporaryTemplateFilePath 'SkipTemplateParameterPrompt' = $true 'Location' = $defaultDeploymentRegion } } else { $parameters = @{ 'TemplateFile' = $TemplateFilePath 'SkipTemplateParameterPrompt' = $true 'Location' = $defaultDeploymentRegion } } if ($WhatIfResultFormat) { $parameters.ResultFormat = $WhatIfResultFormat } # Resource Groups excluding Microsoft.Resources/resourceGroups that needs to be submitted at subscription scope if ($scopeObject.resourcegroup -and $TemplateObject.resources[0].type -ne 'Microsoft.Resources/resourceGroups') { Set-AzOpsContext -ScopeObject $scopeObject $whatIfCommand = 'Get-AzResourceGroupDeploymentWhatIfResult' if ($null -ne $DeploymentStackSettings) { $deploymentCommand = 'New-AzResourceGroupDeploymentStack' Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.ResourceGroupDeploymentStack.Processing' -LogStringValues $scopeObject, $DeploymentStackTemplateFilePath -Target $scopeObject } else { $deploymentCommand = 'New-AzResourceGroupDeployment' Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.ResourceGroup.Processing' -LogStringValues $scopeObject -Target $scopeObject } $parameters.ResourceGroupName = $scopeObject.resourcegroup $parameters.Remove('Location') } # Subscriptions elseif ($scopeObject.subscription) { Set-AzOpsContext -ScopeObject $scopeObject $whatIfCommand = 'Get-AzSubscriptionDeploymentWhatIfResult' if ($null -ne $DeploymentStackSettings) { $deploymentCommand = 'New-AzSubscriptionDeploymentStack' Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.SubscriptionDeploymentStack.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject, $DeploymentStackTemplateFilePath -Target $scopeObject } else { $deploymentCommand = 'New-AzDeployment' Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Subscription.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject } } # Management Groups elseif ($scopeObject.managementGroup -and (-not ($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_'))) { $parameters.ManagementGroupId = $scopeObject.managementgroup $whatIfCommand = 'Get-AzManagementGroupDeploymentWhatIfResult' if ($null -ne $DeploymentStackSettings) { $deploymentCommand = 'New-AzManagementGroupDeploymentStack' Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.ManagementGroupDeploymentStack.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject, $DeploymentStackTemplateFilePath -Target $scopeObject } else { $deploymentCommand = 'New-AzManagementGroupDeployment' Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.ManagementGroup.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject } } # Tenant deployments elseif ($scopeObject.type -eq 'root' -and $scopeObject.scope -eq '/') { Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Root.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject $whatIfCommand = 'Get-AzTenantDeploymentWhatIfResult' $deploymentCommand = 'New-AzTenantDeployment' } # If Management Group resource was not found, validate and prepare for first time deployment of resource elseif ($scopeObject.managementGroup -and (($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_'))) { $resourceScopeFileContent = Get-Content -Path $addition | ConvertFrom-Json -Depth 100 $resource = ($resourceScopeFileContent.resources | Where-Object {$_.type -eq 'Microsoft.Management/managementGroups'} | Select-Object -First 1) $pathDir = (Get-Item -Path $addition -Force).Directory | Resolve-Path -Relative if ((Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -ne '.') { $pathDir = Split-Path -Path $pathDir -Parent } $parentDirScopeObject = New-AzOpsScope -Path (Split-Path -Path $pathDir -Parent) -WhatIf:$false | Where-Object {(-not ($_.StatePath).StartsWith('azopsscope-assume-new-resource_'))} $parentIdScope = New-AzOpsScope -Scope (($resource).properties.details.parent.id) -WhatIf:$false | Where-Object {(-not ($_.StatePath).StartsWith('azopsscope-assume-new-resource_'))} # Validate parent existence with content parent scope, statepath and name match, determines file location match deployment scope if ($parentDirScopeObject -and $parentIdScope -and $parentDirScopeObject.Scope -eq $parentIdScope.Scope -and $parentDirScopeObject.StatePath -eq $parentIdScope.StatePath -and $parentDirScopeObject.Name -eq $parentIdScope.Name) { # Validate directory name match resource information if ((Get-Item -Path $pathDir -Force).Name -eq "$($resource.properties.displayName) ($($resource.name))") { Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Root.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject $whatIfCommand = 'Get-AzTenantDeploymentWhatIfResult' $deploymentCommand = 'New-AzTenantDeployment' } # Invalid directory name else { Write-AzOpsMessage -LogLevel Error -LogString 'New-AzOpsDeployment.Directory.NotFound' -LogStringValues (Get-Item -Path $pathDir).Name, "$($resource.properties.displayName) ($($resource.name))" throw } } # Parent missing else { Write-AzOpsMessage -LogLevel Error -LogString 'New-AzOpsDeployment.Parent.NotFound' -LogStringValues $addition throw } } else { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.Scope.Unidentified' -LogStringValues $scopeObject $scopeFound = $false } # Proceed with WhatIf or Deployment if scope was found if ($scopeFound) { $deploymentResult = [PSCustomObject]@{ filePath = '' parameterFilePath = '' deploymentStackTemplateFilePath = $DeploymentStackTemplateFilePath results = '' deployment = '' } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } if ($WhatifExcludedChangeTypes) { $parameters.ExcludeChangeType = $WhatifExcludedChangeTypes } # Code to execute only when -WhatIf:$true is passed if ($WhatIfPreference) { # Get predictive deployment results from WhatIf API $results = & $whatIfCommand @parameters -ErrorAction Continue -ErrorVariable resultsError if ($resultsError) { $resultsErrorMessage = $resultsError.exception.InnerException.Message # Ignore errors for bicep modules if ($resultsErrorMessage -match 'https://aka.ms/resource-manager-parameter-files' -and $true -eq $bicepTemplate) { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.TemplateParameterError' -Target $scopeObject $invalidTemplate = $true } # Handle WhatIf prediction errors elseif ($resultsErrorMessage -match 'DeploymentWhatIfResourceError' -and $resultsErrorMessage -match "The request to predict template deployment") { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.WhatIfWarning' -LogStringValues $resultsErrorMessage -Target $scopeObject $deploymentResult.results = ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage) } else { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.WhatIfWarning' -LogStringValues $resultsErrorMessage -Target $scopeObject throw $resultsErrorMessage } } elseif ($results.Error) { Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.TemplateError' -LogStringValues $TemplateFilePath -Target $scopeObject return } else { Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.WhatIfResults' -LogStringValues ($results | Out-String) -Target $scopeObject Write-AzOpsMessage -LogLevel InternalComment -LogString 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject } } # Remove ExcludeChangeType parameter as it doesn't exist for deployment cmdlets if ($parameters.ExcludeChangeType) { $parameters.Remove('ExcludeChangeType') } if ($deploymentCommand -match 'DeploymentStack$') { # Add Force parameter for deploymentStack cmdlets $DeploymentStackSettings['Force'] = $true $parameters += $DeploymentStackSettings } $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start $($scopeObject.type) Deployment with $deploymentCommand")) { if (-not $invalidTemplate) { try { Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Deployment' -LogStringValues $deploymentCommand, $($parameters | Out-String), $scopeObject.Scope $deploymentResult.deployment = & $deploymentCommand @parameters -ErrorAction Stop } catch { throw $_ } } } else { # Exit deployment Write-AzOpsMessage -LogLevel InternalComment -LogString 'New-AzOpsDeployment.SkipDueToWhatIf' } } #Cleanup if ($TemporaryTemplateFilePath) { Write-AzOpsMessage -LogLevel InternalComment -LogString 'New-AzOpsDeployment.TemporaryDeploymentStackTemplateFilePath.Remove' -LogStringValues $TemporaryTemplateFilePath Remove-Item -Path $TemporaryTemplateFilePath -Force -ErrorAction SilentlyContinue -WhatIf:$false $parameters.TemplateFile = $TemplateFilePath } #Return if ($deploymentResult) { if ($parameters.TemplateParameterFile) { $deploymentResult.parameterFilePath = $parameters.TemplateParameterFile } if ($parameters.TemplateFile) { $deploymentResult.filePath = $parameters.TemplateFile } else { $deploymentResult.filePath = $TemplateFilePath } if ($deploymentResult.results -eq '') { $deploymentResult.results = $results } return $deploymentResult } } } |