diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index 896242f4..71665a48 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -30,12 +30,6 @@ $null = $modulesToInstall.Add(([PSCustomObject]@{ ModuleVersion = '0.12.0' })) -# Required dependency of the ALZ module itself. -$null = $modulesToInstall.Add(([PSCustomObject]@{ - ModuleName = 'Az.Resources' - ModuleVersion = '6.5.2' - })) - 'Installing PowerShell Modules' foreach ($module in $modulesToInstall) { $installSplat = @{ diff --git a/src/ALZ/ALZ.psd1 b/src/ALZ/ALZ.psd1 index 926df639..83cc0f16 100644 --- a/src/ALZ/ALZ.psd1 +++ b/src/ALZ/ALZ.psd1 @@ -51,12 +51,7 @@ # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @( - @{ - ModuleName = 'Az.Resources' - ModuleVersion = '6.5.2' - } - ) + RequiredModules = @() # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() diff --git a/src/ALZ/Assets/alz-bicep-config/v0.14.0-pre.config.json b/src/ALZ/Assets/alz-bicep-config/v0.14.0-pre.config.json index 3d83ef3b..074e3b97 100644 --- a/src/ALZ/Assets/alz-bicep-config/v0.14.0-pre.config.json +++ b/src/ALZ/Assets/alz-bicep-config/v0.14.0-pre.config.json @@ -133,19 +133,19 @@ "Description": "The prefix that will be added to all resources created by this deployment. (e.g. 'alz')", "Targets": [ { - "Name": "parTopLevelManagementGroupPrefix", + "Name": "parTopLevelManagementGroupPrefix.value", "Destination": "Parameters" }, { - "Name": "parCompanyPrefix", + "Name": "parCompanyPrefix.value", "Destination": "Parameters" }, { - "Name": "parTargetManagementGroupId", + "Name": "parTargetManagementGroupId.value", "Destination": "Parameters" }, { - "Name": "parAssignableScopeManagementGroupId", + "Name": "parAssignableScopeManagementGroupId.value", "Destination": "Parameters" }, { @@ -162,7 +162,7 @@ "Description": "The suffix that will be added to all resources created by this deployment. (e.g. 'test')", "Targets": [ { - "Name": "parTopLevelManagementGroupSuffix", + "Name": "parTopLevelManagementGroupSuffix.value", "Destination": "Parameters" } ], @@ -176,15 +176,23 @@ "Value": "", "Targets": [ { - "Name": "parLocation", + "Name": "parLocation.value", "Destination": "Parameters" }, { - "Name": "parAutomationAccountLocation", + "Name": "parAutomationAccountLocation.value", "Destination": "Parameters" }, { - "Name": "parLogAnalyticsWorkspaceLocation", + "Name": "parLogAnalyticsWorkspaceLocation.value", + "Destination": "Parameters" + }, + { + "Name": "parPolicyAssignmentParameters.value.ascExportResourceGroupLocation.value", + "Destination": "Parameters" + }, + { + "Name": "parVirtualWanHubs.value.[0].parHubLocation", "Destination": "Parameters" }, { @@ -194,23 +202,74 @@ ], "AllowedValues": { "Display": false, - "Description": "Getting Azure deployment locations.", - "Type": "PSScript", - "Script": "Get-AzLocation | Where-Object {$_.RegionType -eq 'Physical'} | Sort-Object Location | Select-Object -ExpandProperty Location", - "Values": [] + "Values": [ + "australiacentral", + "australiacentral2", + "australiaeast", + "australiasoutheast", + "brazilsouth", + "brazilsoutheast", + "canadacentral", + "canadaeast", + "centralindia", + "centralus", + "centraluseuap", + "eastasia", + "eastus", + "eastus2", + "eastus2euap", + "eastusstg", + "francecentral", + "francesouth", + "germanynorth", + "germanywestcentral", + "japaneast", + "japanwest", + "jioindiacentral", + "jioindiawest", + "koreacentral", + "koreasouth", + "northcentralus", + "northeurope", + "norwayeast", + "norwaywest", + "qatarcentral", + "southafricanorth", + "southafricawest", + "southcentralus", + "southeastasia", + "southindia", + "swedencentral", + "switzerlandnorth", + "switzerlandwest", + "uaecentral", + "uaenorth", + "uksouth", + "ukwest", + "westcentralus", + "westeurope", + "westindia", + "westus", + "westus2", + "westus3" + ] } }, "Environment": { "Type": "UserInput", - "Description": "The Type of environment that will be created. (e.g. 'dev', 'test', 'qa', 'staging', 'prod')", + "Description": "The Type of environment that will be created. (e.g. 'live', 'canary')", "Targets": [ { - "Name": "parEnvironment", + "Name": "parEnvironment.value", + "Destination": "Parameters" + }, + { + "Name": "parTags.value.Environment", "Destination": "Parameters" } ], "Value": "", - "DefaultValue": "prod", + "DefaultValue": "live", "Valid": "^[a-zA-Z0-9]{2,10}$" }, "IdentitySubscriptionId": { @@ -218,8 +277,8 @@ "Description": "The identifier of the Identity Subscription. (e.g '00000000-0000-0000-0000-000000000000')", "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$", "Targets": [ - { - "Name": "IdentitySubscriptionId", + { + "Name": "IDENTITY_SUBSCRIPTION_ID", "Destination": "Environment" } ], @@ -230,10 +289,6 @@ "Description": "The identifier of the Connectivity Subscription. (e.g '00000000-0000-0000-0000-000000000000')", "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$", "Targets": [ - { - "Name": "ConnectivitySubscriptionId", - "Destination": "Environment" - }, { "Name": "CONNECTIVITY_SUBSCRIPTION_ID", "Destination": "Environment" @@ -247,22 +302,22 @@ "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$", "Targets": [ { - "Name": "ManagementSubscriptionId", + "Name": "MANAGEMENT_SUBSCRIPTION_ID", "Destination": "Environment" } ], "Value": "" }, - "BillingAccountId": { - "Type": "UserInput", - "Description": "The identifier of the Billing Account. (e.g 00000000-0000-0000-0000-000000000000)", - "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$", - "Value": "" - }, - "EnrollmentAccountId": { + "SecurityContact": { "Type": "UserInput", - "Description": "The identifier of the Enrollment Account. (e.g 00000000-0000-0000-0000-000000000000)", - "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$", + "Description": "The email address of the contact for security issues. (e.g. security@contactme.com)", + "Valid": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "Targets": [ + { + "Name":"parPolicyAssignmentParameters.value.emailSecurityContact.value", + "Destination": "Parameters" + } + ], "Value": "" }, "LogAnalyticsResourceId": { @@ -270,7 +325,91 @@ "Value": "/subscriptions/{%ManagementSubscriptionId%}/resourcegroups/alz-logging/providers/microsoft.operationalinsights/workspaces/alz-log-analytics", "Targets": [ { - "Name": "parLogAnalyticsWorkspaceResourceId", + "Name": "parLogAnalyticsWorkspaceResourceId.value", + "Destination": "Parameters" + } + ] + }, + "AllSubscriptionIds": { + "Type": "Computed", + "Value": [ + "{%ManagementSubscriptionId%}", + "{%ConnectivitySubscriptionId%}", + "{%IdentitySubscriptionId%}" + ], + "Targets": [ + { + "Name": "parSubscriptionIds.value", + "Destination": "Parameters" + } + ] + }, + "VirtualIdToLink": { + "Type": "Computed", + "Value": "", + "Targets": [ + { + "Name": "parVirtualNetworkIdToLink.value", + "Destination": "Parameters" + } + ] + }, + "VirtualWanName":{ + "Type": "Computed", + "Value": "alz-vwan-{%Location%}", + "Targets": [ + { + "Name": "parVirtualWanName.value", + "Destination": "Parameters" + } + ] + }, + "FirewallPoliciesName":{ + "Type": "Computed", + "Value": "alz-azfwpolicy-{%Location%}", + "Targets": [ + { + "Name": "parAzFirewallPoliciesName.value", + "Destination": "Parameters" + } + ] + }, + "AK8sPrivateLink": { + "Type": "Computed", + "Value": "privatelink.{%Location%}.azmk8s.io", + "Targets": [ + { + "Name": "parPrivateDnsZones.value.[0]", + "Destination": "Parameters" + } + ] + }, + "BatchPrivateLink": { + "Type": "Computed", + "Value": "privatelink.{%Location%}.batch.azure.com", + "Targets": [ + { + "Name": "parPrivateDnsZones.value.[1]", + "Destination": "Parameters" + } + ] + }, + "KustoPrivateLink": { + "Type": "Computed", + "Value": "privatelink.{%Location%}.kusto.windows.net", + "Targets": [ + { + "Name": "parPrivateDnsZones.value.[2]", + "Destination": "Parameters" + } + ] + }, + "BackupPrivateLink": { + "Type": "Computed", + "Value": "privatelink.{%Location%}.backup.windowsazure.com", + "Targets": [ + { + "Name": "parPrivateDnsZones.value.[3]", "Destination": "Parameters" } ] diff --git a/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 b/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 index 625cdd27..5f64ca6e 100644 --- a/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 +++ b/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 @@ -11,25 +11,70 @@ function Edit-ALZConfigurationFilesInPlace { [object] $configuration ) - $locations = @("orchestration", "config", "customization") + $locations = @("config") $files = @() foreach ($location in $locations) { $bicepModules = Join-Path $alzEnvironmentDestination $location - $files += @(Get-ChildItem -Path $bicepModules -Recurse -Filter *.parameters.json) + $files += @(Get-ChildItem -Path $bicepModules -Recurse -Filter *.parameters.*.json) } foreach ($file in $files) { $bicepConfiguration = Get-Content $file.FullName | ConvertFrom-Json -AsHashtable $modified = $false + foreach ($configKey in $configuration.PsObject.Properties) { foreach ($target in $configKey.Value.Targets) { - if ($target.Destination -eq "Parameters" -and $null -ne $bicepConfiguration.parameters[$target.Name]) { + + # Find the appropriate item which will be changed in the Bicep file. + # Remove array '[' ']' characters so we can use the index value direct. + $propertyNames = $target.Name -replace "\[|\]","" -split "\." + $bicepConfigNode = $bicepConfiguration.parameters + $index = 0 + + # Keep navigating into properties which the configuration specifies until we reach the bottom most object, + # e.g. not a value type - but the object reference so the value is persisted. + do { + if ($bicepConfigNode -is [array]) { + # If this is an array - use the property as an array index... + if ($propertyNames[$index] -match "[0-9]+" -eq $false) { + throw "Configuration specifies an array, but the index value '${$propertyNames[$index]}' is not a number" + } + + $bicepConfigNode = $bicepConfigNode[$propertyNames[$index]] + + } elseif ($bicepConfigNode.ContainsKey($propertyNames[$index]) -eq $true) { + # We found the item, keep indexing into the object. + $bicepConfigNode = $bicepConfigNode[$propertyNames[$index]] + } else { + # This property doesn't exist at this level in the hierarchy, + # this isn't the property we're looking for, stop looking. + $bicepConfigNode = $null + } + + ++$index + + } while (($null -ne $bicepConfigNode) -and ($index -lt $propertyNames.Length - 1)) + + # If we're here, we've got the object at the bottom of the hierarchy - and we can modify values on it. + if ($target.Destination -eq "Parameters" -and $null -ne $bicepConfigNode) { + $leafPropertyName = $propertyNames[-1] + if ($configKey.Value.Type -eq "Computed") { - $bicepConfiguration.parameters[$target.Name].value = Format-TokenizedConfigurationString $configKey.Value.Value $configuration + # If the value type is computed we replace the value with another which already exists in the configuration hierarchy. + if ($configKey.Value.Value -is [array]) { + $formattedValues = @() + foreach($formatString in $configKey.Value.Value) { + $formattedValues += Format-TokenizedConfigurationString -tokenizedString $formatString -configuration $configuration + } + $bicepConfigNode[$leafPropertyName] = $formattedValues + } else { + $bicepConfigNode[$leafPropertyName] = Format-TokenizedConfigurationString -tokenizedString $configKey.Value.Value -configuration $configuration + } } else { - $bicepConfiguration.parameters[$target.Name].value = $configKey.Value.Value + $bicepConfigNode[$leafPropertyName] = $configKey.Value.Value } + $modified = $true } } diff --git a/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 b/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 index bd905822..1755d8b8 100644 --- a/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 +++ b/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 @@ -1,5 +1,6 @@ function Format-TokenizedConfigurationString { param( + [AllowEmptyString()] [Parameter(Mandatory = $true)] [string] $tokenizedString, diff --git a/src/ALZ/Private/Write-InformationColored.ps1 b/src/ALZ/Private/Write-InformationColored.ps1 index eb72895f..3979d0e2 100644 --- a/src/ALZ/Private/Write-InformationColored.ps1 +++ b/src/ALZ/Private/Write-InformationColored.ps1 @@ -3,8 +3,8 @@ function Write-InformationColored { param( [Parameter(Mandatory)] [Object]$MessageData, - [ConsoleColor]$ForegroundColor = $Host.UI.RawUI.ForegroundColor, # Make sure we use the current colours by default - [ConsoleColor]$BackgroundColor = $Host.UI.RawUI.BackgroundColor, + [ConsoleColor]$ForegroundColor = $Host.UI.RawUI.ForegroundColor ?? "White", # Make sure we use the current colours by default + [ConsoleColor]$BackgroundColor = $Host.UI.RawUI.BackgroundColor ?? "Black", [Switch]$NoNewline ) diff --git a/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 b/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 index 210dc34d..f1234264 100644 --- a/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 +++ b/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 @@ -13,85 +13,52 @@ Import-Module $PathToManifest -Force InModuleScope 'ALZ' { BeforeAll { - $defaultConfig = [pscustomobject]@{ - Prefix = [pscustomobject]@{ - Description = "The prefix that will be added to all resources created by this deployment." - Targets = @( - [pscustomobject]@{ - Name = "parTopLevelManagementGroupPrefix" - Destination = "Parameters" - }, - [pscustomobject]@{ - Name = "parCompanyPrefix" - Destination = "Parameters" - }) - Value = "test" - DefaultValue = "alz" - } - Suffix = [pscustomobject]@{ - Description = "The suffix that will be added to all resources created by this deployment." - Targets = @( - [pscustomobject]@{ - Name = "parTopLevelManagementGroupSuffix" - Destination = "Parameters" - }) - Value = "bla" - DefaultValue = "" - } - Location = [pscustomobject]@{ - Description = "Deployment location." - Targets = @( - [pscustomobject]@{ - Name = "parLocation" - Destination = "Parameters" - }) - AllowedValues = @('ukwest', '') - Value = "eastus" - } - Environment = [pscustomobject]@{ - Description = "The type of environment that will be created . Example: dev, test, qa, staging, prod" - Targets = @( - [pscustomobject]@{ - Name = "parEnvironment" - Destination = "Parameters" - }) - DefaultValue = 'prod' - Value = "dev" - } - Logging = [pscustomobject]@{ - Type = "Computed" - Description = "The type of environment that will be created . Example: dev, test, qa, staging, prod" - Value = "logs/{%Environment%}/{%Location%}" - Targets = @( - [pscustomobject]@{ - Name = "parLogging" - Destination = "Parameters" - }) - } + $testFile1Name = "test.parameters.all.json" + + Mock -CommandName Out-File -MockWith { + Write-InformationColored "Out-File was called with $FilePath and $InputObject" -ForegroundColor Yellow -InformationAction Continue } - $firstFileContent = '{ - "parameters": { - "parCompanyPrefix": { - "value": "" - }, - "parTopLevelManagementGroupPrefix": { - "value": "" - }, - "parLogging" : { - "value": "" + + Mock -CommandName Get-ChildItem -ParameterFilter { $Path -match 'config$' } -MockWith { + @( + [PSCustomObject]@{ + FullName = $testFile1Name } - } - }' - $secondFileContent = '{ - "parameters": { - "parTopLevelManagementGroupSuffix": { - "value": "" - }, - "parLocation": { - "value": "" + ) + } + + function Initialize-TestConfiguration { + param( + [Parameter(Mandatory = $true)] + [string]$configTarget, + + [Parameter(Mandatory = $true)] + [string]$withValue + ) + + return [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Value = $withValue + Targets = @( + [pscustomobject]@{ + Name = $configTarget + Destination = "Parameters" + }) + } } - } - }' + } + + function Format-ExpectedResult { + param( + [Parameter(Mandatory = $true)] + [string]$expectedJson + ) + + # Get the formatting correct by using the same JSON formatter. + return ConvertFrom-Json -InputObject $expectedJson -AsHashtable | ConvertTo-Json -Depth 10 + } } Describe 'Edit-ALZConfigurationFilesInPlace Function Tests' -Tag Unit { BeforeAll { @@ -99,8 +66,383 @@ InModuleScope 'ALZ' { $ErrorActionPreference = 'SilentlyContinue' } Context 'Edit-ALZConfigurationFilesInPlace should replace the parameters correctly' { - BeforeEach { - Mock -CommandName Get-ChildItem -ParameterFilter { $Path -match 'orchestration$' } -MockWith { + + It 'Should replace array values correctly (JSON Object) - first' { + $config = Initialize-TestConfiguration -configTarget "parValue.value.[0]" -withValue "value" + + $fileContent = '{ + "parameters": { + "parValue": { + "value": [ + "replace_me", + "dont_replace_me" + ] + } + } + }' + + $expectedContent = '{ + "parameters": { + "parValue": { + "value": [ + "value", + "dont_replace_me" + ] + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It 'Should replace array an entire array correctly (JSON Object)' { + $config = Initialize-TestConfiguration -configTarget "parValue.value.[1]" -withValue "value" + + $fileContent = '{ + "parameters": { + "parValue": { + "value": [ + "dont_replace_me", + "replace_me" + ] + } + } + }' + + $expectedContent = '{ + "parameters": { + "parValue": { + "value": [ + "dont_replace_me", + "value" + ] + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It 'Should replace array values correctly (JSON Object) - second' { + + $config = [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Value = @( + "1", + "2", + "3" + ) + Targets = @( + [pscustomobject]@{ + Name = "parValue.value" + Destination = "Parameters" + }) + } + } + + $fileContent = '{ + "parameters": { + "parValue": { + "value": ["replace_me"] + } + } + }' + + $expectedContent = '{ + "parameters": { + "parValue": { + "value": ["1", "2", "3"] + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It 'Should not write to files that havent been changed.' { + $config = Initialize-TestConfiguration -configTarget "DoesnotExist.value" -withValue "value" + + $fileContent = '{ + "parameters": { + "parValue": { + "value": "replace_me" + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -Scope It ` + -Times 0 -Exactly + } + + It 'Should replace simple values correctly (Bicep Object)' { + $config = Initialize-TestConfiguration -configTarget "parValue.value" -withValue "value" + + $fileContent = '{ + "parameters": { + "parValue": { + "value": "replace_me" + } + } + }' + + $expectedContent = '{ + "parameters": { + "parValue": { + "value": "value" + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It "Should replace 'Parameter' destinations to nested array objects correctly" { + $config = Initialize-TestConfiguration -configTarget "parNested.value.[0].parChildValue.value" -withValue "nested" + + $fileContent = '{ + "parameters": { + "parNested": { + "value": [{ + "parChildValue": { + "value": "replace_me" + } + }] + } + } + }' + + $expectedContent = '{ + "parameters": { + "parNested": { + "value": [{ + "parChildValue": { + "value": "nested" + } + }] + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It 'Should replace nested values correctly (Plain JSON Object)' { + $config = Initialize-TestConfiguration -configTarget "parNested.value.parChildValue" -withValue "nested" + + $fileContent = '{ + "parameters": { + "parNested": { + "value": { + "parChildValue": "replace_me" + } + } + } + }' + + $expectedContent = '{ + "parameters": { + "parNested": { + "value": { + "parChildValue": "nested" + } + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It 'Should replace nested values correctly (Bicep Object)' { + $config = Initialize-TestConfiguration -configTarget "parNested.value.parChildValue.value" -withValue "nested" + + $fileContent = '{ + "parameters": { + "parNested": { + "value": { + "parChildValue": { + "value": "replace_me" + } + } + } + } + }' + + $expectedContent = '{ + "parameters": { + "parNested": { + "value": { + "parChildValue": { + "value": "nested" + } + } + } + } + }' + + Mock -CommandName Get-Content -ParameterFilter { $Path -eq $testFile1Name } -MockWith { + $fileContent + } + + $expectedContent = Format-ExpectedResult -expectedJson $expectedContent + + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $config + + Should -Invoke -CommandName Out-File ` + -ParameterFilter { $FilePath -eq $testFile1Name -and $InputObject -eq $expectedContent } ` + -Scope It + } + + It 'Multiple files with multiple values should be changed correctly' { + $defaultConfig = [pscustomobject]@{ + Prefix = [pscustomobject]@{ + Description = "The prefix that will be added to all resources created by this deployment." + Targets = @( + [pscustomobject]@{ + Name = "parTopLevelManagementGroupPrefix.value" + Destination = "Parameters" + }, + [pscustomobject]@{ + Name = "parCompanyPrefix.value" + Destination = "Parameters" + }) + Value = "test" + DefaultValue = "alz" + } + Suffix = [pscustomobject]@{ + Description = "The suffix that will be added to all resources created by this deployment." + Targets = @( + [pscustomobject]@{ + Name = "parTopLevelManagementGroupSuffix.value" + Destination = "Parameters" + }) + Value = "bla" + DefaultValue = "" + } + Location = [pscustomobject]@{ + Description = "Deployment location." + Targets = @( + [pscustomobject]@{ + Name = "parLocation.value" + Destination = "Parameters" + }) + AllowedValues = @('ukwest', '') + Value = "eastus" + } + Environment = [pscustomobject]@{ + Description = "The type of environment that will be created . Example: dev, test, qa, staging, prod" + Targets = @( + [pscustomobject]@{ + Name = "parEnvironment.value" + Destination = "Parameters" + }) + DefaultValue = 'prod' + Value = "dev" + } + Logging = [pscustomobject]@{ + Type = "Computed" + Description = "The type of environment that will be created . Example: dev, test, qa, staging, prod" + Value = "logs/{%Environment%}/{%Location%}" + Targets = @( + [pscustomobject]@{ + Name = "parLogging.value" + Destination = "Parameters" + }) + } + } + $firstFileContent = '{ + "parameters": { + "parCompanyPrefix": { + "value": "" + }, + "parTopLevelManagementGroupPrefix": { + "value": "" + }, + "parLogging" : { + "value": "" + } + } + }' + $secondFileContent = '{ + "parameters": { + "parTopLevelManagementGroupSuffix": { + "value": "" + }, + "parLocation": { + "value": "" + } + } + }' + + Mock -CommandName Get-ChildItem -ParameterFilter { $Path -match 'config$' } -MockWith { @( [PSCustomObject]@{ FullName = 'test1.parameters.json' @@ -117,11 +459,6 @@ InModuleScope 'ALZ' { $secondFileContent } - Mock -CommandName Out-File -MockWith { - Write-InformationColored "Out-File was called with $FilePath and $InputObject" -ForegroundColor Yellow -InformationAction Continue - } - } - It 'Files should be changed correctly' { Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $defaultConfig Should -Invoke -CommandName Out-File -Scope It -Times 2 @@ -131,6 +468,7 @@ InModuleScope 'ALZ' { $contentAfterParsing.parameters.parTopLevelManagementGroupPrefix.value = 'test' $contentAfterParsing.parameters.parCompanyPrefix.value = 'test' $contentAfterParsing.parameters.parLogging.value = "logs/dev/eastus" + $contentStringAfterParsing = ConvertTo-Json -InputObject $contentAfterParsing Write-InformationColored $contentStringAfterParsing -ForegroundColor Yellow -InformationAction Continue Should -Invoke -CommandName Out-File -ParameterFilter { $FilePath -eq "test1.parameters.json" -and $InputObject -eq $contentStringAfterParsing } -Scope It @@ -138,6 +476,7 @@ InModuleScope 'ALZ' { $contentAfterParsing = ConvertFrom-Json -InputObject $secondFileContent -AsHashtable $contentAfterParsing.parameters.parTopLevelManagementGroupSuffix.value = 'bla' $contentAfterParsing.parameters.parLocation.value = 'eastus' + $contentStringAfterParsing = ConvertTo-Json -InputObject $contentAfterParsing Write-InformationColored $contentStringAfterParsing -ForegroundColor Yellow -InformationAction Continue Should -Invoke -CommandName Out-File -ParameterFilter { $FilePath -eq "test2.parameters.json" -and $InputObject -eq $contentStringAfterParsing } -Scope It