diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f5b2d93..51675dc1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,6 +8,7 @@ on: branches: [main] paths: - "src/**" + - "!src/AzOps.psd1" permissions: id-token: write diff --git a/docs/wiki/Frequently-Asked-Questions.md b/docs/wiki/Frequently-Asked-Questions.md index 53900427..274db810 100644 --- a/docs/wiki/Frequently-Asked-Questions.md +++ b/docs/wiki/Frequently-Asked-Questions.md @@ -17,6 +17,7 @@ This article answers frequently asked questions relating to AzOps. - [**I want to discover and manage several Azure Firewall Policy's and rule collections spread out across several resource groups and subscriptions**](#i-want-to-discover-and-manage-several-azure-firewall-policys-and-rule-collections-spread-out-across-several-resource-groups-and-subscriptions) - [Push scenarios and settings](#push-scenarios-and-settings) - [**I want to have multiple different deployments at scope using the same template file but different parameter files**](#i-want-to-have-multiple-different-deployments-at-scope-using-the-same-template-file-but-different-parameter-files) + - [**I have AllowMultipleTemplateParameterFiles set to true and want deployments performed in parallel**](#i-have-allowmultipletemplateparameterfiles-set-to-true-and-want-deployments-performed-in-parallel) - [**I have AllowMultipleTemplateParameterFiles set to true and when changes are made to a template no deployment is performed**](#i-have-allowmultipletemplateparameterfiles-set-to-true-and-when-changes-are-made-to-a-template-no-deployment-is-performed) - [**I am getting: Missing defaultValue and no parameter file found, skip deployment**](#i-am-getting-missing-defaultvalue-and-no-parameter-file-found-skip-deployment) @@ -185,6 +186,26 @@ scope/ ``` > Note: To avoid having AzOps deploy the base `template.bicep` unintentionally, ensure you have at least one parameter without default value in `template.bicep` and no lingering 1:1 matching parameter file. +### **I have AllowMultipleTemplateParameterFiles set to true and want deployments performed in parallel** + +Can AzOps perform parallel deployments of the below 3 separate parameter files? +```bash +scope/ +├── template.x1.bicepparam +├── template.x2.bicepparam +├── template.x3.parameters.json +└── template.bicep +``` +Yes, ensure the following setting combinations are applied + +```bash + "Core.AllowMultipleTemplateParameterFiles": true + + "Core.ParallelDeployMultipleTemplateParameterFiles": true +``` + +> Note: By default, AzOps performs serial deployments. + ### **I have AllowMultipleTemplateParameterFiles set to true and when changes are made to a template no deployment is performed** When using a custom deployment templates with multiple corresponding parameter files, can I ensure that changes made to the template triggers AzOps to create separate deployments for each corresponding parameter file? diff --git a/docs/wiki/Settings.md b/docs/wiki/Settings.md index d37fa2ad..87b0424f 100644 --- a/docs/wiki/Settings.md +++ b/docs/wiki/Settings.md @@ -35,11 +35,12 @@ The following configuration values can be modified within the `settings.json` fi | 23 | State | Folder to store AzOpsState artefact, defaults to `root` | `"Core.State: "/root"` | | 24 | SubscriptionsToIncludeResourceGroups | Filter which Subscription IDs should include Resource Groups in pull [Logic Updated in v2.0.0](https://github.com/Azure/AzOps/releases/tag/2.0.0) | `"Core.SubscriptionsToIncludeResourceGroups": ["*"]` | | 25 | TemplateParameterFileSuffix | Default template file suffix. *Not recommended to change* | `"Core.TemplateParameterFileSuffix": ".json"` | -| 26 | AllowMultipleTemplateParameterFiles | Control multiple parameter file behaviour. *Not recommended to change* | `"Core.AllowMultipleTemplateParameterFiles": false` | -| 27 | DeployAllMultipleTemplateParameterFiles | Control base template deployment behaviour with changes and un-changed multiple corresponding parameter files. | `"Core.DeployAllMultipleTemplateParameterFiles": false` | -| 28 | MultipleTemplateParameterFileSuffix | Multiple parameter file suffix identifier. *Example mytemplate.x1.bicepparam* | `"Core.MultipleTemplateParameterFileSuffix": ".x"` | -| 29 | ThrottleLimit | Value declaring number of parallel threads. [Read more](https://github.com/azure/azops/wiki/performance-considerations) | `"Core.ThrottleLimit": 5` | -| 30 | WhatifExcludedChangeTypes | Exclude specific change types from WhatIf operations | `"Core.WhatifExcludedChangeTypes": ["NoChange","Ignore"]` | +| 26 | AllowMultipleTemplateParameterFiles | Control multiple parameter file behaviour. *Not recommended to change* | `"Core.AllowMultipleTemplateParameterFiles": false` | +| 27 | DeployAllMultipleTemplateParameterFiles | Control base template deployment behaviour with changes and un-changed multiple corresponding parameter files. | `"Core.DeployAllMultipleTemplateParameterFiles": false` | +| 28 | MultipleTemplateParameterFileSuffix | Multiple parameter file suffix identifier. *Example mytemplate.x1.bicepparam* | `"Core.MultipleTemplateParameterFileSuffix": ".x"` | +| 29 | ParallelDeployMultipleTemplateParameterFiles | Control parallel deployment of MultipleTemplateParameterFiles behaviour | `"Core.ParallelDeployMultipleTemplateParameterFiles": false` | +| 30 | ThrottleLimit | Value declaring number of parallel threads. [Read more](https://github.com/azure/azops/wiki/performance-considerations) | `"Core.ThrottleLimit": 5` | +| 31 | WhatifExcludedChangeTypes | Exclude specific change types from WhatIf operations | `"Core.WhatifExcludedChangeTypes": ["NoChange","Ignore"]` | ## Workflow / Pipeline Settings diff --git a/src/functions/Invoke-AzOpsPush.ps1 b/src/functions/Invoke-AzOpsPush.ps1 index 90105c4c..5e0e48a3 100644 --- a/src/functions/Invoke-AzOpsPush.ps1 +++ b/src/functions/Invoke-AzOpsPush.ps1 @@ -48,12 +48,14 @@ param ( [AzOpsScope] $ScopeObject, - [string] $FilePath, - [string] - $AzOpsMainTemplate + $AzOpsMainTemplate, + [string[]] + $ConvertedTemplate, + [string[]] + $ConvertedParameter ) #region Initialization Prep @@ -66,7 +68,9 @@ $result = [PSCustomObject] @{ TemplateFilePath = $null + TranspiledTemplateNew = $false TemplateParameterFilePath = $null + TranspiledParametersNew = $false DeploymentName = $null ScopeObject = $ScopeObject Scope = $ScopeObject.Scope @@ -104,12 +108,19 @@ if (Test-Path $templatePath) { Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Resolve.FoundTemplate' -StringValues $FilePath, $templatePath $result.TemplateFilePath = $templatePath + $newScopeObject = New-AzOpsScope -Path $result.TemplateFilePath -StatePath $StatePath -ErrorAction Stop + $result.ScopeObject = $newScopeObject + $result.Scope = $newScopeObject.Scope return $result } elseif (Test-Path $bicepTemplatePath) { Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Resolve.FoundBicepTemplate' -StringValues $FilePath, $bicepTemplatePath - $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $bicepTemplatePath -SkipParam + $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $bicepTemplatePath -SkipParam -ConvertedTemplate $ConvertedTemplate + $result.TranspiledTemplateNew = $transpiledTemplatePaths.transpiledTemplateNew $result.TemplateFilePath = $transpiledTemplatePaths.transpiledTemplatePath + $newScopeObject = New-AzOpsScope -Path $result.TemplateFilePath -StatePath $StatePath -ErrorAction Stop + $result.ScopeObject = $newScopeObject + $result.Scope = $newScopeObject.Scope return $result } } @@ -123,9 +134,14 @@ } if (Test-Path $bicepTemplatePath) { Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Resolve.FoundBicepTemplate' -StringValues $FilePath, $bicepTemplatePath - $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $bicepTemplatePath -BicepParamTemplatePath $fileItem.FullName + $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $bicepTemplatePath -BicepParamTemplatePath $fileItem.FullName -ConvertedTemplate $ConvertedTemplate -ConvertedParameter $ConvertedParameter + $result.TranspiledTemplateNew = $transpiledTemplatePaths.transpiledTemplateNew + $result.TranspiledParametersNew = $transpiledTemplatePaths.transpiledParametersNew $result.TemplateFilePath = $transpiledTemplatePaths.transpiledTemplatePath $result.TemplateParameterFilePath = $transpiledTemplatePaths.transpiledParametersPath + $newScopeObject = New-AzOpsScope -Path $result.TemplateFilePath -StatePath $StatePath -ErrorAction Stop + $result.ScopeObject = $newScopeObject + $result.Scope = $newScopeObject.Scope return $result } } @@ -184,7 +200,7 @@ # Process possible parameter files for template equivalent if (($fileItem.FullName.Split('.')[-2] -eq $paramFile.FullName.Split('.')[-3]) -or ($fileItem.FullName.Split('.')[-2] -eq $paramFile.FullName.Split('.')[-4])) { Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Resolve.MultipleTemplateParameterFile' -StringValues $paramFile.FullName - $multiResult += Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $paramFile -AzOpsMainTemplate $AzOpsMainTemplate + $multiResult += Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $paramFile -AzOpsMainTemplate $AzOpsMainTemplate -ConvertedTemplate $ConvertedTemplate -ConvertedParameter $ConvertedParameter } } if ($multiResult) { @@ -229,6 +245,10 @@ $WhatIfPreferenceState = $WhatIfPreference $WhatIfPreference = $false + # Create array of strings to track bicep file conversion + [string[]] $AzOpsTranspiledTemplate = @() + [string[]] $AzOpsTranspiledParameter = @() + # Remove lingering files from previous run $tempPath = [System.IO.Path]::GetTempPath() if ((Test-Path -Path ($tempPath + 'OUTPUT.md')) -or (Test-Path -Path ($tempPath + 'OUTPUT.json'))) { @@ -341,7 +361,13 @@ # Handle Bicep templates if ($addition.EndsWith(".bicep")) { - $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $addition | Select-Object transpiledTemplatePath + $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $addition -ConvertedTemplate $AzOpsTranspiledTemplate -ConvertedParameter $AzOpsTranspiledParameter + if ($true -eq $transpiledTemplatePaths.transpiledTemplateNew) { + $AzOpsTranspiledTemplate += $transpiledTemplatePaths.transpiledTemplatePath + } + if ($true -eq $transpiledTemplatePaths.transpiledParametersNew) { + $AzOpsTranspiledParameter += $transpiledTemplatePaths.transpiledParametersPath + } $addition = $transpiledTemplatePaths.transpiledTemplatePath } @@ -353,7 +379,16 @@ continue } - Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $addition -AzOpsMainTemplate $AzOpsMainTemplate + $resolvedArmFileAssociation = Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $addition -AzOpsMainTemplate $AzOpsMainTemplate -ConvertedTemplate $AzOpsTranspiledTemplate -ConvertedParameter $AzOpsTranspiledParameter + foreach ($fileAssociation in $resolvedArmFileAssociation) { + if ($true -eq $fileAssociation.transpiledTemplateNew) { + $AzOpsTranspiledTemplate += $fileAssociation.TemplateFilePath + } + if ($true -eq $fileAssociation.transpiledParametersNew) { + $AzOpsTranspiledParameter += $fileAssociation.TemplateParameterFilePath + } + } + $resolvedArmFileAssociation } $deletionList = foreach ($deletion in $deleteSet | Where-Object { $_ -match ((Get-Item $StatePath).Name) }) { @@ -402,10 +437,88 @@ throw } - #Starting Tenant Deployment + #Starting deployment $WhatIfPreference = $WhatIfPreferenceState $uniqueProperties = 'Scope', 'DeploymentName', 'TemplateFilePath', 'TemplateParameterFilePath' - $deploymentList | Select-Object $uniqueProperties -Unique | New-AzOpsDeployment -WhatIf:$WhatIfPreference + $uniqueDeployment = $deploymentList | Select-Object $uniqueProperties -Unique + $deploymentResult = @() + + #Determine what deployment pattern to adopt serial or parallel + if ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and (Get-PSFConfigValue -FullName 'AzOps.Core.ParallelDeployMultipleTemplateParameterFiles') -eq $true) { + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.ParallelCondition' + # Group deployments based on TemplateFilePath + $groups = $uniqueDeployment | Group-Object -Property TemplateFilePath | Where-Object { $_.Count -ge '2' -and $_.Name -ne $(Get-Item $AzOpsMainTemplate).FullName } + if ($groups) { + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.ParallelGroup' + $processedTargets = @() + # Process each deployment and evaluate serial or parallel deployment pattern + foreach ($deployment in $uniqueDeployment) { + if ($deployment.TemplateFilePath -in $groups.Name -and $deployment -notin $processedTargets) { + # Deployment part of group association for parallel processing, process entire group as parallel deployment + $targets = $($groups | Where-Object { $_.Name -eq $deployment.TemplateFilePath }).Group + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.Parallel' -StringValues $deployment.TemplateFilePath, $targets.Count + # Prepare Input Data for parallel processing + $runspaceData = @{ + AzOpsPath = "$($script:ModuleRoot)\AzOps.psd1" + StatePath = $StatePath + WhatIfPreference = $WhatIfPreference + runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup + runspace_AzOpsSubscriptions = $script:AzOpsSubscriptions + runspace_AzOpsPartialRoot = $script:AzOpsPartialRoot + runspace_AzOpsResourceProvider = $script:AzOpsResourceProvider + } + # Pass deployment targets for parallel processing and output deployment result for later + $deploymentResult += $targets | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { + $deployment = $_ + $runspaceData = $using:runspaceData + + Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" + $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru + + & $azOps { + $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup + $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions + $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot + $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider + } + + & $azOps { + $deployment | New-AzOpsDeployment -WhatIf:$runspaceData.WhatIfPreference + } + } -UseNewRunspace + # Add targets to processed list to avoid duplicate deployment + $processedTargets += $targets + } + elseif ($deployment -notin $processedTargets) { + # Deployment not part of group association for parallel processing, process this as serial deployment + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.Serial' -StringValues $deployment.Count + $deploymentResult += $deployment | New-AzOpsDeployment -WhatIf:$WhatIfPreference + } + else { + # Deployment already processed by group association from parallel processing, skip this duplicate deployment + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.Skip' -StringValues $deployment.TemplateFilePath, $deployment.TemplateParameterFilePath + } + } + } + else { + # No deployments with matching TemplateFilePath identified + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.Serial' -StringValues $deployment.Count + $deploymentResult += $uniqueDeployment | New-AzOpsDeployment -WhatIf:$WhatIfPreference + } + } else { + # Perform serial deployment only + Write-PSFMessage -Level Verbose @common -String 'Invoke-AzOpsPush.Deployment.Serial' -StringValues $uniqueDeployment.Count + $deploymentResult += $uniqueDeployment | New-AzOpsDeployment -WhatIf:$WhatIfPreference + } + + if ($deploymentResult) { + # Output deploymentResult outside module + $deploymentResult + #Process deploymentResult and output result + foreach ($result in $deploymentResult) { + Set-AzOpsWhatIfOutput -FilePath $result.filePath -ParameterFilePath $result.parameterFilePath -Results $result.results + } + } #Removal of Supported resourceTypes $uniqueProperties = 'Scope', 'TemplateFilePath', 'TemplateParameterFilePath' diff --git a/src/internal/classes/AzOpsScope.ps1 b/src/internal/classes/AzOpsScope.ps1 index b5c99586..5155337c 100644 --- a/src/internal/classes/AzOpsScope.ps1 +++ b/src/internal/classes/AzOpsScope.ps1 @@ -371,7 +371,6 @@ return $null } [string] IsResourceProvider() { - if ($this.Scope -match $this.regex_managementgroupProvider) { return (($this.regex_managementgroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1] } diff --git a/src/internal/configurations/Core.ps1 b/src/internal/configurations/Core.ps1 index 8eebec11..95bb1284 100644 --- a/src/internal/configurations/Core.ps1 +++ b/src/internal/configurations/Core.ps1 @@ -29,5 +29,6 @@ Set-PSFConfig -Module AzOps -Name Core.TemplateParameterFileSuffix -Value '.json Set-PSFConfig -Module AzOps -Name Core.AllowMultipleTemplateParameterFiles -Value $false -Initialize -Validation string -Description 'Global flag to control multiple parameter file behaviour' Set-PSFConfig -Module AzOps -Name Core.DeployAllMultipleTemplateParameterFiles -Value $false -Initialize -Validation string -Description 'Global flag to control base template deployment behaviour with changes and un-changed multiple corresponding parameter files' Set-PSFConfig -Module AzOps -Name Core.MultipleTemplateParameterFileSuffix -Value '.x' -Initialize -Validation string -Description 'Multiple parameter file suffix identifier' +Set-PSFConfig -Module AzOps -Name Core.ParallelDeployMultipleTemplateParameterFiles -Value $false -Initialize -Validation string -Description 'Global flag to control parallel deployment of MultipleTemplateParameterFiles behaviour' Set-PSFConfig -Module AzOps -Name Core.ThrottleLimit -Value 5 -Initialize -Validation integer -Description 'Throttle limit used in Foreach-Object -Parallel for resource/subscription discovery' Set-PSFConfig -Module AzOps -Name Core.WhatifExcludedChangeTypes -Value @('NoChange', 'Ignore') -Initialize -Validation stringarray -Description 'Exclude specific change types from WhatIf operations.' \ No newline at end of file diff --git a/src/internal/functions/ConvertFrom-AzOpsBicepTemplate.ps1 b/src/internal/functions/ConvertFrom-AzOpsBicepTemplate.ps1 index 27e66a97..4aa0a2d3 100644 --- a/src/internal/functions/ConvertFrom-AzOpsBicepTemplate.ps1 +++ b/src/internal/functions/ConvertFrom-AzOpsBicepTemplate.ps1 @@ -9,10 +9,16 @@ BicepParamTemplatePath, when provided function does not attempt default parameter file discovery. .PARAMETER SkipParam Switch when set will avoid parameter file discovery. + .PARAMETER ConvertedTemplate + Array of strings, already converted base template, if file is on list skip conversion. + .PARAMETER ConvertedParameter + Array of strings, already converted parameter, if file is on list skip conversion. .EXAMPLE ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath "root/tenant root group (xxxx-xxxx-xxxx-xxxx-xxxx)/es (es)/subscription (xxxx-xxxx-xxxx-xxxx)/resource-rg/main.bicep" transpiledTemplatePath : root/tenant root group (xxxx-xxxx-xxxx-xxxx-xxxx)/es (es)/subscription (xxxx-xxxx-xxxx-xxxx)/resource-rg/main.json + transpiledTemplateNew : True transpiledParametersPath : root/tenant root group (xxxx-xxxx-xxxx-xxxx-xxxx)/es (es)/subscription (xxxx-xxxx-xxxx-xxxx)/resource-rg/main.parameters.json + transpiledParametersNew : True #> [CmdletBinding()] @@ -23,23 +29,33 @@ [string] $BicepParamTemplatePath, [switch] - $SkipParam + $SkipParam, + [string[]] + $ConvertedTemplate, + [string[]] + $ConvertedParameter ) begin { # Assert bicep binaries Assert-AzOpsBicepDependency -Cmdlet $PSCmdlet + # Default transpiled values to false + $transpiledTemplateNew = $false + $transpiledParametersNew = $false } process { - # Convert bicep template - $transpiledTemplatePath = $BicepTemplatePath -replace '\.bicep', '.json' - Write-PSFMessage -Level Verbose -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate' -StringValues $BicepTemplatePath, $transpiledTemplatePath - Invoke-AzOpsNativeCommand -ScriptBlock { bicep build $bicepTemplatePath --outfile $transpiledTemplatePath } - # Check if bicep build created (ARM) template - if (-not (Test-Path $transpiledTemplatePath)) { - # If bicep build did not produce file exit with error - Write-PSFMessage -Level Error -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate.Error' -StringValues $BicepTemplatePath - throw + $transpiledTemplatePath = [IO.Path]::GetFullPath("$($BicepTemplatePath -replace '\.bicep', '.json')") + if ($transpiledTemplatePath -notin $ConvertedTemplate) { + # Convert bicep template + Write-PSFMessage -Level Verbose -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate' -StringValues $BicepTemplatePath, $transpiledTemplatePath + Invoke-AzOpsNativeCommand -ScriptBlock { bicep build $bicepTemplatePath --outfile $transpiledTemplatePath } + $transpiledTemplateNew = $true + # Check if bicep build created (ARM) template + if (-not (Test-Path $transpiledTemplatePath)) { + # If bicep build did not produce file exit with error + Write-PSFMessage -Level Error -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate.Error' -StringValues $BicepTemplatePath + throw + } } if (-not $SkipParam) { if (-not $BicepParamTemplatePath) { @@ -53,15 +69,18 @@ Write-PSFMessage -Level Verbose -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.BicepParam' -StringValues $BicepTemplatePath, $bicepParametersPath } if ($bicepParametersPath -and (Test-Path $bicepParametersPath)) { - # Convert bicepparam to ARM parameter file - $transpiledParametersPath = $bicepParametersPath -replace '\.bicepparam', '.parameters.json' - Write-PSFMessage -Level Verbose -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepParam' -StringValues $bicepParametersPath, $transpiledParametersPath - Invoke-AzOpsNativeCommand -ScriptBlock { bicep build-params $bicepParametersPath --outfile $transpiledParametersPath } - # Check if bicep build-params created (ARM) parameters - if (-not (Test-Path $transpiledParametersPath)) { - # If bicep build-params did not produce file exit with error - Write-PSFMessage -Level Error -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepParam.Error' -StringValues $bicepParametersPath - throw + $transpiledParametersPath = [IO.Path]::GetFullPath("$($bicepParametersPath -replace '\.bicepparam', '.parameters.json')") + if ($transpiledParametersPath -notin $ConvertedParameter) { + # Convert bicepparam to ARM parameter file + Write-PSFMessage -Level Verbose -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepParam' -StringValues $bicepParametersPath, $transpiledParametersPath + Invoke-AzOpsNativeCommand -ScriptBlock { bicep build-params $bicepParametersPath --outfile $transpiledParametersPath } + $transpiledParametersNew = $true + # Check if bicep build-params created (ARM) parameters + if (-not (Test-Path $transpiledParametersPath)) { + # If bicep build-params did not produce file exit with error + Write-PSFMessage -Level Error -String 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepParam.Error' -StringValues $bicepParametersPath + throw + } } } else { @@ -71,7 +90,9 @@ # Return transpiled (ARM) template paths $return = [PSCustomObject]@{ transpiledTemplatePath = $transpiledTemplatePath + transpiledTemplateNew = $transpiledTemplateNew transpiledParametersPath = $transpiledParametersPath + transpiledParametersNew = $transpiledParametersNew } return $return } diff --git a/src/internal/functions/New-AzOpsDeployment.ps1 b/src/internal/functions/New-AzOpsDeployment.ps1 index b0e084de..b6577216 100644 --- a/src/internal/functions/New-AzOpsDeployment.ps1 +++ b/src/internal/functions/New-AzOpsDeployment.ps1 @@ -23,6 +23,11 @@ .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 #> [CmdletBinding(SupportsShouldProcess = $true)] @@ -157,6 +162,12 @@ } # Proceed with WhatIf or Deployment if scope was found if ($scopeFound) { + $deploymentResult = [PSCustomObject]@{ + filePath = '' + parameterFilePath = '' + results = '' + deployment = '' + } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } @@ -176,10 +187,13 @@ elseif ($resultsErrorMessage -match 'DeploymentWhatIfResourceError' -and $resultsErrorMessage -match "The request to predict template deployment") { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -Target $scopeObject -Tag Error -StringValues $resultsErrorMessage if ($parameters.TemplateParameterFile) { - Set-AzOpsWhatIfOutput -FilePath $parameters.TemplateFile -ParameterFilePath $parameters.TemplateParameterFile -Results ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage) + $deploymentResult.filePath = $parameters.TemplateFile + $deploymentResult.parameterFilePath = $parameters.TemplateParameterFile + $deploymentResult.results = ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage) } else { - Set-AzOpsWhatIfOutput -FilePath $parameters.TemplateFile -Results ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage) + $deploymentResult.filePath = $parameters.TemplateFile + $deploymentResult.results = ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage) } } else { @@ -195,10 +209,13 @@ Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject if ($parameters.TemplateParameterFile) { - Set-AzOpsWhatIfOutput -FilePath $parameters.TemplateFile -ParameterFilePath $parameters.TemplateParameterFile -Results $results + $deploymentResult.filePath = $parameters.TemplateFile + $deploymentResult.parameterFilePath = $parameters.TemplateParameterFile + $deploymentResult.results = $results } else { - Set-AzOpsWhatIfOutput -FilePath $parameters.TemplateFile -Results $results + $deploymentResult.filePath = $parameters.TemplateFile + $deploymentResult.results = $results } } # Remove ExcludeChangeType parameter as it doesn't exist for deployment cmdlets @@ -208,7 +225,7 @@ $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start $($scopeObject.type) Deployment with $deploymentCommand?")) { if (-not $invalidTemplate) { - & $deploymentCommand @parameters + $deploymentResult.deployment = & $deploymentCommand @parameters } } else { @@ -216,5 +233,9 @@ Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf' } } + #Return + if ($deploymentResult) { + return $deploymentResult + } } } \ No newline at end of file diff --git a/src/internal/functions/Set-AzOpsContext.ps1 b/src/internal/functions/Set-AzOpsContext.ps1 index 10134e50..d8566fea 100644 --- a/src/internal/functions/Set-AzOpsContext.ps1 +++ b/src/internal/functions/Set-AzOpsContext.ps1 @@ -6,7 +6,7 @@ .DESCRIPTION Changes the currently active azure context to the subscription of the specified scope object. .PARAMETER ScopeObject - The scope object into which context to change. + The scope object [AzOpsScope] into which context to change. .EXAMPLE > Set-AzOpsContext -ScopeObject $scopeObject Changes the current context to the subscription of $scopeObject. @@ -15,7 +15,6 @@ [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [AzOpsScope] $ScopeObject ) @@ -25,7 +24,6 @@ process { if (-not $ScopeObject.Subscription) { return } - if ($context.Subscription.Id -ne $ScopeObject.Subscription) { Write-PSFMessage -Level Verbose -String 'Set-AzOpsContext.Change' -StringValues $context.Subscription.Name, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription Set-AzContext -SubscriptionId $scopeObject.Subscription -WhatIf:$false diff --git a/src/internal/functions/Set-AzOpsWhatIfOutput.ps1 b/src/internal/functions/Set-AzOpsWhatIfOutput.ps1 index 5768394a..73ca8c7c 100644 --- a/src/internal/functions/Set-AzOpsWhatIfOutput.ps1 +++ b/src/internal/functions/Set-AzOpsWhatIfOutput.ps1 @@ -45,11 +45,11 @@ ) process { - Write-PSFMessage -Level Verbose -String 'Set-AzOpsWhatIfOutput.WhatIfFile' $tempPath = [System.IO.Path]::GetTempPath() if ((-not (Test-Path -Path ($tempPath + 'OUTPUT.md'))) -or (-not (Test-Path -Path ($tempPath + 'OUTPUT.json')))) { - New-Item -Path ($tempPath + 'OUTPUT.md') -WhatIf:$false - New-Item -Path ($tempPath + 'OUTPUT.json') -WhatIf:$false + Write-PSFMessage -Level Verbose -String 'Set-AzOpsWhatIfOutput.WhatIfFile' + New-Item -Path ($tempPath + 'OUTPUT.md') -WhatIf:$false | Out-Null + New-Item -Path ($tempPath + 'OUTPUT.json') -WhatIf:$false | Out-Null } if ($ParameterFilePath) { @@ -69,7 +69,7 @@ # Gather current OUTPUT.json content $existingContent = @(Get-Content -Path ($tempPath + 'OUTPUT.json') -Raw | ConvertFrom-Json -Depth 100) # Export results to json file - Write-PSFMessage -Level Verbose -String 'Set-AzOpsWhatIfOutput.WhatIfFileAddingJson' + Write-PSFMessage -Level Verbose -String 'Set-AzOpsWhatIfOutput.WhatIfFileAdding' -StringValues 'json', $FilePath, $ParameterFilePath if ($RemoveAzOpsFlag) { $resultJson = [PSCustomObject]@{ WhatIfResult = $Results @@ -105,7 +105,7 @@ } } if ((($mdOutput | Measure-Object -Line -Character -Word).Characters + $existingContentStringMeasureMd.Characters) -le $ResultSizeMaxLimit) { - Write-PSFMessage -Level Verbose -String 'Set-AzOpsWhatIfOutput.WhatIfFileAddingMd' + Write-PSFMessage -Level Verbose -String 'Set-AzOpsWhatIfOutput.WhatIfFileAdding' -StringValues 'markdown', $FilePath, $ParameterFilePath Add-Content -Path ($tempPath + 'OUTPUT.md') -Value $mdOutput -WhatIf:$false } else { diff --git a/src/localized/en-us/Strings.psd1 b/src/localized/en-us/Strings.psd1 index 390e59b5..feb30461 100644 --- a/src/localized/en-us/Strings.psd1 +++ b/src/localized/en-us/Strings.psd1 @@ -189,6 +189,11 @@ 'Invoke-AzOpsPush.Deploy.ResourceProvider' = 'Invoking new state deployment - *.resourceproviders.json for a file {0}' # $addition 'Invoke-AzOpsPush.Deploy.Subscription' = 'Invoking new state deployment - *.subscription.json for a file {0}' # $addition 'Invoke-AzOpsPush.Deployment.Required' = 'Deployment required' # + 'Invoke-AzOpsPush.Deployment.Parallel' = 'Running parallel deployments of {1} items with matching TemplateFilePath: {0}' # $deployment, $targets + 'Invoke-AzOpsPush.Deployment.Serial' = 'Running {0} serial deployments' # $uniqueDeployment + 'Invoke-AzOpsPush.Deployment.Skip' = 'Skipping deployment of template: {0} with parameter: {1}, its already been deployed' # $deployment.TemplateFilePath, $deployment.TemplateParameterFilePath + 'Invoke-AzOpsPush.Deployment.ParallelCondition' = 'Parallel deployment condition true' # + 'Invoke-AzOpsPush.Deployment.ParallelGroup' = 'Identified multiple deployments with matching TemplateFilePath' # $groups 'Invoke-AzOpsPush.Dependency.Missing' = 'Missing resource dependency for successfull deletion. Error exiting runtime.' 'Invoke-AzOpsPush.DeploymentList.NotFound' = 'Expecting deploymentList object, it was not found. Error exiting runtime.' 'Invoke-AzOpsPush.Resolve.FoundTemplate' = 'Found template {1} for parameters {0}' # $FilePath, $templatePath @@ -294,8 +299,7 @@ 'Set-AzOpsStringLength.WithInLimit' = 'String {0} within limit of {1}' # $String 'Set-AzOpsWhatIfOutput.WhatIfFile' = 'Creating WhatIf markdown and json files' # - 'Set-AzOpsWhatIfOutput.WhatIfFileAddingJson' = 'Adding content to WhatIf json file' # - 'Set-AzOpsWhatIfOutput.WhatIfFileAddingMd' = 'Adding content to WhatIf markdown file' # + 'Set-AzOpsWhatIfOutput.WhatIfFileAdding' = 'Adding content to WhatIf {0} file for template {1} with parameter file {2}' # '', $FilePath, $ParameterFilePath 'Set-AzOpsWhatIfOutput.WhatIfFileMax' = 'WhatIf markdown and json files have reached character limit, unable to append more information to files. WhatIf is too large for comment field, for more details look at PR files to determine changes.' # $ResultSizeMaxLimit, $ResultSizeLimit 'Set-AzOpsWhatIfOutput.WhatIfMessageMax' = 'WhatIf have reached maximum character limit, unable to append warning message. WhatIf is too large for comment field, for more details look at PR files to determine changes.' # $ResultSizeMaxLimit, $ResultSizeLimit 'Set-AzOpsWhatIfOutput.WhatIfResults' = 'WhatIf Output {0}' # $results diff --git a/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 b/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 index 0b438c66..78169c5e 100644 --- a/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 +++ b/src/tests/functional/Microsoft.Authorization/policyAssignments/scenario.ps1 @@ -85,7 +85,7 @@ Describe "Scenario - policyAssignments" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 b/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 index 28b1f9be..faada35f 100644 --- a/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 +++ b/src/tests/functional/Microsoft.Authorization/roleAssignments/scenario.ps1 @@ -85,7 +85,7 @@ Describe "Scenario - roleAssignments" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 b/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 index 99b83c90..9ec4b297 100644 --- a/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 +++ b/src/tests/functional/Microsoft.Compute/virtualMachines/scenario.ps1 @@ -88,7 +88,7 @@ Describe "Scenario - virtualMachines" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Insights/activityLogAlerts/scenario.ps1 b/src/tests/functional/Microsoft.Insights/activityLogAlerts/scenario.ps1 index 5fe0f97f..5f7fe9ef 100644 --- a/src/tests/functional/Microsoft.Insights/activityLogAlerts/scenario.ps1 +++ b/src/tests/functional/Microsoft.Insights/activityLogAlerts/scenario.ps1 @@ -76,7 +76,7 @@ Describe "Scenario - activityLogAlerts" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.KeyVault/vaults/scenario.ps1 b/src/tests/functional/Microsoft.KeyVault/vaults/scenario.ps1 index b817af2c..90888ad8 100644 --- a/src/tests/functional/Microsoft.KeyVault/vaults/scenario.ps1 +++ b/src/tests/functional/Microsoft.KeyVault/vaults/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - vaults" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Logic/workflows/scenario.ps1 b/src/tests/functional/Microsoft.Logic/workflows/scenario.ps1 index ca576ee8..5d865468 100644 --- a/src/tests/functional/Microsoft.Logic/workflows/scenario.ps1 +++ b/src/tests/functional/Microsoft.Logic/workflows/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - workflows" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.ManagedIdentity/userAssignedIdentities/scenario.ps1 b/src/tests/functional/Microsoft.ManagedIdentity/userAssignedIdentities/scenario.ps1 index dcfa89ae..bb6fd931 100644 --- a/src/tests/functional/Microsoft.ManagedIdentity/userAssignedIdentities/scenario.ps1 +++ b/src/tests/functional/Microsoft.ManagedIdentity/userAssignedIdentities/scenario.ps1 @@ -70,7 +70,7 @@ Describe "Scenario - userAssignedIdentities" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Management/managementGroups/scenario.ps1 b/src/tests/functional/Microsoft.Management/managementGroups/scenario.ps1 index 7a12f97f..f3409c99 100644 --- a/src/tests/functional/Microsoft.Management/managementGroups/scenario.ps1 +++ b/src/tests/functional/Microsoft.Management/managementGroups/scenario.ps1 @@ -67,7 +67,7 @@ Describe "Scenario - managementGroups" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 b/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 index c84aa053..e69ed1e4 100644 --- a/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/azureFirewalls/scenario.ps1 @@ -76,7 +76,7 @@ Describe "Scenario - azureFirewalls" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/bastionHosts/scenario.ps1 b/src/tests/functional/Microsoft.Network/bastionHosts/scenario.ps1 index 21dc7e17..3628f3a2 100644 --- a/src/tests/functional/Microsoft.Network/bastionHosts/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/bastionHosts/scenario.ps1 @@ -76,7 +76,7 @@ Describe "Scenario - bastionHosts" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/connections/scenario.ps1 b/src/tests/functional/Microsoft.Network/connections/scenario.ps1 index 78270043..a3d7f884 100644 --- a/src/tests/functional/Microsoft.Network/connections/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/connections/scenario.ps1 @@ -70,7 +70,7 @@ Describe "Scenario - connections" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/localNetworkGateways/scenario.ps1 b/src/tests/functional/Microsoft.Network/localNetworkGateways/scenario.ps1 index 24f30a2d..969e0b8c 100644 --- a/src/tests/functional/Microsoft.Network/localNetworkGateways/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/localNetworkGateways/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - localNetworkGateways" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/networkInterfaces/scenario.ps1 b/src/tests/functional/Microsoft.Network/networkInterfaces/scenario.ps1 index 17c9530b..9a1c8a65 100644 --- a/src/tests/functional/Microsoft.Network/networkInterfaces/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/networkInterfaces/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - networkInterfaces" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/networkSecurityGroups/scenario.ps1 b/src/tests/functional/Microsoft.Network/networkSecurityGroups/scenario.ps1 index 650421ef..747e7c60 100644 --- a/src/tests/functional/Microsoft.Network/networkSecurityGroups/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/networkSecurityGroups/scenario.ps1 @@ -70,7 +70,7 @@ Describe "Scenario - networkSecurityGroups" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/privateDnsZones/scenario.ps1 b/src/tests/functional/Microsoft.Network/privateDnsZones/scenario.ps1 index 8355713d..256e715a 100644 --- a/src/tests/functional/Microsoft.Network/privateDnsZones/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/privateDnsZones/scenario.ps1 @@ -67,7 +67,7 @@ Describe "Scenario - privateDnsZones" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/privateEndpoints/scenario.ps1 b/src/tests/functional/Microsoft.Network/privateEndpoints/scenario.ps1 index e238d89b..2d1e681e 100644 --- a/src/tests/functional/Microsoft.Network/privateEndpoints/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/privateEndpoints/scenario.ps1 @@ -70,7 +70,7 @@ Describe "Scenario - privateEndpoints" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/publicIPAddresses/scenario.ps1 b/src/tests/functional/Microsoft.Network/publicIPAddresses/scenario.ps1 index 4bc9c96d..49ae497c 100644 --- a/src/tests/functional/Microsoft.Network/publicIPAddresses/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/publicIPAddresses/scenario.ps1 @@ -70,7 +70,7 @@ Describe "Scenario - publicIPAddresses" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/routeTables/scenario.ps1 b/src/tests/functional/Microsoft.Network/routeTables/scenario.ps1 index 49129d4c..c9a81dcc 100644 --- a/src/tests/functional/Microsoft.Network/routeTables/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/routeTables/scenario.ps1 @@ -70,7 +70,7 @@ Describe "Scenario - routeTables" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Network/virtualNetworks/scenario.ps1 b/src/tests/functional/Microsoft.Network/virtualNetworks/scenario.ps1 index 403f93f3..1ba692be 100644 --- a/src/tests/functional/Microsoft.Network/virtualNetworks/scenario.ps1 +++ b/src/tests/functional/Microsoft.Network/virtualNetworks/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - virtualNetworks" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Resources/resourceGroups/scenario.ps1 b/src/tests/functional/Microsoft.Resources/resourceGroups/scenario.ps1 index 5cf05c11..b49e07de 100644 --- a/src/tests/functional/Microsoft.Resources/resourceGroups/scenario.ps1 +++ b/src/tests/functional/Microsoft.Resources/resourceGroups/scenario.ps1 @@ -67,7 +67,7 @@ Describe "Scenario - resourceGroups" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Storage/storageAccounts/scenario.ps1 b/src/tests/functional/Microsoft.Storage/storageAccounts/scenario.ps1 index e802d240..f20811b4 100644 --- a/src/tests/functional/Microsoft.Storage/storageAccounts/scenario.ps1 +++ b/src/tests/functional/Microsoft.Storage/storageAccounts/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - storageAccounts" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Web/serverfarms/scenario.ps1 b/src/tests/functional/Microsoft.Web/serverfarms/scenario.ps1 index f0256984..a39da9f0 100644 --- a/src/tests/functional/Microsoft.Web/serverfarms/scenario.ps1 +++ b/src/tests/functional/Microsoft.Web/serverfarms/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - serverfarms" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/functional/Microsoft.Web/sites/scenario.ps1 b/src/tests/functional/Microsoft.Web/sites/scenario.ps1 index 3c77ba8f..1df7daba 100644 --- a/src/tests/functional/Microsoft.Web/sites/scenario.ps1 +++ b/src/tests/functional/Microsoft.Web/sites/scenario.ps1 @@ -73,7 +73,7 @@ Describe "Scenario - sites" { #region Push Test It "Push should be successful" { - $script:push.ProvisioningState | Should -Be "Succeeded" + $script:push.deployment.ProvisioningState | Should -Be "Succeeded" } #endregion Push Test } diff --git a/src/tests/integration/Repository.Tests.ps1 b/src/tests/integration/Repository.Tests.ps1 index 8182511e..ec89b013 100644 --- a/src/tests/integration/Repository.Tests.ps1 +++ b/src/tests/integration/Repository.Tests.ps1 @@ -132,6 +132,7 @@ Describe "Repository" { $script:policySetDefinitionsDep = Get-AzPolicySetDefinition -Name 'TestPolicySetDefinitionDep' -ManagementGroupName $($script:testManagementGroup.Name) $script:subscription = (Get-AzSubscription | Where-Object Id -eq $script:subscriptionId) $script:resourceGroup = (Get-AzResourceGroup | Where-Object ResourceGroupName -eq "App1-azopsrg") + $script:resourceGroupParallelDeploy = (Get-AzResourceGroup | Where-Object ResourceGroupName -eq "ParallelDeploy-azopsrg") $script:roleAssignments = (Get-AzRoleAssignment -ObjectId "023e7c1c-1fa4-4818-bb78-0a9c5e8b0217" | Where-Object { $_.Scope -eq "/subscriptions/$script:subscriptionId" -and $_.RoleDefinitionId -eq "acdd72a7-3385-48ef-bd42-f606fba81ae7" }) $script:policyExemptions = Get-AzPolicyExemption -Name "PolicyExemptionTest" -Scope "/subscriptions/$script:subscriptionId" $script:routeTable = (Get-AzResource -Name "RouteTable" -ResourceGroupName $($script:resourceGroup).ResourceGroupName) @@ -283,6 +284,11 @@ Describe "Repository" { $script:resourceGroupDeploymentName = "AzOps-{0}-{1}" -f $($script:resourceGroupPath.Name.Replace(".json", '')), $deploymentLocationId Write-PSFMessage -Level Debug -Message "ResourceGroupFile: $($script:resourceGroupFile)" -FunctionName "BeforeAll" + $script:resourceGroupParallelDeployPath = ($filePaths | Where-Object Name -eq "microsoft.resources_resourcegroups-$(($script:resourceGroupParallelDeploy.ResourceGroupName).toLower()).json") + $script:resourceGroupParallelDeployDirectory = ($script:resourceGroupParallelDeployPath).Directory + $script:resourceGroupParallelDeployFile = ($script:resourceGroupParallelDeployPath).FullName + Write-PSFMessage -Level Debug -Message "ParallelDeployResourceGroupFile: $($script:resourceGroupParallelDeployFile)" -FunctionName "BeforeAll" + $script:roleAssignmentsPath = ($filePaths | Where-Object Name -eq "microsoft.authorization_roleassignments-$(($script:roleAssignments.RoleAssignmentId).toLower() -replace ".*/").json") $script:roleAssignmentsDirectory = ($script:roleAssignmentsPath).Directory $script:roleAssignmentsFile = ($script:roleAssignmentsPath).FullName @@ -1114,6 +1120,38 @@ Describe "Repository" { $script:deployAllRtParamPathDeployment.Count | Should -Be 2 } #endregion + + #region Multiple deployments to test parallel deployment logic + It "Deploy parallel storage accounts and compare to serial timing" { + Set-PSFConfig -FullName AzOps.Core.AllowMultipleTemplateParameterFiles -Value $true + Set-PSFConfig -FullName AzOps.Core.DeployAllMultipleTemplateParameterFiles -Value $true + Set-PSFConfig -FullName AzOps.Core.ParallelDeployMultipleTemplateParameterFiles -Value $true + $script:deployAllSta1ParamPath = Get-ChildItem -Path "$($global:testRoot)/templates/staparalleldeploy*" | Copy-Item -Destination $script:resourceGroupParallelDeployDirectory -PassThru -Force + $script:deployAllSta2ParamPath = Get-ChildItem -Path "$($global:testRoot)/templates/staserialdeploy*" | Copy-Item -Destination $script:resourceGroupParallelDeployDirectory -PassThru -Force + $changeSet = @( + "A`t$($script:deployAllSta1ParamPath.FullName[0])", + "A`t$($script:deployAllSta2ParamPath.FullName[0])" + ) + {Invoke-AzOpsPush -ChangeSet $changeSet} | Should -Not -Throw + Start-Sleep -Seconds 30 + $script:deployAllStaParamPathDeployment = Get-AzResource -ResourceGroupName $($script:resourceGroupParallelDeploy).ResourceGroupName -ResourceType 'Microsoft.Storage/storageAccounts' + $script:deployAllStaParamPathDeployment.Count | Should -Be 4 + $query = "resourcechanges | where resourceGroup =~ '$($($script:resourceGroupParallelDeploy).ResourceGroupName)' and properties.targetResourceType == 'microsoft.storage/storageaccounts' and properties.changeType == 'Create' | extend changeTime=todatetime(properties.changeAttributes.timestamp), targetResourceId=tostring(properties.targetResourceId) | summarize arg_max(changeTime, *) by targetResourceId | project changeTime, targetResourceId, properties.changeType, properties.targetResourceType | order by changeTime asc" + $createTime = Search-AzGraph -Query $query -Subscription $script:subscriptionId + # Calculate differences between creation timing + $diff1 = New-TimeSpan -Start $createTime.changeTime[0] -End $createTime.changeTime[1] + $diff2 = New-TimeSpan -Start $createTime.changeTime[0] -End $createTime.changeTime[2] + $diff3 = New-TimeSpan -Start $createTime.changeTime[1] -End $createTime.changeTime[2] + $diff4 = New-TimeSpan -Start $createTime.changeTime[0] -End $createTime.changeTime[3] + # Check if time difference is within x seconds + $allowedDiff = '8' + if ($diff1.TotalSeconds -le $allowedDiff -and $diff2.TotalSeconds -le $allowedDiff -and $diff3.TotalSeconds -le $allowedDiff -and $diff4.TotalSeconds -ge $allowedDiff) { + # Time difference is within x seconds of each other + $timeTest = "good" + } + $timeTest | Should -Be 'good' + } + #endregion } AfterAll { diff --git a/src/tests/templates/azuredeploy.jsonc b/src/tests/templates/azuredeploy.jsonc index d8a378f7..90d16405 100644 --- a/src/tests/templates/azuredeploy.jsonc +++ b/src/tests/templates/azuredeploy.jsonc @@ -518,6 +518,12 @@ "name": "Lock2-azopsrg", "location": "northeurope" }, + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2019-10-01", + "name": "ParallelDeploy-azopsrg", + "location": "northeurope" + }, { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2021-04-01-preview", diff --git a/src/tests/templates/staparalleldeploy.bicep b/src/tests/templates/staparalleldeploy.bicep new file mode 100644 index 00000000..ebe95f2c --- /dev/null +++ b/src/tests/templates/staparalleldeploy.bicep @@ -0,0 +1,21 @@ +param staName string +param location string = resourceGroup().location + +var storageName = '${toLower(staName)}${uniqueString(resourceGroup().id)}' + +resource storage_resource 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: storageName + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_GZRS' + } + properties: { + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'None' + defaultAction: 'Deny' + } + supportsHttpsTrafficOnly: true + } +} diff --git a/src/tests/templates/staparalleldeploy.xabcd.bicepparam b/src/tests/templates/staparalleldeploy.xabcd.bicepparam new file mode 100644 index 00000000..0a38a67b --- /dev/null +++ b/src/tests/templates/staparalleldeploy.xabcd.bicepparam @@ -0,0 +1,3 @@ +using './staparalleldeploy.bicep' + +param staName = 'p1azops' diff --git a/src/tests/templates/staparalleldeploy.xabcde.bicepparam b/src/tests/templates/staparalleldeploy.xabcde.bicepparam new file mode 100644 index 00000000..87762ee7 --- /dev/null +++ b/src/tests/templates/staparalleldeploy.xabcde.bicepparam @@ -0,0 +1,3 @@ +using './staparalleldeploy.bicep' + +param staName = 'p2azops' diff --git a/src/tests/templates/staparalleldeploy.xabcdf.bicepparam b/src/tests/templates/staparalleldeploy.xabcdf.bicepparam new file mode 100644 index 00000000..10236da8 --- /dev/null +++ b/src/tests/templates/staparalleldeploy.xabcdf.bicepparam @@ -0,0 +1,3 @@ +using './staparalleldeploy.bicep' + +param staName = 'p3azops' diff --git a/src/tests/templates/staserialdeploy2.bicep b/src/tests/templates/staserialdeploy2.bicep new file mode 100644 index 00000000..ebe95f2c --- /dev/null +++ b/src/tests/templates/staserialdeploy2.bicep @@ -0,0 +1,21 @@ +param staName string +param location string = resourceGroup().location + +var storageName = '${toLower(staName)}${uniqueString(resourceGroup().id)}' + +resource storage_resource 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: storageName + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_GZRS' + } + properties: { + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'None' + defaultAction: 'Deny' + } + supportsHttpsTrafficOnly: true + } +} diff --git a/src/tests/templates/staserialdeploy2.xabcdfg.bicepparam b/src/tests/templates/staserialdeploy2.xabcdfg.bicepparam new file mode 100644 index 00000000..2cde6083 --- /dev/null +++ b/src/tests/templates/staserialdeploy2.xabcdfg.bicepparam @@ -0,0 +1,3 @@ +using './staserialdeploy2.bicep' + +param staName = 's1azops'