Skip to content

Commit

Permalink
[New Feature] Add WhatIf feature to Test module locally script (#4241)
Browse files Browse the repository at this point in the history
* Add WhatIf feature to Test module locally script

* update

* update

* update

* update

* Update modules/web/serverfarm/tests/e2e/max/main.test.bicep

Co-authored-by: Alexander Sehr <[email protected]>

---------

Co-authored-by: Alexander Sehr <[email protected]>
  • Loading branch information
tyconsulting and AlexanderSehr authored Nov 28, 2023
1 parent 11b702b commit 3b5965b
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 4 deletions.
185 changes: 185 additions & 0 deletions utilities/pipelines/resourceDeployment/Get-TemplateDeploymenWhatIf.ps1
Original file line number Diff line number Diff line change
@@ -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 "What-If Deployment Test 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', 'WhatIf')) {
$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', 'WhatIf')) {
$res = New-AzDeployment @DeploymentInputs -Location $Location -WhatIf
}
break
}
'managementGroup' {
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', 'WhatIf')) {
$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)
}
}
78 changes: 74 additions & 4 deletions utilities/tools/Test-ModuleLocally.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -37,6 +40,7 @@ $TestModuleLocallyInput = @{
ModuleTestFilePath = 'C:\network\route-table\.test\parameters.json'
PesterTest = $false
DeploymentTest = $false
WhatIfTest = $false
ValidationTest = $true
ValidateOrDeployParameters = @{
Location = 'westeurope'
Expand All @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -127,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 {

Expand Down Expand Up @@ -155,7 +208,10 @@ function Test-ModuleLocally {
[switch] $DeploymentTest,

[Parameter(Mandatory = $false)]
[switch] $ValidationTest
[switch] $ValidationTest,

[Parameter(Mandatory = $false)]
[switch] $WhatIfTest
)

begin {
Expand All @@ -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 {

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -278,7 +335,20 @@ function Test-ModuleLocally {
}
}
}

# What-If validation for template
# -----------------
if ($WhatIfTest) {
# Loop through test files
foreach ($moduleTestFile in $moduleTestFiles) {
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 {
$functionInput['TemplateFilePath'] = $moduleTestFile
Get-TemplateDeploymenWhatIf @functionInput
}
}
}
# Deploy template
# ---------------
if ($DeploymentTest) {
Expand Down

0 comments on commit 3b5965b

Please sign in to comment.