From 7aaf1fdf4ff4fbb4e65fd9e7df6b94192f5fc4fa Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Tue, 14 Nov 2023 20:46:06 +1100 Subject: [PATCH 1/6] Add WhatIf feature to Test module locally script --- .../Get-TemplateDeploymenWhatIf.ps1 | 185 ++++++++++++++++++ utilities/tools/Test-ModuleLocally.ps1 | 76 ++++++- 2 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 diff --git a/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 b/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 new file mode 100644 index 0000000000..cdb3db31e3 --- /dev/null +++ b/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 @@ -0,0 +1,185 @@ +<# +.SYNOPSIS +Get a template What-If deployment result using a given parameter file + +.DESCRIPTION +Get a template What-If deployment resultusing a given parameter file +Works on a resource group, subscription, managementgroup and tenant level + +.PARAMETER parametersBasePath +Mandatory. The path to the root of the parameters folder to test with + +.PARAMETER templateFilePath +Mandatory. Path to the template file from root. + +.PARAMETER parameterFilePath +Optional. Path to the parameter file from root. + +.PARAMETER location +Mandatory. Location to test in. E.g. WestEurope + +.PARAMETER resourceGroupName +Optional. Name of the resource group to deploy into. Mandatory if deploying into a resource group (resource group level) + +.PARAMETER subscriptionId +Optional. ID of the subscription to deploy into. Mandatory if deploying into a subscription (subscription level) using a Management groups service connection + +.PARAMETER managementGroupId +Optional. Name of the management group to deploy into. Mandatory if deploying into a management group (management group level) + +.PARAMETER additionalParameters +Optional. Additional parameters you can provide with the deployment. E.g. @{ resourceGroupName = 'myResourceGroup' } + +.EXAMPLE +Get-TemplateDeploymenWhatIf -templateFilePath 'C:/key-vault/vault/main.bicep' -parameterFilePath 'C:/key-vault/vault/.test/parameters.json' -location 'WestEurope' -resourceGroupName 'aLegendaryRg' + +Get What-If deployment result for the main.bicep of the KeyVault module with the parameter file 'parameters.json' using the resource group 'aLegendaryRg' in location 'WestEurope' + +.EXAMPLE +Get-TemplateDeploymenWhatIf -templateFilePath 'C:/key-vault/vault/main.bicep' -location 'WestEurope' -resourceGroupName 'aLegendaryRg' + +Get What-If deployment result for the main.bicep of the KeyVault module using the resource group 'aLegendaryRg' in location 'WestEurope' + +.EXAMPLE +Get-TemplateDeploymenWhatIf -templateFilePath 'C:/resources/resource-group/main.json' -parameterFilePath 'C:/resources/resource-group/.test/parameters.json' -location 'WestEurope' + +Get What-If deployment result for the main.json of the ResourceGroup module with the parameter file 'parameters.json' in location 'WestEurope' +#> +function Get-TemplateDeploymenWhatIf { + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory)] + [string] $templateFilePath, + + [Parameter(Mandatory)] + [string] $location, + + [Parameter(Mandatory = $false)] + [string] $parameterFilePath, + + [Parameter(Mandatory = $false)] + [string] $resourceGroupName, + + [Parameter(Mandatory = $false)] + [string] $subscriptionId, + + [Parameter(Mandatory = $false)] + [string] $managementGroupId, + + [Parameter(Mandatory = $false)] + [Hashtable] $additionalParameters + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + + # Load helper + . (Join-Path (Get-Item -Path $PSScriptRoot).parent.FullName 'sharedScripts' 'Get-ScopeOfTemplateFile.ps1') + } + + process { + $DeploymentInputs = @{ + TemplateFile = $templateFilePath + Verbose = $true + OutVariable = 'ValidationErrors' + } + if (-not [String]::IsNullOrEmpty($parameterFilePath)) { + $DeploymentInputs['TemplateParameterFile'] = $parameterFilePath + } + $ValidationErrors = $null + + # Additional parameter object provided yes/no + if ($additionalParameters) { + $DeploymentInputs += $additionalParameters + } + + $deploymentScope = Get-ScopeOfTemplateFile -TemplateFilePath $templateFilePath -Verbose + + $deploymentNamePrefix = Split-Path -Path (Split-Path $templateFilePath -Parent) -LeafBase + if ([String]::IsNullOrEmpty($deploymentNamePrefix)) { + $deploymentNamePrefix = 'templateDeployment-{0}' -f (Split-Path $templateFilePath -LeafBase) + } + if ($templateFilePath -match '.*(\\|\/)Microsoft.+') { + # If we can assume we're operating in a module structure, we can further fetch the provider namespace & resource type + $shortPathElem = (($templateFilePath -split 'Microsoft\.')[1] -replace '\\', '/') -split '/' # e.g., AppConfiguration, configurationStores, .test, common, main.test.bicep + $providerNamespace = $shortPathElem[0] # e.g., AppConfiguration + $providerNamespaceShort = ($providerNamespace -creplace '[^A-Z]').ToLower() # e.g., ac + + $resourceType = $shortPathElem[1] # e.g., configurationStores + $resourceTypeShort = ('{0}{1}' -f ($resourceType.ToLower())[0], ($resourceType -creplace '[^A-Z]')).ToLower() # e.g. cs + + $testFolderShort = Split-Path (Split-Path $templateFilePath -Parent) -Leaf # e.g., common + + $deploymentNamePrefix = "$providerNamespaceShort-$resourceTypeShort-$testFolderShort" # e.g., ac-cs-common + } + + # Generate a valid deployment name. Must match ^[-\w\._\(\)]+$ + do { + $deploymentName = ('{0}-{1}' -f $deploymentNamePrefix, (Get-Date -Format 'yyyyMMddTHHMMssffffZ'))[0..63] -join '' + } while ($deploymentName -notmatch '^[-\w\._\(\)]+$') + + if ($deploymentScope -ne 'resourceGroup') { + Write-Verbose "Testing with deployment name [$deploymentName]" -Verbose + $DeploymentInputs['DeploymentName'] = $deploymentName + } + + ################# + ## INVOKE TEST ## + ################# + switch ($deploymentScope) { + 'resourceGroup' { + if (-not [String]::IsNullOrEmpty($subscriptionId)) { + Write-Verbose ('Setting context to subscription [{0}]' -f $subscriptionId) + $null = Set-AzContext -Subscription $subscriptionId + } + if (-not (Get-AzResourceGroup -Name $resourceGroupName -ErrorAction 'SilentlyContinue')) { + if ($PSCmdlet.ShouldProcess("Resource group [$resourceGroupName] in location [$location]", 'Create')) { + $null = New-AzResourceGroup -Name $resourceGroupName -Location $location + } + } + if ($PSCmdlet.ShouldProcess('Resource group level deployment', 'Test')) { + $res = New-AzResourceGroupDeployment @DeploymentInputs -WhatIf + } + break + } + 'subscription' { + if (-not [String]::IsNullOrEmpty($subscriptionId)) { + Write-Verbose ('Setting context to subscription [{0}]' -f $subscriptionId) + $null = Set-AzContext -Subscription $subscriptionId + } + if ($PSCmdlet.ShouldProcess('Subscription level deployment', 'Test')) { + $res = New-AzDeployment @DeploymentInputs -Location $Location -WhatIf + } + break + } + 'managementGroup' { + if ($PSCmdlet.ShouldProcess('Management group level deployment', 'Test')) { + $res = New-AzManagementGroupDeployment @DeploymentInputs -Location $Location -ManagementGroupId $ManagementGroupId -WhatIf + } + break + } + 'tenant' { + Write-Verbose 'Handling tenant level validation' + if ($PSCmdlet.ShouldProcess('Tenant level deployment', 'Test')) { + $res = New-AzTenantDeployment @DeploymentInputs -Location $location -WhatIf + } + break + } + default { + throw "[$deploymentScope] is a non-supported template scope" + } + } + if ($ValidationErrors) { + 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 + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/Test-ModuleLocally.ps1 b/utilities/tools/Test-ModuleLocally.ps1 index cc291faa18..6fd8690dd2 100644 --- a/utilities/tools/Test-ModuleLocally.ps1 +++ b/utilities/tools/Test-ModuleLocally.ps1 @@ -24,6 +24,9 @@ Optional. A switch parameter that triggers the deployment of the module .PARAMETER ValidationTest Optional. A switch parameter that triggers the validation of the module only without deployment +.PARAMETER WhatIfTest +Optional. A switch parameter that triggers the what-if test of the module only without deployment + .PARAMETER SkipParameterFileTokens Optional. A switch parameter that enables you to skip the search for local custom parameter file tokens. @@ -37,6 +40,7 @@ $TestModuleLocallyInput = @{ ModuleTestFilePath = 'C:\network\route-table\.test\parameters.json' PesterTest = $false DeploymentTest = $false + WhatIfTest = $false ValidationTest = $true ValidateOrDeployParameters = @{ Location = 'westeurope' @@ -60,6 +64,7 @@ $TestModuleLocallyInput = @{ ModuleTestFilePath = 'C:\network\route-table\.test\common\main.test.bicep' PesterTest = $false DeploymentTest = $false + WhatIfTest = $false ValidationTest = $true ValidateOrDeployParameters = @{ Location = 'westeurope' @@ -76,12 +81,60 @@ Test-ModuleLocally @TestModuleLocallyInput -Verbose Run a Test-Az*Deployment using a test file with the provided tokens + +$TestModuleLocallyInput = @{ + TemplateFilePath = 'C:\network\route-table\main.bicep' + ModuleTestFilePath = 'C:\network\route-table\.test\parameters.json' + PesterTest = $false + DeploymentTest = $false + WhatIfTest = $true + ValidationTest = $false + ValidateOrDeployParameters = @{ + Location = 'westeurope' + ResourceGroupName = 'validation-rg' + SubscriptionId = '00000000-0000-0000-0000-000000000000' + ManagementGroupId = '00000000-0000-0000-0000-000000000000' + RemoveDeployment = $false + } + AdditionalTokens = @{ + tenantId = '00000000-0000-0000-0000-000000000000' + } +} +Test-ModuleLocally @TestModuleLocallyInput -Verbose + +Get What-If deployment result using a specific parameter-template combination with the provided tokens + +.EXAMPLE + +$TestModuleLocallyInput = @{ + TemplateFilePath = 'C:\network\route-table\main.bicep' + ModuleTestFilePath = 'C:\network\route-table\.test\common\main.test.bicep' + PesterTest = $false + DeploymentTest = $false + WhatIfTest = $true + ValidationTest = $false + ValidateOrDeployParameters = @{ + Location = 'westeurope' + ResourceGroupName = 'validation-rg' + SubscriptionId = '00000000-0000-0000-0000-000000000000' + ManagementGroupId = '00000000-0000-0000-0000-000000000000' + RemoveDeployment = $false + } + AdditionalTokens = @{ + tenantId = '00000000-0000-0000-0000-000000000000' + } +} +Test-ModuleLocally @TestModuleLocallyInput -Verbose + +Get What-If deployment result using a test file with the provided tokens + .EXAMPLE $TestModuleLocallyInput = @{ TemplateFilePath = 'C:\network\route-table\main.bicep' PesterTest = $true DeploymentTest = $false + WhatIfTest = $false ValidationTest = $true ValidateOrDeployParameters = @{ Location = 'westeurope' @@ -155,7 +208,10 @@ function Test-ModuleLocally { [switch] $DeploymentTest, [Parameter(Mandatory = $false)] - [switch] $ValidationTest + [switch] $ValidationTest, + + [Parameter(Mandatory = $false)] + [switch] $WhatIfTest ) begin { @@ -168,6 +224,7 @@ function Test-ModuleLocally { # Load Modules Validation / Deployment Scripts . (Join-Path $utilitiesFolderPath 'pipelines' 'resourceDeployment' 'New-TemplateDeployment.ps1') . (Join-Path $utilitiesFolderPath 'pipelines' 'resourceDeployment' 'Test-TemplateDeployment.ps1') + . (Join-Path $utilitiesFolderPath 'pipelines' 'resourceDeployment' 'Get-TemplateDeploymenWhatIf.ps1') } process { @@ -247,7 +304,7 @@ function Test-ModuleLocally { # Validation & Deployment tests # ################################# - if (($ValidationTest -or $DeploymentTest) -and $ValidateOrDeployParameters) { + if (($ValidationTest -or $DeploymentTest -or $WhatIfTest) -and $ValidateOrDeployParameters) { # Invoke Token Replacement Functionality and Convert Tokens in Parameter Files $null = Convert-TokensInFileList @tokenConfiguration @@ -278,7 +335,20 @@ function Test-ModuleLocally { } } } - + # What-If validation for template + # ----------------- + if ($WhatIfTest) { + # Loop through test files + foreach ($moduleTestFile in $moduleTestFiles) { + Write-Verbose ('Validating module [{0}] with test file [{1}]' -f $ModuleName, (Split-Path $moduleTestFile -Leaf)) -Verbose + if ((Split-Path $moduleTestFile -Extension) -eq '.json') { + Get-TemplateDeploymenWhatIf @functionInput -ParameterFilePath $moduleTestFile + } else { + $functionInput['TemplateFilePath'] = $moduleTestFile + Get-TemplateDeploymenWhatIf @functionInput + } + } + } # Deploy template # --------------- if ($DeploymentTest) { From 3104c50d43493b942e521776cbd47e04b2caabab Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Tue, 14 Nov 2023 20:58:27 +1100 Subject: [PATCH 2/6] update --- modules/web/serverfarm/tests/e2e/max/main.test.bicep | 2 +- null | 0 .../resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 | 10 +++++----- 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 null diff --git a/modules/web/serverfarm/tests/e2e/max/main.test.bicep b/modules/web/serverfarm/tests/e2e/max/main.test.bicep index ab5b234c99..577e330f17 100644 --- a/modules/web/serverfarm/tests/e2e/max/main.test.bicep +++ b/modules/web/serverfarm/tests/e2e/max/main.test.bicep @@ -21,7 +21,7 @@ param serviceShort string = 'wsfmax' param enableDefaultTelemetry bool = true @description('Optional. A token to inject into the name of each resource.') -param namePrefix string = '[[namePrefix]]' +param namePrefix string = 'ttete' // ============ // // Dependencies // diff --git a/null b/null new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 b/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 index cdb3db31e3..5e6e3ec0f6 100644 --- a/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 +++ b/utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1 @@ -120,7 +120,7 @@ function Get-TemplateDeploymenWhatIf { } while ($deploymentName -notmatch '^[-\w\._\(\)]+$') if ($deploymentScope -ne 'resourceGroup') { - Write-Verbose "Testing with deployment name [$deploymentName]" -Verbose + Write-Verbose "What-If Deployment Test with deployment name [$deploymentName]" -Verbose $DeploymentInputs['DeploymentName'] = $deploymentName } @@ -138,7 +138,7 @@ function Get-TemplateDeploymenWhatIf { $null = New-AzResourceGroup -Name $resourceGroupName -Location $location } } - if ($PSCmdlet.ShouldProcess('Resource group level deployment', 'Test')) { + if ($PSCmdlet.ShouldProcess('Resource group level deployment', 'WhatIf')) { $res = New-AzResourceGroupDeployment @DeploymentInputs -WhatIf } break @@ -148,20 +148,20 @@ function Get-TemplateDeploymenWhatIf { Write-Verbose ('Setting context to subscription [{0}]' -f $subscriptionId) $null = Set-AzContext -Subscription $subscriptionId } - if ($PSCmdlet.ShouldProcess('Subscription level deployment', 'Test')) { + if ($PSCmdlet.ShouldProcess('Subscription level deployment', 'WhatIf')) { $res = New-AzDeployment @DeploymentInputs -Location $Location -WhatIf } break } 'managementGroup' { - if ($PSCmdlet.ShouldProcess('Management group level deployment', 'Test')) { + if ($PSCmdlet.ShouldProcess('Management group level deployment', 'WhatIf')) { $res = New-AzManagementGroupDeployment @DeploymentInputs -Location $Location -ManagementGroupId $ManagementGroupId -WhatIf } break } 'tenant' { Write-Verbose 'Handling tenant level validation' - if ($PSCmdlet.ShouldProcess('Tenant level deployment', 'Test')) { + if ($PSCmdlet.ShouldProcess('Tenant level deployment', 'WhatIf')) { $res = New-AzTenantDeployment @DeploymentInputs -Location $location -WhatIf } break From bff30222efee43ae83da5a432aacf138e828c31c Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Tue, 14 Nov 2023 20:58:53 +1100 Subject: [PATCH 3/6] update --- null | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 null diff --git a/null b/null deleted file mode 100644 index e69de29bb2..0000000000 From 311df3ea56c0e4192bf59b17d316e2e58370e906 Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Tue, 14 Nov 2023 21:00:20 +1100 Subject: [PATCH 4/6] update --- utilities/tools/Test-ModuleLocally.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/tools/Test-ModuleLocally.ps1 b/utilities/tools/Test-ModuleLocally.ps1 index 6fd8690dd2..1304221d66 100644 --- a/utilities/tools/Test-ModuleLocally.ps1 +++ b/utilities/tools/Test-ModuleLocally.ps1 @@ -340,7 +340,7 @@ function Test-ModuleLocally { if ($WhatIfTest) { # Loop through test files foreach ($moduleTestFile in $moduleTestFiles) { - Write-Verbose ('Validating module [{0}] with test file [{1}]' -f $ModuleName, (Split-Path $moduleTestFile -Leaf)) -Verbose + Write-Verbose ('Get Deployment What-If result for module [{0}] with test file [{1}]' -f $ModuleName, (Split-Path $moduleTestFile -Leaf)) -Verbose if ((Split-Path $moduleTestFile -Extension) -eq '.json') { Get-TemplateDeploymenWhatIf @functionInput -ParameterFilePath $moduleTestFile } else { From eace03cb0bb28340444ac765ebf4278bf4029cd3 Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Tue, 14 Nov 2023 21:04:49 +1100 Subject: [PATCH 5/6] update --- utilities/tools/Test-ModuleLocally.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/tools/Test-ModuleLocally.ps1 b/utilities/tools/Test-ModuleLocally.ps1 index 1304221d66..2147c66219 100644 --- a/utilities/tools/Test-ModuleLocally.ps1 +++ b/utilities/tools/Test-ModuleLocally.ps1 @@ -180,7 +180,7 @@ Run all Pester tests for the given template file including tests for the use of .NOTES - Make sure you provide the right information in the 'ValidateOrDeployParameters' parameter for this function to work. -- Ensure you have the ability to perform the deployment operations using your account (if planning to test deploy) +- Ensure you have the ability to perform the deployment operations using your account (if planning to test deploy or performing what-if validation.) #> function Test-ModuleLocally { From 94a188809f8d58e2e35828609f264c079a761a8b Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Tue, 14 Nov 2023 10:33:26 +0000 Subject: [PATCH 6/6] Update modules/web/serverfarm/tests/e2e/max/main.test.bicep Co-authored-by: Alexander Sehr --- modules/web/serverfarm/tests/e2e/max/main.test.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/web/serverfarm/tests/e2e/max/main.test.bicep b/modules/web/serverfarm/tests/e2e/max/main.test.bicep index 577e330f17..ab5b234c99 100644 --- a/modules/web/serverfarm/tests/e2e/max/main.test.bicep +++ b/modules/web/serverfarm/tests/e2e/max/main.test.bicep @@ -21,7 +21,7 @@ param serviceShort string = 'wsfmax' param enableDefaultTelemetry bool = true @description('Optional. A token to inject into the name of each resource.') -param namePrefix string = 'ttete' +param namePrefix string = '[[namePrefix]]' // ============ // // Dependencies //