diff --git a/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 b/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 index b74def46..40f5a695 100644 --- a/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 +++ b/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 @@ -26,10 +26,6 @@ function Build-ALZDeploymentEnvFile { if ($target.Destination -eq "Environment") { $formattedValue = $configurationValue.Value.Value - if ($configurationValue.Value.Type -eq "Computed") { - $formattedValue = Format-TokenizedConfigurationString -tokenizedString $configurationValue.Value.Value -configuration $configuration - } - Add-Content -Path $envFile -Value "$($($target.Name))=`"$formattedValue`"" | Out-String | Write-Verbose } } diff --git a/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 b/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 index c5a16951..3e7f834b 100644 --- a/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 +++ b/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 @@ -64,37 +64,7 @@ function Edit-ALZConfigurationFilesInPlace { # If we're here, we can modify this file and we've got an actual object specified by the Name path value - 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") { - # 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 - } - - if ($null -ne $configKey.Value.Process) { - $scriptBlock = [ScriptBlock]::Create($configKey.Value.Process) - $formattedValues = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $formattedValues - $formattedValues = @($formattedValues) - } - - $bicepConfigNode[$leafPropertyName] = $formattedValues - } else { - - $formattedValue = Format-TokenizedConfigurationString -tokenizedString $configKey.Value.Value -configuration $configuration - - if ($null -ne $configKey.Value.Process) { - $scriptBlock = [ScriptBlock]::Create($configKey.Value.Process) - $formattedValue = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $formattedValue - } - - $bicepConfigNode[$leafPropertyName] = $formattedValue - } - } else { - $bicepConfigNode[$leafPropertyName] = $configKey.Value.Value - } - + $bicepConfigNode[$leafPropertyName] = $configKey.Value.Value $modified = $true } } diff --git a/src/ALZ/Private/New-ALZDirectoryEnvironment.ps1 b/src/ALZ/Private/New-ALZDirectoryEnvironment.ps1 index e08ee11f..e074449e 100644 --- a/src/ALZ/Private/New-ALZDirectoryEnvironment.ps1 +++ b/src/ALZ/Private/New-ALZDirectoryEnvironment.ps1 @@ -18,5 +18,4 @@ function New-ALZDirectoryEnvironment { New-Item -ItemType Directory -Path $config -Force | Out-String | Write-Verbose New-Item -ItemType Directory -Path $upstream -Force | Out-String | Write-Verbose New-Item -ItemType Directory -Path $configModules -Force | Out-String | Write-Verbose - } \ No newline at end of file diff --git a/src/ALZ/Private/Set-ComputedConfiguration.ps1 b/src/ALZ/Private/Set-ComputedConfiguration.ps1 new file mode 100644 index 00000000..1dbadf9c --- /dev/null +++ b/src/ALZ/Private/Set-ComputedConfiguration.ps1 @@ -0,0 +1,41 @@ +# If the value type is computed we replace the value with another which already exists in the configuration hierarchy. +function Set-ComputedConfiguration { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [PSCustomObject] $configuration + ) + + if ($PSCmdlet.ShouldProcess("ALZ-Bicep computed configuration.", "calculate computed values")) { + foreach ($configKey in $configuration.PsObject.Properties) { + if ($configKey.Value.Type -ne "Computed") { + continue; + } + + if ($configKey.Value.Value -is [array]) { + $formattedValues = @() + foreach($formatString in $configKey.Value.Value) { + $formattedValues += Format-TokenizedConfigurationString -tokenizedString $formatString -configuration $configuration + } + + if ($null -ne $configKey.Value.Process) { + $scriptBlock = [ScriptBlock]::Create($configKey.Value.Process) + $formattedValues = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $formattedValues + $formattedValues = @($formattedValues) + } + + $configKey.Value.Value = $formattedValues + } else { + + $formattedValue = Format-TokenizedConfigurationString -tokenizedString $configKey.Value.Value -configuration $configuration + + if ($null -ne $configKey.Value.Process) { + $scriptBlock = [ScriptBlock]::Create($configKey.Value.Process) + $formattedValue = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $formattedValue + } + + $configKey.Value.Value = $formattedValue + } + } + } +} \ No newline at end of file diff --git a/src/ALZ/Public/New-ALZEnvironment.ps1 b/src/ALZ/Public/New-ALZEnvironment.ps1 index c6aa44e2..7159536a 100644 --- a/src/ALZ/Public/New-ALZEnvironment.ps1 +++ b/src/ALZ/Public/New-ALZEnvironment.ps1 @@ -54,13 +54,14 @@ function New-ALZEnvironment { $alzEnvironmentDestinationInternalCode = Join-Path $alzEnvironmentDestination "upstream-releases" Get-ALZGithubRelease -directoryForReleases $alzEnvironmentDestinationInternalCode -githubRepoUrl $bicepConfig.module_url -releases @($bicepConfig.version) | Out-String | Write-Verbose - + Write-InformationColored "Copying ALZ-Bicep module to $alzEnvironmentDestinationInternalCode" -ForegroundColor Green -InformationAction Continue Copy-ALZParametersFile -alzEnvironmentDestination $alzEnvironmentDestination -upstreamReleaseDirectory $(Join-Path $alzEnvironmentDestinationInternalCode $bicepConfig.version) -configFiles $bicepConfig.config_files | Out-String | Write-Verbose Write-InformationColored "ALZ-Bicep source directory: $alzBicepSourceDirectory" -ForegroundColor Green -InformationAction Continue $configuration = Request-ALZEnvironmentConfig -configurationParameters $bicepConfig.parameters + Set-ComputedConfiguration -configuration $configuration | Out-String | Write-Verbose Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination $alzEnvironmentDestination -configuration $configuration | Out-String | Write-Verbose Build-ALZDeploymentEnvFile -configuration $configuration -Destination $alzEnvironmentDestination | Out-String | Write-Verbose diff --git a/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 b/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 index a344ccee..4d9cc9e3 100644 --- a/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 +++ b/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 @@ -77,38 +77,6 @@ InModuleScope 'ALZ' { Should -Invoke New-Item -ParameterFilter { $Path -match ".env$" } -Scope It -Times 1 -Exactly Should -Invoke Add-Content -Scope It -Times 1 -Exactly } - - It 'Handles Computed values correctly and adds to the .env file.' { - - Mock -CommandName New-Item - Mock -CommandName Add-Content - - $configuration = [pscustomobject]@{ - Setting1 = [pscustomobject]@{ - Targets = @( - [pscustomobject]@{ - Name = "Setting1" - Destination = "Environment" - }) - Value = "Test" - } - Setting2 = [pscustomobject]@{ - Targets = @( - [pscustomobject]@{ - Name = "Setting2" - Destination = "Environment" - }) - Type = "Computed" - Value = "{%Setting1%}" - } - } - - Build-ALZDeploymentEnvFile -configuration $configuration -destination "test" - - Should -Invoke New-Item -ParameterFilter { $Path -match ".env$" } -Scope It -Times 1 -Exactly - Should -Invoke Add-Content -ParameterFilter { $Value -match "^Setting1=`"Test`"$" } -Scope It -Times 1 -Exactly - Should -Invoke Add-Content -ParameterFilter { $Value -match "^Setting2=`"Test`"$" } -Scope It -Times 1 -Exactly - } } } } \ No newline at end of file diff --git a/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 b/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 index 61a78456..5adbcac1 100644 --- a/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 +++ b/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 @@ -447,6 +447,7 @@ InModuleScope 'ALZ' { $secondFileContent } + Set-ComputedConfiguration -configuration $defaultConfig Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination '.' -configuration $defaultConfig Should -Invoke -CommandName Out-File -Scope It -Times 2 @@ -469,239 +470,6 @@ InModuleScope 'ALZ' { Should -Invoke -CommandName Out-File -ParameterFilter { $FilePath -eq "test2.parameters.json" -and $InputObject -eq $contentStringAfterParsing } -Scope It } - It 'Computed, Processed array values replace values correctly' { - $config = [pscustomobject]@{ - Nested = [pscustomobject]@{ - Type = "Computed" - Description = "A Test Value" - Process = '@($args | Select-Object -Unique)' - Value = @( - "1", - "1", - "3" - ) - Targets = @( - [pscustomobject]@{ - Name = "parValue.value" - Destination = "Parameters" - }) - } - } - - $fileContent = '{ - "parameters": { - "parValue": { - "value": [] - } - } - }' - - $expectedContent = '{ - "parameters": { - "parValue": { - "value": [ "1", "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 'Computed, Processed array values replace values correctly in a case insensitive deduplication.' { - $config = [pscustomobject]@{ - Nested = [pscustomobject]@{ - Type = "Computed" - Description = "A Test Value" - Process = '@($args | ForEach-Object { $_.ToLower() } | Select-Object -Unique)' - Value = @( - "A", - "a", - "A", - "a" - ) - Targets = @( - [pscustomobject]@{ - Name = "parValue.value" - Destination = "Parameters" - }) - } - } - - $fileContent = '{ - "parameters": { - "parValue": { - "value": [] - } - } - }' - - $expectedContent = '{ - "parameters": { - "parValue": { - "value": [ "a" ] - } - } - }' - - 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 'Computed, Processed array values replace values correctly and keep array type when only one item remains.' { - $config = [pscustomobject]@{ - Nested = [pscustomobject]@{ - Type = "Computed" - Description = "A Test Value" - Process = '@($args | Select-Object -Unique)' - Value = @( - "1", - "1", - "1" - ) - Targets = @( - [pscustomobject]@{ - Name = "parValue.value" - Destination = "Parameters" - }) - } - } - - $fileContent = '{ - "parameters": { - "parValue": { - "value": [] - } - } - }' - - $expectedContent = '{ - "parameters": { - "parValue": { - "value": [ "1" ] - } - } - }' - - 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 'Computed, Processed values replace values correctly' { - $config = [pscustomobject]@{ - Nested = [pscustomobject]@{ - Type = "Computed" - Description = "A Test Value" - Process = '($args[0] -eq "eastus") ? "eastus2" : ($args[0] -eq "eastus2") ? "eastus" : $args[0]' - Value = "eastus" - Targets = @( - [pscustomobject]@{ - Name = "parValue.value" - Destination = "Parameters" - }) - } - } - - $fileContent = '{ - "parameters": { - "parValue": { - "value": "replace_me" - } - } - }' - - $expectedContent = '{ - "parameters": { - "parValue": { - "value": "eastus2" - } - } - }' - - 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 'Computed, Processed values replace values correctly' { - $config = [pscustomobject]@{ - Nested = [pscustomobject]@{ - Type = "Computed" - Description = "A Test Value" - Process = '($args[0] -eq "goodbye") ? "Hello" : "Goodbye"' - Value = "goodbye" - Targets = @( - [pscustomobject]@{ - Name = "parValue.value" - Destination = "Parameters" - }) - } - } - - $fileContent = '{ - "parameters": { - "parValue": { - "value": "replace_me" - } - } - }' - - $expectedContent = '{ - "parameters": { - "parValue": { - "value": "Hello" - } - } - }' - - 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 file specific configuration should be changed correctly' { $defaultConfig = [pscustomobject]@{ Value1 = [pscustomobject]@{ diff --git a/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 b/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 new file mode 100644 index 00000000..34cfa001 --- /dev/null +++ b/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 @@ -0,0 +1,155 @@ +#------------------------------------------------------------------------- +Set-Location -Path $PSScriptRoot +#------------------------------------------------------------------------- +$ModuleName = 'ALZ' +$PathToManifest = [System.IO.Path]::Combine('..', '..', '..', $ModuleName, "$ModuleName.psd1") +#------------------------------------------------------------------------- +if (Get-Module -Name $ModuleName -ErrorAction 'SilentlyContinue') { + #if the module is already in memory, remove it + Remove-Module -Name $ModuleName -Force +} +Import-Module $PathToManifest -Force +#------------------------------------------------------------------------- + +InModuleScope 'ALZ' { + Describe 'Set-ComputedConfiguration Private Function Tests' -Tag Unit { + BeforeAll { + $WarningPreference = 'SilentlyContinue' + $ErrorActionPreference = 'SilentlyContinue' + } + Context 'Set-ComputedConfiguration should update the configuration correctly' { + It 'Handles Computed values correctly.' { + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Environment" + }) + Type = "Computed" + Value = "{%Setting1%}" + } + } + + Set-ComputedConfiguration -configuration $configuration + $configuration.Setting2.Value | Should -BeExactly "Test" + } + + It 'Computed, Processed array values replace values correctly' { + $configuration = [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Process = '@($args | Select-Object -Unique)' + Value = @( + "1", + "1", + "3" + ) + Targets = @( + [pscustomobject]@{ + Name = "parValue.value" + Destination = "Parameters" + }) + } + } + + Set-ComputedConfiguration -configuration $configuration + $configuration.Nested.Value | Should -BeExactly @("1", "3") + } + + It 'Computed, Processed array values replace values correctly in a case insensitive deduplication.' { + $configuration = [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Process = '@($args | ForEach-Object { $_.ToLower() } | Select-Object -Unique)' + Value = @( + "A", + "a", + "A", + "a" + ) + Targets = @( + [pscustomobject]@{ + Name = "parValue.value" + Destination = "Parameters" + }) + } + } + + Set-ComputedConfiguration -configuration $configuration + $configuration.Nested.Value | Should -BeExactly @("a") + } + + It 'Computed, Processed array values replace values correctly and keep array type when only one item remains.' { + $configuration = [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Process = '@($args | Select-Object -Unique)' + Value = @( + "1", + "1", + "1" + ) + Targets = @( + [pscustomobject]@{ + Name = "parValue.value" + Destination = "Parameters" + }) + } + } + + Set-ComputedConfiguration -configuration $configuration + $configuration.Nested.Value | Should -BeExactly @("1") + } + + It 'Computed, Processed values replace values correctly' { + $configuration = [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Process = '($args[0] -eq "eastus") ? "eastus2" : ($args[0] -eq "eastus2") ? "eastus" : $args[0]' + Value = "eastus" + Targets = @( + [pscustomobject]@{ + Name = "parValue.value" + Destination = "Parameters" + }) + } + } + + Set-ComputedConfiguration -configuration $configuration + $configuration.Nested.Value | Should -BeExactly "eastus2" + } + + It 'Computed, Processed values replace values correctly' { + $configuration = [pscustomobject]@{ + Nested = [pscustomobject]@{ + Type = "Computed" + Description = "A Test Value" + Process = '($args[0] -eq "goodbye") ? "Hello" : "Goodbye"' + Value = "goodbye" + Targets = @( + [pscustomobject]@{ + Name = "parValue.value" + Destination = "Parameters" + }) + } + } + + Set-ComputedConfiguration -configuration $configuration + $configuration.Nested.Value | Should -BeExactly "Hello" + } + } + } +} \ No newline at end of file