diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index bc362476..896242f4 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -33,11 +33,9 @@ $null = $modulesToInstall.Add(([PSCustomObject]@{ # Required dependency of the ALZ module itself. $null = $modulesToInstall.Add(([PSCustomObject]@{ ModuleName = 'Az.Resources' - ModuleVersion = '5.6.0' + ModuleVersion = '6.5.2' })) - - 'Installing PowerShell Modules' foreach ($module in $modulesToInstall) { $installSplat = @{ @@ -52,8 +50,7 @@ foreach ($module in $modulesToInstall) { Install-Module @installSplat Import-Module -Name $module.ModuleName -ErrorAction Stop ' - Successfully installed {0}' -f $module.ModuleName - } - catch { + } catch { $message = 'Failed to install {0}' -f $module.ModuleName " - $message" throw diff --git a/src/ALZ/ALZ.psd1 b/src/ALZ/ALZ.psd1 index d035f8be..842d56bc 100644 --- a/src/ALZ/ALZ.psd1 +++ b/src/ALZ/ALZ.psd1 @@ -54,7 +54,7 @@ RequiredModules = @( @{ ModuleName = 'Az.Resources' - ModuleVersion = '5.6.0' + ModuleVersion = '6.5.2' } ) diff --git a/src/ALZ/Assets/alz-bicep-config/v0.13.0.ux.config.json b/src/ALZ/Assets/alz-bicep-config/v0.13.0.ux.config.json new file mode 100644 index 00000000..8192c877 --- /dev/null +++ b/src/ALZ/Assets/alz-bicep-config/v0.13.0.ux.config.json @@ -0,0 +1,205 @@ +{ + "Prefix": { + "Type": "UserInput", + "Description": "The prefix that will be added to all resources created by this deployment. (e.g. 'alz')", + "Targets": [ + { + "Name": "parTopLevelManagementGroupPrefix", + "Destination": "Parameters" + }, + { + "Name": "parCompanyPrefix", + "Destination": "Parameters" + }, + { + "Name": "parTargetManagementGroupId", + "Destination": "Parameters" + }, + { + "Name": "parAssignableScopeManagementGroupId", + "Destination": "Parameters" + } + ], + "Value": "", + "DefaultValue": "alz", + "Valid": "^[a-zA-Z]{3,5}$" + }, + "Suffix": { + "Type": "UserInput", + "Description": "The suffix that will be added to all resources created by this deployment. (e.g. 'test')", + "Targets": [ + { + "Name": "parTopLevelManagementGroupSuffix", + "Destination": "Parameters" + } + ], + "Value": "", + "DefaultValue": "", + "Valid": "^[a-zA-Z]{0,5}$" + }, + "Location": { + "Type": "UserInput", + "Description": "Deployment location.", + "Value": "", + "Targets": [ + { + "Name": "parLocation", + "Destination": "Parameters" + }, + { + "Name": "parAutomationAccountLocation", + "Destination": "Parameters" + }, + { + "Name": "parLogAnalyticsWorkspaceLocation", + "Destination": "Parameters" + } + ], + "AllowedValues": [ + "asia", + "asiapacific", + "australia", + "australiacentral", + "australiacentral2", + "australiaeast", + "australiasoutheast", + "brazil", + "brazilsouth", + "brazilsoutheast", + "canada", + "canadacentral", + "canadaeast", + "centralindia", + "centralus", + "centraluseuap", + "centralusstage", + "eastasia", + "eastasiastage", + "eastus", + "eastus2", + "eastus2euap", + "eastus2stage", + "eastusstg", + "europe", + "france", + "francecentral", + "francesouth", + "germany", + "germanynorth", + "germanywestcentral", + "global", + "india", + "japan", + "japaneast", + "japanwest", + "jioindiacentral", + "jioindiawest", + "korea", + "koreacentral", + "koreasouth", + "northcentralus", + "northcentralusstage", + "northeurope", + "norway", + "norwayeast", + "norwaywest", + "qatarcentral", + "singapore", + "southafrica", + "southafricanorth", + "southafricawest", + "southcentralus", + "southcentralusstage", + "southeastasia", + "southindia", + "swedencentral", + "switzerland", + "switzerlandnorth", + "switzerlandwest", + "uaecentral", + "uaenorth", + "uksouth", + "ukwest", + "unitedstates", + "westcentralus", + "westeurope", + "westindia", + "westus", + "westus2", + "westus2stage", + "westus3", + "westusstage" + ] + }, + "Environment": { + "Type": "UserInput", + "Description": "The Type of environment that will be created. (e.g. 'dev', 'test', 'qa', 'staging', 'prod')", + "Targets": [ + { + "Name": "parEnvironment", + "Destination": "Parameters" + } + ], + "Value": "", + "DefaultValue": "prod", + "Valid": "^[a-zA-Z0-9]{2,10}$" + }, + "IdentitySubscriptionId": { + "Type": "UserInput", + "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", + "Destination": "Environment" + } + ], + "Value": "" + }, + "ConnectivitySubscriptionId": { + "Type": "UserInput", + "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" + } + ], + "Value": "" + }, + "ManagementSubscriptionId": { + "Type": "UserInput", + "Description": "The identifier of the Management 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": "ManagementSubscriptionId", + "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": { + "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}$", + "Value": "" + }, + "LogAnalyticsResourceId": { + "Type": "Computed", + "Value": "/subscriptions/{%ManagementSubscriptionId%}/resourcegroups/alz-logging/providers/microsoft.operationalinsights/workspaces/alz-log-analytics", + "Targets": [ + { + "Name": "parLogAnalyticsWorkspaceResourceId", + "Destination": "Parameters" + } + ] + } +} \ No newline at end of file diff --git a/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 b/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 new file mode 100644 index 00000000..31fb1f91 --- /dev/null +++ b/src/ALZ/Private/Build-ALZDeploymentEnvFile.ps1 @@ -0,0 +1,31 @@ +function Build-ALZDeploymentEnvFile { + param ( + [Parameter(Mandatory = $true)] + [PSCustomObject] $configuration, + + [Parameter(Mandatory = $false)] + [string] $destination = "." + ) + <# + .SYNOPSIS + This function uses configuration to build a .env file for use in the deployment pipeline. + .EXAMPLE + Build-ALZDeploymentEnvFile -configuration configuration + .EXAMPLE + Build-ALZDeploymentEnvFile -configuration configuration -destination "." + .OUTPUTS + N/A + #> + + $envFile = Join-Path $destination ".env" + + New-Item -Path $envFile -ItemType file -Force | Out-Null + + foreach ($configurationValue in $configuration.PsObject.Properties) { + foreach ($target in $configurationValue.Value.Targets) { + if ($target.Destination -eq "Environment") { + Add-Content -Path $envFile -Value "$($($target.Name))=`"$($configurationValue.Value.Value)`"" | Out-Null + } + } + } +} \ No newline at end of file diff --git a/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 b/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 index 38113f5b..8b8325cc 100644 --- a/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 +++ b/src/ALZ/Private/Edit-ALZConfigurationFilesInPlace.ps1 @@ -23,9 +23,13 @@ function Edit-ALZConfigurationFilesInPlace { $bicepConfiguration = Get-Content $file.FullName | ConvertFrom-Json -AsHashtable $modified = $false foreach ($configKey in $configuration.PsObject.Properties) { - foreach ($name in $configKey.Value.Names) { - if ($null -ne $bicepConfiguration.parameters[$name]) { - $bicepConfiguration.parameters[$name].value = $configKey.Value.Value + foreach ($target in $configKey.Value.Targets) { + if ($target.Destination -eq "Parameters" -and $null -ne $bicepConfiguration.parameters[$target.Name]) { + if ($configKey.Value.Type -eq "Computed") { + $bicepConfiguration.parameters[$target.Name].value = Format-TokenizedConfigurationString $configKey.Value.Value $configuration + } else { + $bicepConfiguration.parameters[$target.Name].value = $configKey.Value.Value + } $modified = $true } } diff --git a/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 b/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 new file mode 100644 index 00000000..bd905822 --- /dev/null +++ b/src/ALZ/Private/Format-TokenizedConfigurationString.ps1 @@ -0,0 +1,25 @@ +function Format-TokenizedConfigurationString { + param( + [Parameter(Mandatory = $true)] + [string] $tokenizedString, + + [Parameter(Mandatory = $true)] + [object] $configuration + ) + $values = $tokenizedString -split "\{\%|\%\}" + + $returnValue = "" + foreach ($value in $values) { + $isToken = $tokenizedString -contains "{%$value%}" + if ($null -ne $configuration.$value) { + $returnValue += $configuration.$value.Value + } elseif (($null -eq $configuration.$value) -and $isToken) { + Write-InformationColored "Specified replacement token '${value}' not found in configuration." -ForegroundColor Yellow -InformationAction Continue + $returnValue += "{%$value%}" + } else { + $returnValue += $value + } + } + + return $returnValue +} \ No newline at end of file diff --git a/src/ALZ/Private/Get-Configuration.ps1 b/src/ALZ/Private/Get-Configuration.ps1 new file mode 100644 index 00000000..37dbd4a0 --- /dev/null +++ b/src/ALZ/Private/Get-Configuration.ps1 @@ -0,0 +1,31 @@ +function Get-Configuration { + param( + [Parameter(Mandatory = $false)] + [ValidateSet("bicep", "terraform")] + [string] $alzIacProvider = "bicep", + + [Parameter(Mandatory = $false)] + [string] $alzEnvironmentDestination = ".", + + [Parameter(Mandatory = $false)] + [string] $alzBicepVersion = "v0.13.0" + ) + <# + .SYNOPSIS + This function uses a template configuration to prompt for and return a user specified/modified configuration object. + .EXAMPLE + Get-Configuration + .EXAMPLE + Get-Configuration -alzIacProvider "bicep" + .OUTPUTS + System.Object. The resultant configuration values. + #> + + if ($alzIacProvider -eq "terraform") { + throw "Terraform is not yet supported." + } + + $uxConfigurationFile = Join-Path $alzEnvironmentDestination "alz-bicep-config" "$alzBicepVersion.ux.config.json" + return Get-Content -Path $uxConfigurationFile -Raw | ConvertFrom-Json +} + diff --git a/src/ALZ/Private/Initialize-ConfigurationObject.ps1 b/src/ALZ/Private/Initialize-ConfigurationObject.ps1 deleted file mode 100644 index 4456c32c..00000000 --- a/src/ALZ/Private/Initialize-ConfigurationObject.ps1 +++ /dev/null @@ -1,50 +0,0 @@ - -function Initialize-ConfigurationObject { - param( - [Parameter(Mandatory = $false)] - [ValidateSet("bicep", "terraform")] - [string] $alzIacProvider = "bicep" - ) - <# - .SYNOPSIS - This function uses a template configuration to prompt for and return a user specified/modified configuration object. - .EXAMPLE - Initialize-ConfigurationObject - .EXAMPLE - Initialize-ConfigurationObject -alzIacProvider "bicep" - .OUTPUTS - System.Object. The resultant configuration values. - #> - - if ($alzIacProvider -eq "terraform") { - throw "Terraform is not yet supported." - } - - return [pscustomobject]@{ - Prefix = [pscustomobject]@{ - description = "The prefix that will be added to all resources created by this deployment." - names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") - value = "alz" - defaultValue = "alz" - } - Suffix = [pscustomobject]@{ - Description = "The suffix that will be added to all resources created by this deployment." - Names = @("parTopLevelManagementGroupSuffix") - Value = "" - DefaultValue = "" - } - Location = [pscustomobject]@{ - Description = "Deployment location." - Names = @("parLocation") - AllowedValues = @(Get-AzLocation | Sort-Object Location | Select-Object -ExpandProperty Location ) - Value = "" - } - Environment = [pscustomobject]@{ - Description = "The type of environment that will be created . Example: dev, test, qa, staging, prod" - Names = @("parEnvironment") - DefaultValue = 'prod' - Value = "" - } - } -} - diff --git a/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 b/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 index 6d07b857..e5d1616e 100644 --- a/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 +++ b/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 @@ -1,17 +1,23 @@ function Request-ALZEnvironmentConfig { param( - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $true)] [ValidateSet("bicep", "terraform")] - [string] $alzIacProvider = "bicep" + [string] $alzIacProvider, + + [Parameter(Mandatory = $true)] + [string] $alzEnvironmentDestination, + + [Parameter(Mandatory = $true)] + [string] $alzBicepVersion ) <# .SYNOPSIS This function uses a template configuration to prompt for and return a user specified/modified configuration object. .EXAMPLE - New-SlzEnvironmentConfig + Request-ALZEnvironmentConfig .EXAMPLE - New-SlzEnvironmentConfig -sourceConfigurationFile "orchestration/scripts/parameters/sovereignLandingZone.parameters.json" + Request-ALZEnvironmentConfig -alzIacProvider "bicep" .OUTPUTS System.Object. The resultant configuration values. #> @@ -19,12 +25,14 @@ function Request-ALZEnvironmentConfig { throw "Terraform is not yet supported." } - $configuration = Initialize-ConfigurationObject -alzIacProvider $alzIacProvider + $configuration = Get-Configuration -alzIacProvider $alzIacProvider -alzEnvironmentDestination $alzEnvironmentDestination -alzBicepVersion $alzBicepVersion Write-Verbose "Configuration object initialized." - Write-Verbose "Configuration object: $(ConvertTo-Json $configuration)" + Write-Verbose "Configuration object: $(ConvertTo-Json $configuration -Depth 10)" foreach ($configurationValue in $configuration.PsObject.Properties) { - Request-ConfigurationValue $configurationValue.Name $configurationValue.Value + if ($configurationValue.Value.Type -eq "UserInput") { + Request-ConfigurationValue $configurationValue.Name $configurationValue.Value + } } return $configuration diff --git a/src/ALZ/Private/Request-ConfigurationValue.ps1 b/src/ALZ/Private/Request-ConfigurationValue.ps1 index f71512bf..5ab6cd6d 100644 --- a/src/ALZ/Private/Request-ConfigurationValue.ps1 +++ b/src/ALZ/Private/Request-ConfigurationValue.ps1 @@ -4,16 +4,21 @@ function Request-ConfigurationValue { [string] $configName, [Parameter(Mandatory = $true)] - [object] $configValue + [object] $configValue, + + [Parameter(Mandatory = $false)] + [System.Boolean] $withRetries = $true ) - $allowedValues = $configValue.allowedValues - $hasAllowedValues = $null -ne $configValue.allowedValues + $allowedValues = $configValue.AllowedValues + $hasAllowedValues = $null -ne $configValue.AllowedValues + + $defaultValue = $configValue.DefaultValue + $hasDefaultValue = $null -ne $configValue.DefaultValue - $defaultValue = $configValue.defaultValue - $hasDefaultValue = $null -ne $configValue.defaultValue + $hasValidator = $null -ne $configValue.Valid - Write-InformationColored $configValue.description -ForegroundColor White -InformationAction Continue + Write-InformationColored $configValue.Description -ForegroundColor White -InformationAction Continue if ($hasAllowedValues) { Write-InformationColored "[allowed: $allowedValues] " -ForegroundColor Yellow -InformationAction Continue } @@ -29,13 +34,27 @@ function Request-ConfigurationValue { $readValue = Read-Host + $previousValue = $configValue.Value + if ($hasDefaultValue -and $readValue -eq "") { - $configValue.value = $configValue.defaultValue + $configValue.Value = $configValue.defaultValue } else { - $configValue.value = $readValue + $configValue.Value = $readValue } + + $hasNotSpecifiedValue = ($null -eq $configValue.Value -or "" -eq $configValue.Value) -and ($configValue.Value -ne $configValue.DefaultValue) + $isDisallowedValue = $hasAllowedValues -and $allowedValues.Contains($configValue.Value) -eq $false + $isNotValid = $hasValidator -and $configValue.Value -match $configValue.Valid -eq $false + + if ($hasNotSpecifiedValue -or $isDisallowedValue -or $isNotValid) { + Write-InformationColored "Please specify a valid value for this field." -ForegroundColor Red -InformationAction Continue + $configValue.Value = $previousValue + $validationError = $true + } + + $shouldRetry = $validationError -and $withRetries } - while ((($null -eq $configValue.value -or "" -eq $configValue.value) -and ($configValue.value -ne $configValue.defaultValue)) -or ($hasAllowedValues -and $allowedValues.Contains($configValue.value) -eq $false)) + while (($hasNotSpecifiedValue -or $isDisallowedValue -or $isNotValid) -and $shouldRetry) Write-InformationColored "" -InformationAction Continue } diff --git a/src/ALZ/Public/New-ALZEnvironment.ps1 b/src/ALZ/Public/New-ALZEnvironment.ps1 index a17a0c21..582a83cc 100644 --- a/src/ALZ/Public/New-ALZEnvironment.ps1 +++ b/src/ALZ/Public/New-ALZEnvironment.ps1 @@ -45,12 +45,11 @@ function New-ALZEnvironment { return $false } - $configuration = Request-ALZEnvironmentConfig -alzIacProvider $alzIacProvider - if ($PSCmdlet.ShouldProcess("ALZ-Bicep module configuration", "modify")) { New-ALZDirectoryEnvironment -alzEnvironmentDestination $alzEnvironmentDestination | Out-Null + $assetsDirectory = Join-Path $(Get-ScriptRoot) "../Assets" Copy-Item -Path "$assetsDirectory/*" -Recurse -Destination $alzEnvironmentDestination -Force @@ -62,7 +61,10 @@ function New-ALZEnvironment { Initialize-ALZBicepConfigFile -alzEnvironmentDestination $alzEnvironmentDestination -alzBicepVersion $alzBicepVersion | Out-Null } + $configuration = Request-ALZEnvironmentConfig -alzIacProvider $alzIacProvider -alzEnvironmentDestination $alzEnvironmentDestination -alzBicepVersion $alzBicepVersion + Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination $alzEnvironmentDestination -configuration $configuration | Out-Null + Build-ALZDeploymentEnvFile -configuration $configuration -Destination $alzEnvironmentDestination | Out-Null } return $true diff --git a/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 b/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 new file mode 100644 index 00000000..4d9cc9e3 --- /dev/null +++ b/src/Tests/Unit/Private/Build-ALZDeploymentEnvFile.Tests.ps1 @@ -0,0 +1,82 @@ +#------------------------------------------------------------------------- +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 'Build-AZLDeploymentEnvFile Private Function Tests' -Tag Unit { + BeforeAll { + $WarningPreference = 'SilentlyContinue' + $ErrorActionPreference = 'SilentlyContinue' + } + Context 'Build-AZLDeploymentEnvFile should create a .env file correctly' { + It 'Creates a config file based on configuration.' { + + Mock -CommandName New-Item + Mock -CommandName Add-Content + + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Environment" + }) + Value = "Test2" + } + } + + 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=`"Test1`"$" } -Scope It -Times 1 -Exactly + Should -Invoke Add-Content -ParameterFilter { $Value -match "^Setting2=`"Test2`"$" } -Scope It -Times 1 -Exactly + } + It 'Omits configuration not intended for the .env file.' { + + Mock -CommandName New-Item + Mock -CommandName Add-Content + + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Parameters" + }) + Value = "Test2" + } + } + + Build-ALZDeploymentEnvFile -configuration $configuration -destination "test" + + Should -Invoke New-Item -ParameterFilter { $Path -match ".env$" } -Scope It -Times 1 -Exactly + Should -Invoke Add-Content -Scope It -Times 1 -Exactly + } + } + } +} \ No newline at end of file diff --git a/src/Tests/Unit/Private/Create-ALZEnvironmentConfig.Tests.ps1 b/src/Tests/Unit/Private/Create-ALZEnvironmentConfig.Tests.ps1 deleted file mode 100644 index dc326d35..00000000 --- a/src/Tests/Unit/Private/Create-ALZEnvironmentConfig.Tests.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -#------------------------------------------------------------------------- -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 'Request-ConfigurationValue Public Function Tests' -Tag Unit { - BeforeAll { - $WarningPreference = 'SilentlyContinue' - $ErrorActionPreference = 'SilentlyContinue' - } - Context 'Non Az Module' { - BeforeEach { - Mock -CommandName Get-Module -MockWith { - $null - } - } - It 'should return the not met for non AZ module' { - Test-ALZRequirement | Should -BeExactly "ALZ requirements are not met." - } - } - Context 'Incompatible Powershell version lower them 7' { - BeforeEach { - Mock -CommandName Get-PSVersion -MockWith { - [PSCustomObject]@{ - PSVersion = [PSCustomObject]@{ - Major = 6 - Minor = 2 - } - } - } - } - It 'should return the not met for non compatible pwsh versions' { - Test-ALZRequirement | Should -BeExactly "ALZ requirements are not met." - } - } - Context 'Incompatible Powershell version 7.0' { - BeforeEach { - Mock -CommandName Get-PSVersion -MockWith { - [PSCustomObject]@{ - PSVersion = [PSCustomObject]@{ - Major = 7 - Minor = 0 - } - } - } - } - It 'should return the not met for non compatible pwsh versions' { - Test-ALZRequirement | Should -BeExactly "ALZ requirements are not met." - } - } - Context 'Git not installed' { - BeforeEach { - Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'git' } -MockWith { - $null - } - } - It 'should return the not met for no git instalation' { - Test-ALZRequirement | Should -BeExactly "ALZ requirements are not met." - } - } - Context 'Bicep not installed' { - BeforeEach { - Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'bicep' } -MockWith { - $null - } - } - It 'should return the not met for no bicep instalation' { - Test-ALZRequirement | Should -BeExactly "ALZ requirements are not met." - } - } - Context 'Success' { - - BeforeEach { - Mock -CommandName Get-Module -MockWith { - [PSCustomObject]@{ - Name = 'Az' - } - } - Mock -CommandName Get-PSVersion -MockWith { - [PSCustomObject]@{ - PSVersion = [PSCustomObject]@{ - Major = 7 - Minor = 1 - } - } - } - Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'git' } -MockWith { - [PSCustomObject]@{ - Name = 'git' - } - } - Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'bicep' } -MockWith { - [PSCustomObject]@{ - Name = 'bicep' - } - } - } - - It 'should return the expected results' { - Test-ALZRequirement | Should -BeExactly "ALZ requirements are met." - } - - } - } -} diff --git a/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 b/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 index c44c382d..210dc34d 100644 --- a/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 +++ b/src/Tests/Unit/Private/Edit-ALZConfigurationFilesInPlace.Tests.ps1 @@ -16,28 +16,58 @@ InModuleScope 'ALZ' { $defaultConfig = [pscustomobject]@{ Prefix = [pscustomobject]@{ Description = "The prefix that will be added to all resources created by this deployment." - Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + 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." - Names = @("parTopLevelManagementGroupSuffix") + Targets = @( + [pscustomobject]@{ + Name = "parTopLevelManagementGroupSuffix" + Destination = "Parameters" + }) Value = "bla" DefaultValue = "" } Location = [pscustomobject]@{ Description = "Deployment location." - Names = @("parLocation") + 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" - Names = @("parEnvironment") + 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" + }) + } } $firstFileContent = '{ "parameters": { @@ -46,6 +76,9 @@ InModuleScope 'ALZ' { }, "parTopLevelManagementGroupPrefix": { "value": "" + }, + "parLogging" : { + "value": "" } } }' @@ -97,9 +130,11 @@ InModuleScope 'ALZ' { $contentAfterParsing = ConvertFrom-Json -InputObject $firstFileContent -AsHashtable $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 + $contentAfterParsing = ConvertFrom-Json -InputObject $secondFileContent -AsHashtable $contentAfterParsing.parameters.parTopLevelManagementGroupSuffix.value = 'bla' $contentAfterParsing.parameters.parLocation.value = 'eastus' diff --git a/src/Tests/Unit/Private/Format-TokenizedConfigurationString.Tests.ps1 b/src/Tests/Unit/Private/Format-TokenizedConfigurationString.Tests.ps1 new file mode 100644 index 00000000..a183ee12 --- /dev/null +++ b/src/Tests/Unit/Private/Format-TokenizedConfigurationString.Tests.ps1 @@ -0,0 +1,118 @@ +#------------------------------------------------------------------------- +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 'Format-TokenizedConfigurationString tests ' -Tag Unit { + BeforeAll { + $WarningPreference = 'SilentlyContinue' + $ErrorActionPreference = 'SilentlyContinue' + } + Context 'Replace the specified tokens with values in the configuration object.' { + BeforeEach { + } + It 'When there is one token to replace.' { + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Parameters" + }) + Value = "Test2" + } + } + + Format-TokenizedConfigurationString "{%Setting1%}" $configuration | Should -Be "Test1" + } + + It 'When there are two tokens to replace.' { + + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Parameters" + }) + Value = "Test2" + } + } + + Format-TokenizedConfigurationString "{%Setting1%}/{%Setting2%}" $configuration | Should -Be "Test1/Test2" + } + + It 'When the token is not found.' { + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Parameters" + }) + Value = "Test2" + } + } + + Format-TokenizedConfigurationString "{%DoesntMatch%}" $configuration | Should -Be "{%DoesntMatch%}" + } + + It 'When the token is repeated.' { + + $configuration = [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting1" + Destination = "Environment" + }) + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Targets = @( + [pscustomobject]@{ + Name = "Setting2" + Destination = "Parameters" + }) + Value = "Test2" + } + } + + Format-TokenizedConfigurationString "{%Setting1%}/{%Setting1%}/{%Setting1%}/{%Setting1%}" $configuration | Should -Be "Test1/Test1/Test1/Test1" + } + } + } +} diff --git a/src/Tests/Unit/Private/Get-Configuration.Tests.ps1 b/src/Tests/Unit/Private/Get-Configuration.Tests.ps1 new file mode 100644 index 00000000..70182174 --- /dev/null +++ b/src/Tests/Unit/Private/Get-Configuration.Tests.ps1 @@ -0,0 +1,126 @@ +#------------------------------------------------------------------------- +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 'Get-Configuration Function Tests' -Tag Unit { + BeforeAll { + $WarningPreference = 'SilentlyContinue' + $ErrorActionPreference = 'SilentlyContinue' + } + Context 'Create the correct folders for the environment' { + BeforeEach { + Mock -CommandName Get-Content -MockWith { + ' + { + "Prefix": { + "Type": "UserInput", + "Description": "The prefix that will be added to all resources created by this deployment. (e.g. alz)", + "Targets": [ + { + "Name": "parTopLevelManagementGroupPrefix", + "Destination": "Parameters" + }, + { + "Name": "parCompanyPrefix", + "Destination": "Parameters" + }, + { + "Name": "parTargetManagementGroupId", + "Destination": "Parameters" + }, + { + "Name": "parAssignableScopeManagementGroupId", + "Destination": "Parameters" + } + ], + "Value": "", + "DefaultValue": "alz", + "Valid": "^[a-zA-Z]{3,5}$" + }, + "Suffix": { + "Type": "UserInput", + "Description": "The suffix that will be added to all resources created by this deployment. (e.g. test)", + "Targets": [ + { + "Name": "parTopLevelManagementGroupSuffix", + "Destination": "Parameters" + } + ], + "Value": "", + "DefaultValue": "", + "Valid": "^[a-zA-Z]{0,5}$" + }, + "Location": { + "Type": "UserInput", + "Description": "Deployment location.", + "Value": "", + "Targets": [ + { + "Name": "parLocation", + "Destination": "Parameters" + }, + { + "Name": "parAutomationAccountLocation", + "Destination": "Parameters" + }, + { + "Name": "parLogAnalyticsWorkspaceLocation", + "Destination": "Parameters" + } + ], + "AllowedValues": [ + "eastus", + "ukwest" + ] + }, + "Environment": { + "Type": "UserInput", + "Description": "The Type of environment that will be created. (e.g. dev, test, qa, staging, prod)", + "Targets": [ + { + "Name": "parEnvironment", + "Destination": "Parameters" + } + ], + "Value": "", + "DefaultValue": "prod", + "Valid": "^[a-zA-Z0-9]{2,10}$" + }, + }' + } + } + It 'configuration loads correctly.' { + $content = Get-Configuration + $content.Prefix.Value | Should -Be '' + $content.Prefix.DefaultValue | Should -Be 'alz' + $content.Prefix.Description | Should -Be "The prefix that will be added to all resources created by this deployment. (e.g. alz)" + + $content.Suffix.Value | Should -Be '' + $content.Suffix.DefaultValue | Should -Be '' + $content.Suffix.Description | Should -Be "The suffix that will be added to all resources created by this deployment. (e.g. test)" + + $content.Location.Value | Should -Be '' + $content.Location.Description | Should -Be 'Deployment location.' + $content.Location.AllowedValues | Should -Be @('eastus', 'ukwest') + + $content.Environment.Value | Should -Be '' + $content.Environment.Description | Should -Be "The type of environment that will be created. (e.g. dev, test, qa, staging, prod)" + $content.Environment.DefaultValue | Should -Be 'prod' + } + + It 'Throws for unsupported Terraform IAC' { + { Get-Configuration -alzIacProvider "terraform" } | Should -Throw -ExpectedMessage "Terraform is not yet supported." + } + } + } +} diff --git a/src/Tests/Unit/Private/Initialize-ConfigurationObject.Tests.ps1 b/src/Tests/Unit/Private/Initialize-ConfigurationObject.Tests.ps1 deleted file mode 100644 index d9cb1dbb..00000000 --- a/src/Tests/Unit/Private/Initialize-ConfigurationObject.Tests.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -#------------------------------------------------------------------------- -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 'New-ALZDirectoryEnvironment Function Tests' -Tag Unit { - BeforeAll { - $WarningPreference = 'SilentlyContinue' - $ErrorActionPreference = 'SilentlyContinue' - } - Context 'Create the correctr foldes for the environment' { - BeforeEach { - Mock -CommandName Get-AzLocation -MockWith { - @( - [PSCustomObject]@{ - Location = 'ukwest' - }, - [PSCustomObject]@{ - Location = 'eastus' - } - ) - } - } - It 'should return the not met for non AZ module' { - $content = Initialize-ConfigurationObject - $content.Prefix.Value | Should -Be 'alz' - $content.Prefix.DefaultValue | Should -Be 'alz' - $content.Prefix.Description | Should -Be 'The prefix that will be added to all resources created by this deployment.' - $content.Prefix.Names | Should -Be @('parTopLevelManagementGroupPrefix', 'parCompanyPrefix') - - $content.Suffix.Value | Should -Be '' - $content.Suffix.DefaultValue | Should -Be '' - $content.Suffix.Description | Should -Be 'The suffix that will be added to all resources created by this deployment.' - $content.Suffix.Names | Should -Be @('parTopLevelManagementGroupSuffix') - - $content.Location.Value | Should -Be '' - $content.Location.Description | Should -Be 'Deployment location.' - $content.Location.Names | Should -Be @('parLocation') - $content.Location.AllowedValues | Should -Be @('eastus', 'ukwest') - - $content.Environment.Value | Should -Be '' - $content.Environment.Description | Should -Be 'The type of environment that will be created . Example: dev, test, qa, staging, prod' - $content.Environment.Names | Should -Be @('parEnvironment') - $content.Environment.DefaultValue | Should -Be 'prod' - - } - } - } -} diff --git a/src/Tests/Unit/Private/Request-ALZEnvironmentConfig.Tests.ps1 b/src/Tests/Unit/Private/Request-ALZEnvironmentConfig.Tests.ps1 new file mode 100644 index 00000000..b622a8b4 --- /dev/null +++ b/src/Tests/Unit/Private/Request-ALZEnvironmentConfig.Tests.ps1 @@ -0,0 +1,50 @@ +#------------------------------------------------------------------------- +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 'Request-ALZEnvironmentConfig Private Function Tests' -Tag Unit { + BeforeAll { + $WarningPreference = 'SilentlyContinue' + $ErrorActionPreference = 'SilentlyContinue' + } + Context 'Request-ALZEnvironmentConfig should request CLI input for configuration.' { + It 'Based on the configuration object' { + + Mock -CommandName Get-Configuration -MockWith { + [pscustomobject]@{ + Setting1 = [pscustomobject]@{ + Type = "UserInput" + ForEnvironment = $true + Value = "Test1" + } + Setting2 = [pscustomobject]@{ + Type = "UserInput" + ForEnvironment = $true + Value = "Test2" + } + } + } + + Mock -CommandName Request-ConfigurationValue + + Request-ALZEnvironmentConfig -alzIacProvider "bicep" -alzEnvironmentDestination "." -alzBicepVersion "v0.13.0" + + Should -Invoke Request-ConfigurationValue -Scope It -Times 2 -Exactly + } + + It 'Throws if the unsupported Terraform IAC is specified.' { + { Request-ALZEnvironmentConfig -alzIacProvider "terraform" -alzEnvironmentDestination "." -alzBicepVersion "v0.13.0" } | Should -Throw -ExpectedMessage "Terraform is not yet supported." + } + } + } +} \ No newline at end of file diff --git a/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 b/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 index b3b0b611..cf34c51a 100644 --- a/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 +++ b/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 @@ -29,17 +29,86 @@ InModuleScope 'ALZ' { } It 'Prompt the user for configuration with a default value.' { $configValue = @{ - description = "The prefix that will be added to all resources created by this deployment." - names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") - value = "alz" - defaultValue = "alz" + Description = "The prefix that will be added to all resources created by this deployment." + Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + Value = "" + DefaultValue = "alz" } Request-ConfigurationValue -configName "prefix" -configValue $configValue Assert-MockCalled -CommandName Write-InformationColored -Times 3 - $configValue.value | Should -BeExactly "user input value" + $configValue.Value | Should -BeExactly "user input value" + } + + It 'Prompt the user for configuration and providing no value selects the default value.' { + Mock -CommandName Read-Host -MockWith { + "" + } + + $configValue = @{ + Description = "The prefix that will be added to all resources created by this deployment." + Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + Value = "" + DefaultValue = "alz" + } + + Request-ConfigurationValue -configName "prefix" -configValue $configValue + + Assert-MockCalled -CommandName Write-InformationColored -Times 3 + + $configValue.Value | Should -BeExactly "alz" + } + + It 'Prompt the user with warning text if no value is specified and no default value is present.' { + Mock -CommandName Read-Host -MockWith { + "" + } + + $configValue = @{ + Description = "The prefix that will be added to all resources created by this deployment." + Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + Value = "" + } + + Request-ConfigurationValue -configName "prefix" -configValue $configValue -withRetries $false + + Should -Invoke -CommandName Write-InformationColored -ParameterFilter { $ForegroundColor -eq "Red" } -Scope It + + $configValue.Value | Should -BeExactly "" + } + + It 'Prompt the user with warning text when an invalid value is specified and leave the existing value unchanged.' { + $configValue = @{ + Description = "The prefix that will be added to all resources created by this deployment." + Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + Value = "" + DefaultValue = "alz" + Valid = "^[a-zA-Z]{3,5}$" + } + + Request-ConfigurationValue -configName "prefix" -configValue $configValue -withRetries $false + + Should -Invoke -CommandName Write-InformationColored -ParameterFilter { $ForegroundColor -eq "Red" } -Scope It + $configValue.Value | Should -BeExactly "" + } + + It 'Prompt the user with warning text when a value is specified which isnt in the allowed list and leave the existing value unchanged.' { + Mock -CommandName Read-Host -MockWith { + "notinthelist" + } + + $configValue = @{ + Description = "The prefix that will be added to all resources created by this deployment." + Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + Value = "" + AllowedValues = @("alz", "slz") + } + Request-ConfigurationValue -configName "prefix" -configValue $configValue -withRetries $false + + Should -Invoke -CommandName Write-InformationColored -ParameterFilter { $ForegroundColor -eq "Red" } -Scope It + $configValue.Value | Should -BeExactly "" } } } diff --git a/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 b/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 index f124fc7c..8329474d 100644 --- a/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 +++ b/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 @@ -39,6 +39,7 @@ InModuleScope 'ALZ' { } Mock -CommandName Edit-ALZConfigurationFilesInPlace + Mock -CommandName Build-ALZDeploymentEnvFile Mock -CommandName Get-ALZBicepSource -MockWith { "C:\temp\source" @@ -47,6 +48,8 @@ InModuleScope 'ALZ' { Mock -CommandName New-ALZDirectoryEnvironment -MockWith { } Mock -CommandName Copy-Item -MockWith { } + + Mock -CommandName Write-InformationColored } It 'should return the output directory on completion' { @@ -55,6 +58,12 @@ InModuleScope 'ALZ' { Assert-MockCalled -CommandName Edit-ALZConfigurationFilesInPlace -Exactly 1 } + + It 'Warns if the unsupported Terraform IAC is specified.' { + New-ALZEnvironment -alzIacProvider "terraform" + + Should -Invoke -CommandName Write-InformationColored -ParameterFilter { $ForegroundColor -eq "Red" } -Scope It + } } } } diff --git a/src/Tests/Unit/Public/Test-ALZRequirement.Tests.ps1 b/src/Tests/Unit/Public/Test-ALZRequirement.Tests.ps1 index 1ce4229e..af1b1cc8 100644 --- a/src/Tests/Unit/Public/Test-ALZRequirement.Tests.ps1 +++ b/src/Tests/Unit/Public/Test-ALZRequirement.Tests.ps1 @@ -108,7 +108,6 @@ InModuleScope 'ALZ' { It 'should return the expected results' { Test-ALZRequirement | Should -BeExactly "ALZ requirements are met." } - } } }