diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4877a6fd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{ps1,psm1,psd1}] +indent_style = space +indent_size = 4 + + diff --git a/.github/workflows/PullRequest.yml b/.github/workflows/PullRequest.yml index 0b6aa086..e1b0750e 100644 --- a/.github/workflows/PullRequest.yml +++ b/.github/workflows/PullRequest.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - pwsh: ['7.1.3'] + pwsh: ['7.1.3', '7.5.0'] steps: - name: Check out repository uses: actions/checkout@v3 diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index dceae285..49517b30 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -42,6 +42,7 @@ jobs: # VALIDATE_TERRAFORM_TERRASCAN: true # disabled for now as does not support TF 1.3 optional(type, default) VALIDATE_TERRAFORM_TFLINT: true VALIDATE_YAML: true + VALIDATE_EDITORCONFIG: true # VALIDATE_GO: true # Disabled because it down not work :( # Additional settings: # If a shell script is not executable, the bash-exec diff --git a/.gitignore b/.gitignore index 4e4bfe16..efa7b79a 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,4 @@ templates/.test_azuredevops templates/.test_github .vscode/settings.json /ALZ -.tools \ No newline at end of file +.tools diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index 71665a48..4df355f0 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -29,6 +29,15 @@ $null = $modulesToInstall.Add(([PSCustomObject]@{ ModuleName = 'platyPS' ModuleVersion = '0.12.0' })) +# Required for Invoke-EABillingSPNPermissionsSetup to work +$null = $modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'Az.Accounts' + ModuleVersion = '2.10.4' + })) +$null = $modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'Az.Resources' + ModuleVersion = '6.5.0' + })) 'Installing PowerShell Modules' foreach ($module in $modulesToInstall) { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 100644 index a1d84e3e..00000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.2.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.2.4] - -- Add support for Terraform - -## [0.2.3] - -- fix min prefix length from 3 to 2 - -## [0.2.2] - -- Add fix for resource group name reference for LAW id -- Change supported ALZ version to v0.14.0 - -## [0.2.1] - -- Fixed the issue by adding in two new computed values to correlate to -each parameter -- Changed alz bicep config for the soon to be new release - -## [0.2.0] - -- Need to adjust the policy assignment param file as switched to the ALZ -defaults. -- Need to switch to orchestration version of management diagnostic -setting module param file. -- Switching custom modules directory to match naming convention of -custom parameters directory. - -## [0.1.4] - -- Adding Computed values to Environment -- Adding file targeted config to ALZ-BICEP-CONFIG \ No newline at end of file diff --git a/src/ALZ.Settings.ps1 b/src/ALZ.Settings.ps1 index 93817bff..cb208439 100644 --- a/src/ALZ.Settings.ps1 +++ b/src/ALZ.Settings.ps1 @@ -1,2 +1,2 @@ # specify the minimum required major PowerShell version that the build script should validate -[version]$script:requiredPSVersion = '5.1.0' \ No newline at end of file +[version]$script:requiredPSVersion = '7.1.3' diff --git a/src/ALZ/ALZ.psd1 b/src/ALZ/ALZ.psd1 index 0f0f27b7..e23e66e7 100644 --- a/src/ALZ/ALZ.psd1 +++ b/src/ALZ/ALZ.psd1 @@ -9,33 +9,33 @@ @{ # Script module or binary module file associated with this manifest. - RootModule = 'ALZ.psm1' + RootModule = 'ALZ.psm1' # Version number of this module. - ModuleVersion = '0.1.0' + ModuleVersion = '0.1.0' # Supported PSEditions # CompatiblePSEditions = @() # ID used to uniquely identify this module - GUID = '74a4385f-281e-4776-bd7c-3b6a09d6ba63' + GUID = '74a4385f-281e-4776-bd7c-3b6a09d6ba63' # Author of this module - Author = 'Microsoft Corporation' + Author = 'Microsoft Corporation' # Company or vendor of this module - CompanyName = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' # Copyright statement for this module - Copyright = '(c) Microsoft Corporation. All rights reserved.' + Copyright = '(c) Microsoft Corporation. All rights reserved.' # Description of the functionality provided by this module - Description = 'Azure Landing Zones Powershell Module' + Description = 'Azure Landing Zones Powershell Module' CompatiblePSEditions = 'Core' # Minimum version of the PowerShell engine required by this module - PowerShellVersion = '7.4' + PowerShellVersion = '7.4' # Name of the PowerShell host required by this module # PowerShellHostName = '' @@ -53,7 +53,16 @@ # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @() + RequiredModules = @( + @{ + ModuleName = 'Az.Accounts' + ModuleVersion = '2.10.4' + }, + @{ + ModuleName = 'Az.Resources' + ModuleVersion = '6.5.0' + } + ) # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() @@ -71,19 +80,20 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( + FunctionsToExport = @( 'Test-AcceleratorRequirement', - 'Deploy-Accelerator' + 'Deploy-Accelerator', + 'Invoke-EABillingSPNPermissionsSetup' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() + CmdletsToExport = @() # Variables to export from this module - VariablesToExport = '*' + VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() + AliasesToExport = @() # DSC resources to export from this module # DscResourcesToExport = @() @@ -95,7 +105,7 @@ # FileList = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ + PrivateData = @{ PSData = @{ @@ -112,7 +122,7 @@ LicenseUri = 'https://github.com/Azure/ALZ-PowerShell-Module/blob/main/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://github.com/Azure/ALZ-Powershell-Module' + ProjectUri = 'https://github.com/Azure/ALZ-PowerShell-Module' # A URL to an icon representing this module. IconUri = 'https://raw.githubusercontent.com/Azure/ALZ-PowerShell-Module/main/docs/rsz_alzlogo.png' diff --git a/src/ALZ/ALZ.psm1 b/src/ALZ/ALZ.psm1 index 430dd753..f676807e 100644 --- a/src/ALZ/ALZ.psm1 +++ b/src/ALZ/ALZ.psm1 @@ -28,4 +28,4 @@ foreach ($file in @($public + $private)) { # export all public functions -Export-ModuleMember -Function $public.Basename -Alias * \ No newline at end of file +Export-ModuleMember -Function $public.Basename -Alias * diff --git a/src/ALZ/Private/Config-Helpers/Convert-BicepConfigToInputConfig.ps1 b/src/ALZ/Private/Config-Helpers/Convert-BicepConfigToInputConfig.ps1 index 2980f54f..f589c381 100644 --- a/src/ALZ/Private/Config-Helpers/Convert-BicepConfigToInputConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Convert-BicepConfigToInputConfig.ps1 @@ -14,13 +14,13 @@ function Convert-BicepConfigToInputConfig { if ($PSCmdlet.ShouldProcess("Parse Interface Variables into Config", "modify")) { $configItems = [PSCustomObject]@{} - if($appendToObject -ne $null) { + if ($appendToObject -ne $null) { $configItems = $appendToObject } Write-Verbose $validators - foreach($variable in $bicepConfig.inputs.PSObject.Properties) { + foreach ($variable in $bicepConfig.inputs.PSObject.Properties) { Write-Verbose "Parsing variable $($variable.Name)" $description = $variable.Value.description @@ -28,38 +28,38 @@ function Convert-BicepConfigToInputConfig { $configItem | Add-Member -NotePropertyName "Source" -NotePropertyValue $variable.Value.source $configItem | Add-Member -NotePropertyName "Value" -NotePropertyValue "" - if($variable.Value.PSObject.Properties.Name -contains "sourceInput") { + if ($variable.Value.PSObject.Properties.Name -contains "sourceInput") { $configItem | Add-Member -NotePropertyName "SourceInput" -NotePropertyValue $variable.Value.sourceInput } - if($variable.Value.PSObject.Properties.Name -contains "pattern") { + if ($variable.Value.PSObject.Properties.Name -contains "pattern") { $configItem | Add-Member -NotePropertyName "Pattern" -NotePropertyValue $variable.Value.pattern } - if($variable.Value.PSObject.Properties.Name -contains "process") { + if ($variable.Value.PSObject.Properties.Name -contains "process") { $configItem | Add-Member -NotePropertyName "Process" -NotePropertyValue $variable.Value.process } - if($variable.Value.PSObject.Properties.Name -contains "default") { + if ($variable.Value.PSObject.Properties.Name -contains "default") { $defaultValue = $variable.Value.default $configItem | Add-Member -NotePropertyName "DefaultValue" -NotePropertyValue $defaultValue } - if($variable.Value.PSObject.Properties.Name -contains "validation") { + if ($variable.Value.PSObject.Properties.Name -contains "validation") { $validationType = $variable.Value.validation $validator = $validators.PSObject.Properties[$validationType].Value $description = "$description ($($validator.Description))" Write-Verbose "Adding $($variable.Value.validation) validation for $($variable.Name). Validation type: $($validator.Type)" - if($validator.Type -eq "AllowedValues"){ + if ($validator.Type -eq "AllowedValues") { $configItem | Add-Member -NotePropertyName "AllowedValues" -NotePropertyValue $validator.AllowedValues } - if($validator.Type -eq "Valid"){ + if ($validator.Type -eq "Valid") { $configItem | Add-Member -NotePropertyName "Valid" -NotePropertyValue $validator.Valid } $configItem | Add-Member -NotePropertyName "Validator" -NotePropertyValue $validationType } - if($variable.Value.PSObject.Properties.Name -contains "targets") { + if ($variable.Value.PSObject.Properties.Name -contains "targets") { $configItem | Add-Member -NotePropertyName "targets" -NotePropertyValue $variable.Value.targets } @@ -69,4 +69,4 @@ function Convert-BicepConfigToInputConfig { } return $configItems -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToInputConfig.ps1 b/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToInputConfig.ps1 index dfd3e424..dfde3d77 100644 --- a/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToInputConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToInputConfig.ps1 @@ -17,7 +17,7 @@ function Convert-HCLVariablesToInputConfig { if ($PSCmdlet.ShouldProcess("Parse HCL Variables into Config", "modify")) { $terraformVariables = & $hclParserToolPath $targetVariableFile | ConvertFrom-Json - if($terraformVariables.PSObject.Properties.Name -notcontains "variable") { + if ($terraformVariables.PSObject.Properties.Name -notcontains "variable") { Write-Verbose "No variables found in $targetVariableFile, skipping..." return $appendToObject } @@ -25,22 +25,22 @@ function Convert-HCLVariablesToInputConfig { Write-Verbose "Variables found in $targetVariableFile, processing..." $configItems = [PSCustomObject]@{} - if($appendToObject -ne $null) { + if ($appendToObject -ne $null) { $configItems = $appendToObject } - foreach($variable in $terraformVariables.variable.PSObject.Properties) { - if($variable.Value[0].PSObject.Properties.Name -contains "description") { + foreach ($variable in $terraformVariables.variable.PSObject.Properties) { + if ($variable.Value[0].PSObject.Properties.Name -contains "description") { $description = $variable.Value[0].description $validationTypeSplit = $description -split "\|" $hasValidation = $false - if($validationTypeSplit.Length -gt 1) { + if ($validationTypeSplit.Length -gt 1) { $description = $validationTypeSplit[0].Trim() } - if($validationTypeSplit.Length -eq 2) { + if ($validationTypeSplit.Length -eq 2) { $splitItem = $validationTypeSplit[1].Trim() $validationType = $splitItem $hasValidation = $true @@ -51,17 +51,17 @@ function Convert-HCLVariablesToInputConfig { $configItem | Add-Member -NotePropertyName "Value" -NotePropertyValue "" $configItem | Add-Member -NotePropertyName "Source" -NotePropertyValue "input" - if($variable.Value[0].PSObject.Properties.Name -contains "default") { + if ($variable.Value[0].PSObject.Properties.Name -contains "default") { $configItem | Add-Member -NotePropertyName "DefaultValue" -NotePropertyValue $variable.Value[0].default } - if($hasValidation) { + if ($hasValidation) { $validator = $validators.PSObject.Properties[$validationType].Value $description = "$description ($($validator.Description))" - if($validator.Type -eq "AllowedValues"){ + if ($validator.Type -eq "AllowedValues") { $configItem | Add-Member -NotePropertyName "AllowedValues" -NotePropertyValue $validator.AllowedValues } - if($validator.Type -eq "Valid"){ + if ($validator.Type -eq "Valid") { $configItem | Add-Member -NotePropertyName "Valid" -NotePropertyValue $validator.Valid } $configItem | Add-Member -NotePropertyName "Validator" -NotePropertyValue $validationType @@ -75,4 +75,4 @@ function Convert-HCLVariablesToInputConfig { } return $configItems -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Convert-ParametersToInputConfig.ps1 b/src/ALZ/Private/Config-Helpers/Convert-ParametersToInputConfig.ps1 index 4ba8458f..3f65b04b 100644 --- a/src/ALZ/Private/Config-Helpers/Convert-ParametersToInputConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Convert-ParametersToInputConfig.ps1 @@ -6,12 +6,12 @@ function Convert-ParametersToInputConfig { [hashtable] $parameters ) - foreach($parameterKey in $parameters.Keys) { + foreach ($parameterKey in $parameters.Keys) { $parameter = $parameters[$parameterKey] Write-Verbose "Processing parameter $parameterKey $(ConvertTo-Json $parameter -Depth 100)" - foreach($parameterAlias in $parameter.aliases) { - if($inputConfig.PsObject.Properties.Name -contains $parameterAlias) { + foreach ($parameterAlias in $parameter.aliases) { + if ($inputConfig.PsObject.Properties.Name -contains $parameterAlias) { Write-Verbose "Alias $parameterAlias exists in input config, renaming..." $configItem = $inputConfig.PSObject.Properties | Where-Object { $_.Name -eq $parameterAlias } $inputConfig | Add-Member -NotePropertyName $parameterKey -NotePropertyValue @{ @@ -23,17 +23,17 @@ function Convert-ParametersToInputConfig { } } - if($inputConfig.PsObject.Properties.Name -notcontains $parameterKey) { + if ($inputConfig.PsObject.Properties.Name -notcontains $parameterKey) { $variableValue = [Environment]::GetEnvironmentVariable("ALZ_$($parameterKey)") - if($null -eq $variableValue) { - if($parameter.type -eq "SwitchParameter") { + if ($null -eq $variableValue) { + if ($parameter.type -eq "SwitchParameter") { $variableValue = $parameter.value.IsPresent } else { $variableValue = $parameter.value } } - if($parameter.type -eq "SwitchParameter") { + if ($parameter.type -eq "SwitchParameter") { $variableValue = [bool]::Parse($variableValue) } Write-Verbose "Adding parameter $parameterKey with value $variableValue" @@ -45,4 +45,4 @@ function Convert-ParametersToInputConfig { } return $inputConfig -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Format-TokenizedConfigurationString.ps1 b/src/ALZ/Private/Config-Helpers/Format-TokenizedConfigurationString.ps1 index 1755d8b8..ee05dd62 100644 --- a/src/ALZ/Private/Config-Helpers/Format-TokenizedConfigurationString.ps1 +++ b/src/ALZ/Private/Config-Helpers/Format-TokenizedConfigurationString.ps1 @@ -23,4 +23,4 @@ function Format-TokenizedConfigurationString { } return $returnValue -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Get-ALZConfig.ps1 b/src/ALZ/Private/Config-Helpers/Get-ALZConfig.ps1 index 67ed1c56..90b12170 100644 --- a/src/ALZ/Private/Config-Helpers/Get-ALZConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Get-ALZConfig.ps1 @@ -8,19 +8,19 @@ function Get-ALZConfig { [string] $hclParserToolPath = "" ) - if(!(Test-Path $configFilePath)) { + if (!(Test-Path $configFilePath)) { Write-Error "The config file does not exist at $configFilePath" throw "The config file does not exist at $configFilePath" } - if($null -eq $inputConfig) { + if ($null -eq $inputConfig) { $inputConfig = [PSCustomObject]@{} } # Import the config and transform it to a PowerShell object $extension = (Get-Item -Path $configFilePath).Extension.ToLower() $config = $null - if($extension -eq ".yml" -or $extension -eq ".yaml") { + if ($extension -eq ".yml" -or $extension -eq ".yaml") { if (!(Get-Module -ListAvailable -Name powershell-Yaml)) { Write-Host "Installing YAML module" Install-Module powershell-Yaml -Force @@ -33,7 +33,7 @@ function Get-ALZConfig { throw $errorMessage } - } elseif($extension -eq ".json") { + } elseif ($extension -eq ".json") { try { $config = [PSCustomObject](Get-Content -Path $configFilePath | ConvertFrom-Json) } catch { @@ -41,7 +41,7 @@ function Get-ALZConfig { Write-Error $errorMessage throw $errorMessage } - } elseif($extension -eq ".tfvars") { + } elseif ($extension -eq ".tfvars") { try { $config = [PSCustomObject](& $hclParserToolPath $configFilePath | ConvertFrom-Json) } catch { @@ -55,7 +55,7 @@ function Get-ALZConfig { Write-Verbose "Config file loaded from $configFilePath with $($config.PSObject.Properties.Name.Count) properties." - foreach($property in $config.PSObject.Properties) { + foreach ($property in $config.PSObject.Properties) { $inputConfig | Add-Member -NotePropertyName $property.Name -NotePropertyValue @{ Value = $property.Value Source = $extension @@ -63,4 +63,4 @@ function Get-ALZConfig { } return $inputConfig -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Get-AvailabilityZonesSupport.ps1 b/src/ALZ/Private/Config-Helpers/Get-AvailabilityZonesSupport.ps1 index 4095ff0e..80eebd6b 100644 --- a/src/ALZ/Private/Config-Helpers/Get-AvailabilityZonesSupport.ps1 +++ b/src/ALZ/Private/Config-Helpers/Get-AvailabilityZonesSupport.ps1 @@ -12,4 +12,4 @@ function Get-AvailabilityZonesSupport { $jsonZones = ConvertTo-Json $zone.zones -Depth 10 Write-Verbose "Zones for $region are $jsonZones" return $zone.zones -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 b/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 index 384e1eef..baf8737a 100644 --- a/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 +++ b/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 @@ -5,38 +5,38 @@ function Get-AzureRegionData { ) $terraformCode = @' -terraform { - required_providers { - azapi = { - source = "azure/azapi" - version = "~> 2.0" + terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.0" + } + } } - } -} -module "regions" { - source = "Azure/avm-utl-regions/azurerm" - version = "0.3.0" - use_cached_data = false - availability_zones_filter = false - recommended_filter = false -} + module "regions" { + source = "Azure/avm-utl-regions/azurerm" + version = "0.3.0" + use_cached_data = false + availability_zones_filter = false + recommended_filter = false + } -locals { - regions = { for region in module.regions.regions_by_name : region.name => { - display_name = region.display_name - zones = region.zones == null ? [] : [for zone in region.zones : tostring(zone)] + locals { + regions = { for region in module.regions.regions_by_name : region.name => { + display_name = region.display_name + zones = region.zones == null ? [] : [for zone in region.zones : tostring(zone)] + } + } } - } -} -output "regions_and_zones" { - value = local.regions -} + output "regions_and_zones" { + value = local.regions + } '@ $regionFolder = Join-Path $toolsPath "azure-regions" - if(Test-Path $regionFolder) { + if (Test-Path $regionFolder) { Remove-Item $regionFolder -Recurse -Force } @@ -55,7 +55,7 @@ output "regions_and_zones" { $zonesSupport = @() $supportedRegions = @() - foreach($region in $regionsAndZones.PSObject.Properties) { + foreach ($region in $regionsAndZones.PSObject.Properties) { $supportedRegions += $region.Name $zonesSupport += @{ region = $region.Name @@ -67,4 +67,4 @@ output "regions_and_zones" { zonesSupport = $zonesSupport supportedRegions = $supportedRegions } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Remove-TerraformMetaFileSet.ps1 b/src/ALZ/Private/Config-Helpers/Remove-TerraformMetaFileSet.ps1 index d2f0d035..72023f5c 100644 --- a/src/ALZ/Private/Config-Helpers/Remove-TerraformMetaFileSet.ps1 +++ b/src/ALZ/Private/Config-Helpers/Remove-TerraformMetaFileSet.ps1 @@ -18,8 +18,8 @@ function Remove-TerraformMetaFileSet { [switch]$writeVerboseLogs ) - if($PSCmdlet.ShouldProcess("Remove files", "modify")) { - if($terraformFilesOrFoldersToRemove.Length -eq 0 ) { + if ($PSCmdlet.ShouldProcess("Remove files", "modify")) { + if ($terraformFilesOrFoldersToRemove.Length -eq 0 ) { Write-Verbose "No folders or files specified, so not removing aything from $path" return } @@ -27,12 +27,12 @@ function Remove-TerraformMetaFileSet { $filesAndFolders = Get-ChildItem -Path $path -Force foreach ($fileOrFolder in $filesAndFolders) { - if($terraformFilesOrFoldersToRemove -contains $fileOrFolder.Name) { - if($writeVerboseLogs) { + if ($terraformFilesOrFoldersToRemove -contains $fileOrFolder.Name) { + if ($writeVerboseLogs) { Write-Verbose "Exact Match - Removing: $($fileOrFolder.FullName)" } Remove-Item -Path $fileOrFolder.FullName -Force -Recurse | Out-Null } } } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Remove-UnrequiredFileSet.ps1 b/src/ALZ/Private/Config-Helpers/Remove-UnrequiredFileSet.ps1 index ebb72a52..b6f3b46a 100644 --- a/src/ALZ/Private/Config-Helpers/Remove-UnrequiredFileSet.ps1 +++ b/src/ALZ/Private/Config-Helpers/Remove-UnrequiredFileSet.ps1 @@ -11,8 +11,8 @@ function Remove-UnrequiredFileSet { [switch]$writeVerboseLogs ) - if($PSCmdlet.ShouldProcess("Remove files", "modify")) { - if($foldersOrFilesToRetain.Length -eq 0 -and $subFoldersOrFilesToRemove.Length -eq 0) { + if ($PSCmdlet.ShouldProcess("Remove files", "modify")) { + if ($foldersOrFilesToRetain.Length -eq 0 -and $subFoldersOrFilesToRemove.Length -eq 0) { Write-Verbose "No folders or files to retain specified, so not removing aything from $path" return } @@ -25,8 +25,8 @@ function Remove-UnrequiredFileSet { $folderRelativePath = $file.Directory.FullName.Replace($path, "").Replace("\", "/").TrimStart("/") foreach ($folderOrFileToRetain in $foldersOrFilesToRetain) { # If we have an exact match of the file name and path, always retain it. - if($folderOrFileToRetain.TrimStart("./") -eq $fileRelativePath) { - if($writeVerboseLogs) { + if ($folderOrFileToRetain.TrimStart("./") -eq $fileRelativePath) { + if ($writeVerboseLogs) { Write-Verbose "Exact Match - Retaining: $fileRelativePath at $($file.FullName)" } $filesToRetain += $file.FullName @@ -36,14 +36,14 @@ function Remove-UnrequiredFileSet { # If we match on a pattern, take into account the subfolders or files to remove. if ($fileRelativePath -like "$folderOrFileToRetain*") { $skipFile = $false - foreach($subfolderOrFileToRemove in $subFoldersOrFilesToRemove) { + foreach ($subfolderOrFileToRemove in $subFoldersOrFilesToRemove) { if ($file.Name -eq $subfolderOrFileToRemove -or $file.Directory.Name -eq $subfolderOrFileToRemove -or $fileRelativePath.EndsWith("/$subfolderOrFileToRemove") -or $folderRelativePath.EndsWith("/$subfolderOrFileToRemove")) { $skipFile = $true } } - if(!$skipFile) { - if($writeVerboseLogs) { + if (!$skipFile) { + if ($writeVerboseLogs) { Write-Verbose "Pattern Match - Retaining: $fileRelativePath at $($file.FullName)" } $filesToRetain += $file.FullName @@ -52,9 +52,9 @@ function Remove-UnrequiredFileSet { } } - foreach($file in $files) { - if($filesToRetain -notcontains $file.FullName) { - if($writeVerboseLogs) { + foreach ($file in $files) { + if ($filesToRetain -notcontains $file.FullName) { + if ($writeVerboseLogs) { Write-Verbose "Removing: $($file.FullName)" } Remove-Item -Path $file.FullName -Force | Out-Null @@ -62,11 +62,11 @@ function Remove-UnrequiredFileSet { } $folders = Get-ChildItem -Path $path -Directory -Recurse -Force - foreach($folder in $folders) { - if(Test-Path $folder.FullName) { + foreach ($folder in $folders) { + if (Test-Path $folder.FullName) { $folderItems = Get-ChildItem -Path $folder.FullName -Recurse -File -Force - if($folderItems.Count -eq 0) { - if($writeVerboseLogs) { + if ($folderItems.Count -eq 0) { + if ($writeVerboseLogs) { Write-Verbose "Removing empty folder: $($folder.FullName)" } Remove-Item -Path $folder.FullName -Force -Recurse | Out-Null @@ -74,4 +74,4 @@ function Remove-UnrequiredFileSet { } } } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Set-ComputedConfiguration.ps1 b/src/ALZ/Private/Config-Helpers/Set-ComputedConfiguration.ps1 index d0095023..a7b1277b 100644 --- a/src/ALZ/Private/Config-Helpers/Set-ComputedConfiguration.ps1 +++ b/src/ALZ/Private/Config-Helpers/Set-ComputedConfiguration.ps1 @@ -14,7 +14,7 @@ function Set-ComputedConfiguration { if ($configKey.Value.Value -is [array]) { $formattedValues = @() - foreach($formatString in $configKey.Value.Value) { + foreach ($formatString in $configKey.Value.Value) { $formattedValues += Format-TokenizedConfigurationString -tokenizedString $formatString -configuration $configuration } @@ -37,4 +37,4 @@ function Set-ComputedConfiguration { } } } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Write-JsonFile.ps1 b/src/ALZ/Private/Config-Helpers/Write-JsonFile.ps1 index fba6e228..e3910b4b 100644 --- a/src/ALZ/Private/Config-Helpers/Write-JsonFile.ps1 +++ b/src/ALZ/Private/Config-Helpers/Write-JsonFile.ps1 @@ -10,7 +10,7 @@ function Write-JsonFile { if ($PSCmdlet.ShouldProcess("Download Terraform Tools", "modify")) { - if(Test-Path $jsonFilePath) { + if (Test-Path $jsonFilePath) { Remove-Item -Path $jsonFilePath } @@ -18,7 +18,7 @@ function Write-JsonFile { foreach ($configKey in $configuration.PsObject.Properties | Sort-Object Name) { foreach ($target in $configKey.Value.Targets) { - if($target.Destination -eq "Environment") { + if ($target.Destination -eq "Environment") { $environmentVariables.$($target.Name) = $configKey.Value.Value } } @@ -27,4 +27,4 @@ function Write-JsonFile { $json = ConvertTo-Json -InputObject $environmentVariables -Depth 100 $json | Out-File -FilePath $jsonFilePath } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 b/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 index 65ca0b53..dac277a8 100644 --- a/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 +++ b/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 @@ -13,26 +13,26 @@ function Write-TfvarsJsonFile { if ($PSCmdlet.ShouldProcess("Download Terraform Tools", "modify")) { - if(Test-Path $tfvarsFilePath) { + if (Test-Path $tfvarsFilePath) { Remove-Item -Path $tfvarsFilePath } $jsonObject = [ordered]@{} - foreach($configurationProperty in $configuration.PSObject.Properties | Sort-Object Name) { - if($skipItems -contains $configurationProperty.Name) { + foreach ($configurationProperty in $configuration.PSObject.Properties | Sort-Object Name) { + if ($skipItems -contains $configurationProperty.Name) { Write-Verbose "Skipping configuration property: $($configurationProperty.Name)" continue } - + $configurationValue = $configurationProperty.Value.Value - if($null -ne $configurationValue -and $configurationValue.ToString() -eq "sourced-from-env") { + if ($null -ne $configurationValue -and $configurationValue.ToString() -eq "sourced-from-env") { Write-Verbose "Sourced from env var: $($configurationProperty.Name)" continue } - if($configurationProperty.Value.Validator -eq "configuration_file_path") { + if ($configurationProperty.Value.Validator -eq "configuration_file_path") { $configurationValue = [System.IO.Path]::GetFileName($configurationValue) } diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Copy-ParameterFileCollection.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Copy-ParameterFileCollection.ps1 index f730dc0c..99f48a21 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Copy-ParameterFileCollection.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Copy-ParameterFileCollection.ps1 @@ -23,4 +23,4 @@ function Copy-ParametersFileCollection { Write-Warning "The file $sourcePath does not exist." } } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 index 0696c183..701f7f7a 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 @@ -11,7 +11,7 @@ function Get-ExistingLocalRelease { $path = "" $checkPath = Join-Path $targetDirectory $targetFolder $checkFolders = Get-ChildItem -Path $checkPath -Directory - if($null -ne $checkFolders) { + if ($null -ne $checkFolders) { $checkFolders = $checkFolders | Sort-Object { $_.Name } -Descending $mostRecentCheckFolder = $checkFolders[0] @@ -26,4 +26,4 @@ function Get-ExistingLocalRelease { releaseTag = $releaseTag path = $path } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 index 4d96d8d8..8d5fd1bf 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 @@ -26,10 +26,10 @@ function Invoke-Terraform { if ($PSCmdlet.ShouldProcess("Apply Terraform", "modify")) { # Check and Set Subscription ID $removeSubscriptionId = $false - if($null -eq $env:ARM_SUBSCRIPTION_ID -or $env:ARM_SUBSCRIPTION_ID -eq "") { + if ($null -eq $env:ARM_SUBSCRIPTION_ID -or $env:ARM_SUBSCRIPTION_ID -eq "") { Write-Verbose "Setting environment variable ARM_SUBSCRIPTION_ID" $subscriptionId = $(az account show --query id -o tsv) - if($null -eq $subscriptionId -or $subscriptionId -eq "") { + if ($null -eq $subscriptionId -or $subscriptionId -eq "") { Write-Error "Subscription ID not found. Please ensure you are logged in to Azure and have selected a subscription. Use 'az account show' to check." return } @@ -40,11 +40,11 @@ function Invoke-Terraform { terraform -chdir="$moduleFolderPath" init $action = "apply" - if($destroy) { + if ($destroy) { $action = "destroy" } - if(!$silent) { + if (!$silent) { Write-InformationColored "Terraform init has completed, now running the $action..." -ForegroundColor Green -NewLineBefore -InformationAction Continue } @@ -60,7 +60,7 @@ function Invoke-Terraform { $arguments += "plan" $arguments += "-out=$planFileName" $arguments += "-input=false" - if($tfvarsFileName -ne "") { + if ($tfvarsFileName -ne "") { $arguments += "-var-file=$tfvarsFileName" } @@ -68,7 +68,7 @@ function Invoke-Terraform { $arguments += "-destroy" } - if(!$silent) { + if (!$silent) { Write-InformationColored "Running Plan Command for $action : $command $arguments" -ForegroundColor Green -NewLineBefore -InformationAction Continue & $command $arguments } else { @@ -79,23 +79,23 @@ function Invoke-Terraform { # Stop and display timer $StopWatch.Stop() - if(!$silent) { + if (!$silent) { Write-InformationColored "Time taken to complete Terraform plan:" -ForegroundColor Green -NewLineBefore -InformationAction Continue } $StopWatch.Elapsed | Format-Table - if($exitCode -ne 0) { + if ($exitCode -ne 0) { Write-InformationColored "Terraform plan for $action failed with exit code $exitCode. Please review the error and try again or raise an issue." -ForegroundColor Red -NewLineBefore -InformationAction Continue throw "Terraform plan failed with exit code $exitCode. Please review the error and try again or raise an issue." } - if(!$autoApprove) { + if (!$autoApprove) { Write-InformationColored "Terraform plan has completed, please review the plan and confirm you wish to continue." -ForegroundColor Yellow -NewLineBefore -InformationAction Continue $choices = [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No") $message = "Please confirm you wish to apply the plan." $title = "Confirm Terraform plan" $resultIndex = $host.ui.PromptForChoice($title, $message, $choices, 0) - if($resultIndex -eq 1) { + if ($resultIndex -eq 1) { Write-InformationColored "You have chosen not to apply the plan. Exiting..." -ForegroundColor Red -NewLineBefore -InformationAction Continue return } @@ -113,7 +113,7 @@ function Invoke-Terraform { $arguments += "-input=false" $arguments += "$planFileName" - if(!$silent) { + if (!$silent) { Write-InformationColored "Running Apply Command for $action : $command $arguments" -ForegroundColor Green -NewLineBefore -InformationAction Continue & $command $arguments } else { @@ -125,7 +125,7 @@ function Invoke-Terraform { $currentAttempt = 0 $maxAttempts = 5 - while($exitCode -ne 0 -and $currentAttempt -lt $maxAttempts) { + while ($exitCode -ne 0 -and $currentAttempt -lt $maxAttempts) { Write-InformationColored "Terraform $action failed with exit code $exitCode. This is likely a transient issue, so we are retrying..." -ForegroundColor Yellow -NewLineBefore -InformationAction Continue $currentAttempt++ $command = "terraform" @@ -134,7 +134,7 @@ function Invoke-Terraform { $arguments += "apply" $arguments += "-auto-approve" $arguments += "-input=false" - if($tfvarsFileName -ne "") { + if ($tfvarsFileName -ne "") { $arguments += "-var-file=$tfvarsFileName" } if ($destroy) { @@ -146,24 +146,24 @@ function Invoke-Terraform { $exitCode = $LASTEXITCODE } - if($removeSubscriptionId) { + if ($removeSubscriptionId) { Write-Verbose "Removing environment variable ARM_SUBSCRIPTION_ID that was set prior to this run" $env:ARM_SUBSCRIPTION_ID = $null } # Stop and display timer $StopWatch.Stop() - if(!$silent) { + if (!$silent) { Write-InformationColored "Time taken to complete Terraform apply:" -ForegroundColor Green -NewLineBefore -InformationAction Continue } $StopWatch.Elapsed | Format-Table - if($exitCode -ne 0) { + if ($exitCode -ne 0) { Write-InformationColored "Terraform $action failed with exit code $exitCode after $maxAttempts attempts. Please review the error and try again or raise an issue." -ForegroundColor Red -NewLineBefore -InformationAction Continue throw "Terraform $action failed with exit code $exitCode after $maxAttempts attempts. Please review the error and try again or raise an issue." } else { - if($output -ne "") { - if($outputFilePath -eq "") { + if ($output -ne "") { + if ($outputFilePath -eq "") { $outputFilePath = Join-Path $moduleFolderPath "output.json" } $command = "terraform" @@ -179,4 +179,4 @@ function Invoke-Terraform { } } } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 index 7b70bd9b..72814116 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 @@ -281,4 +281,4 @@ function New-Bootstrap { Write-InformationColored "Bootstrap has completed successfully! Thanks for using our tool. Head over to Phase 3 in the documentation to continue..." -ForegroundColor Green -NewLineBefore -InformationAction Continue } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-FolderStructure.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-FolderStructure.ps1 index a3eedc4e..3cffdf7b 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-FolderStructure.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-FolderStructure.ps1 @@ -29,22 +29,22 @@ function New-FolderStructure { if ($PSCmdlet.ShouldProcess("ALZ-Terraform module configuration", "modify")) { Write-Verbose "Downloading modules to $targetDirectory" - if(!($release.StartsWith("v")) -and ($release -ne "latest")) { + if (!($release.StartsWith("v")) -and ($release -ne "latest")) { $release = "v$release" } $releaseTag = "" $path = "" - if($overrideSourceDirectoryPath -ne "") { + if ($overrideSourceDirectoryPath -ne "") { $releaseTag = "local" $path = Join-Path $targetDirectory $targetFolder $releaseTag - if((Test-Path $path) -and !$replaceFiles) { + if ((Test-Path $path) -and !$replaceFiles) { Write-Verbose "Folder $path already exists, so not copying files." } else { Write-InformationColored "Copying files from $overrideSourceDirectoryPath to $path" -ForegroundColor Green -InformationAction Continue - if(!(Test-Path $path)) { + if (!(Test-Path $path)) { New-Item -Path $path -ItemType "Directory" } Copy-Item -Path "$overrideSourceDirectoryPath/$sourceFolder/*" -Destination "$path" -Recurse -Force | Out-String | Write-Verbose @@ -62,4 +62,4 @@ function New-FolderStructure { releaseTag = $releaseTag } } -} \ No newline at end of file +} diff --git a/src/ALZ/Private/Shared/Test-Utility-Functions.ps1 b/src/ALZ/Private/Shared/Test-Utility-Functions.ps1 index 414771b7..69ca75ff 100644 --- a/src/ALZ/Private/Shared/Test-Utility-Functions.ps1 +++ b/src/ALZ/Private/Shared/Test-Utility-Functions.ps1 @@ -5,4 +5,4 @@ function Get-PSVersion { $PSVersionTable } function Get-ScriptRoot { $PSScriptRoot } # Used to allow mocking of the Get-Module AZ -function Get-AZVersion { Get-Module -Name Az -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 } \ No newline at end of file +function Get-AZVersion { Get-Module -Name Az -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 } diff --git a/src/ALZ/Private/Shared/Write-InformationColored.ps1 b/src/ALZ/Private/Shared/Write-InformationColored.ps1 index b3771741..43c22c8e 100644 --- a/src/ALZ/Private/Shared/Write-InformationColored.ps1 +++ b/src/ALZ/Private/Shared/Write-InformationColored.ps1 @@ -9,7 +9,7 @@ function Write-InformationColored { [switch]$NewLineBefore ) - if($NewLineBefore) { + if ($NewLineBefore) { $MessageData = "$([Environment]::NewLine)$MessageData" } @@ -21,4 +21,4 @@ function Write-InformationColored { } Write-Information $msg -} \ No newline at end of file +} diff --git a/src/ALZ/Public/Invoke-EABillingSPNPermissionsSetup.ps1 b/src/ALZ/Public/Invoke-EABillingSPNPermissionsSetup.ps1 new file mode 100644 index 00000000..c6ae702f --- /dev/null +++ b/src/ALZ/Public/Invoke-EABillingSPNPermissionsSetup.ps1 @@ -0,0 +1,182 @@ +function Invoke-EABillingSPNPermissionsSetup { + <# +.SYNOPSIS +Creates a new SPN, or uses an existing SPN/MI, and assigns it the 'SubscriptionCreator' role to it to allow it to create subscriptions in the specified EA billing enrollment account. + +.DESCRIPTION +Creates a new SPN, or uses an existing SPN/MI, and assigns it the 'SubscriptionCreator' role to it to allow it to create subscriptions in the specified EA billing enrollment account. + +.EXAMPLE +# Create a new SPN and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters +/Invoke-EABillingSPNPermissionsSetup.ps1 -eaEnrollmentNumber "123456" -eaEnrollmentAccountNumber "987654" + +# Create a new SPN and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaBillingAccountResourceId' parameter +./Invoke-EABillingSPNPermissionsSetup.ps1 -eaBillingAccountResourceId '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' + +# Create a new SPN, with a custom name, and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters +./Invoke-EABillingSPNPermissionsSetup.ps1 -eaEnrollmentNumber "123456" -eaEnrollmentAccountNumber "987654" -newSpnDisplayName 'spn-lz-sub-vending-custom-name' + +# Create a new SPN, with a custom name, and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaBillingAccountResourceId' parameter +./Invoke-EABillingSPNPermissionsSetup.ps1 -eaBillingAccountResourceId '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' -newSpnDisplayName 'spn-lz-sub-vending-custom-name' + +# Use an existing SPN/MI and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters +./Invoke-EABillingSPNPermissionsSetup.ps1 -eaEnrollmentNumber "123456" -eaEnrollmentAccountNumber "987654" -existingSpnMiObjectId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + +# Use an existing SPN/MI and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaBillingAccountResourceId' parameter +./Invoke-EABillingSPNPermissionsSetup.ps1 -eaBillingAccountResourceId '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' -existingSpnMiObjectId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' +.NOTES +# Release notes 22/12/2022 - V1.0.0: +- Initial release. + +# Release notes 23/12/2022 - V1.1.0: +- Added simplified inputs for the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters to form the 'eaBillingAccountResourceId' parameter value, instead of having to provide the full resource ID in the 'eaBillingAccountResourceId' parameter. +#> + + # Check for pre-reqs + #Requires -PSEdition Core + + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 1, HelpMessage = "Provide the EA enrollment number that the SPN will be granted the 'SubscriptionCreator' role upon. Example: '1234567'. This parameter is only used if the 'eaBillingAccountResourceId' parameter is not provided. It's value is used to create the 'eaBillingAccountResourceId' parameter value, that looks like this: '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' (this parameter value is the middle numerical value).")] + [string] + $eaEnrollmentNumber, + + [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 2, HelpMessage = "Provide the EA enrollment Account Number/ID that the SPN will be granted the 'SubscriptionCreator' role upon. Example: '987654'. This parameter is only used if the 'eaBillingAccountResourceId' parameter is not provided. It's value is used to create the 'eaBillingAccountResourceId' parameter value, that looks like this: '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' (this parameter value is the middle numerical value).")] + [string] + $eaEnrollmentAccountNumber, + + [Parameter(ParameterSetName = "Advanced", Mandatory = $false, Position = 4, HelpMessage = "Provide the EA enrollment/billing account ID that the SPN will be granted the 'SubscriptionCreator' role upon. Example: '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/123456'")] + [string] + $eaBillingAccountResourceId, + + [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 3, HelpMessage = "(Optional) Provide an existing Service Principal Name (SPN) (aka Enterprise Application) 'Object ID' to grant the 'SubscriptionCreator' role to on the specified billing account instead of creating a new one. If left blank a new SPN will be created. This can also be the object ID of a Managed Identity's SPN.")] + [string] + $existingSpnMiObjectId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + + [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 5, HelpMessage = "(Optional) Provide a Display Name for the new Service Principal (SPN) (aka Enterprise Application) to be created. If left blank the default value of 'spn-lz-sub-vending' will be used.")] + [string] + $newSpnDisplayName = "spn-lz-sub-vending" + ) + + # Checks + Write-Host "Checking inputs..." -ForegroundColor Cyan + Write-Host "" + + # Check $eaBillingAccountResourceId is valid and populate from $eaEnrollmentNumber and $eaEnrollmentAccountNumber if not provided + if ($eaBillingAccountResourceId -eq $null -or $eaBillingAccountResourceId -eq "") { + Write-Host "eaBillingAccountResourceId paramter value not set, forming parameter value from eaEnrollmentNumber and eaEnrollmentAccountNumber parameters..." -ForegroundColor Magenta + if ($eaEnrollmentNumber -eq $null -or $eaEnrollmentAccountNumber -eq $null -or $eaEnrollmentNumber -eq '' -or $eaEnrollmentAccountNumber -eq '') { + throw "No values provdided for the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters. These parameters are required if the 'eaBillingAccountResourceId' parameter is not provided. Please provide values for these parameters and try again." + } + $eaBillingAccountResourceId = "/providers/Microsoft.Billing/billingAccounts/$eaEnrollmentNumber/enrollmentAccounts/$eaEnrollmentAccountNumber" + Write-Host "eaBillingAccountResourceId parameter value set to '$($eaBillingAccountResourceId)'" -ForegroundColor Green + Write-Host "" + } + + # Check $eaBillingAccountResourceId is valid and exists + Write-Host "EA billing account parameters provided..." -ForegroundColor Cyan + Write-Host "Checking the specified EA billing account ID '$($eaBillingAccountResourceId)' exists..." -ForegroundColor Yellow + + if ($null -ne $eaBillingAccountResourceId -and $eaBillingAccountResourceId -ne "") { + $geteaBillingAccountResourceId = Invoke-AzRestMethod -Method GET -Path "$($eaBillingAccountResourceId)?api-version=2019-10-01-preview" -ErrorAction SilentlyContinue + + if ($geteaBillingAccountResourceId.StatusCode -ne 200) { + Write-Error "HTTP Status Code: $($geteaBillingAccountResourceId.StatusCode)" + Write-Error "HTTP Repsone Content: $($geteaBillingAccountResourceId.Content)" + throw "The specified EA billing account ID '$($eaBillingAccountResourceId)' does not exist. Please check the value and try again. Also ensure you are logged in as the EA Account Owner for the specified EA billing account." + } else { + Write-Host "The specified EA billing account ID '$($eaBillingAccountResourceId)' exists. Continuing..." -ForegroundColor Green + Write-Host "" + } + } + + # Check $existingSpnMiObjectId is valid and exists + if ($existingSpnMiObjectId -ne $null -and $existingSpnMiObjectId -ne "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") { + Write-Host "Existing SPN/MI provided..." -ForegroundColor Cyan + Write-Host "Checking the specified SPN/MI 'Object ID' '$($existingSpnMiObjectId)' exists..." -ForegroundColor Yellow + $getexistingSpnMiObjectId = Get-AzADServicePrincipal -ObjectId $existingSpnMiObjectId -ErrorAction SilentlyContinue + + if ($null -eq $getexistingSpnMiObjectId) { + throw "The specified SPN/MI 'Object ID' '$($existingSpnMiObjectId)' does not exist. Please check the value and try again." + } else { + Write-Host "The specified SPN/MI 'Object ID' '$($existingSpnMiObjectId)' exists with a Display Name of: '$($getexistingSpnMiObjectId.DisplayName)' with a Type of: '$($getexistingSpnMiObjectId.ServicePrincipalType)'. Continuing..." -ForegroundColor Green + Write-Host "" + $finalSpnMiObjectId = $getexistingSpnMiObjectId.Id + $finalSpnMiAppId = $getexistingSpnMiObjectId.AppId + $finalSpnMiDisplayName = $getexistingSpnMiObjectId.DisplayName + $finalSpnMiType = $getexistingSpnMiObjectId.ServicePrincipalType + } + } else { + # Create a new SPN (aka Enterprise Application) as no existing SPN/MI was provided via the $existingSpnMiObjectId parameter + Write-Host "No Existing SPN/MI provided. Proceeding to create a new SPN (aka Enterprise Application)..." -ForegroundColor Cyan + Write-Host "Creating a new SPN (aka Enterprise Application) with a Display Name of '$($newSpnDisplayName)'..." -ForegroundColor Yellow + + $newSpn = New-AzADServicePrincipal -DisplayName $newSpnDisplayName -Description "Service Principal Name (SPN) for the Landing Zone Subscription Vending. See https://aka.ms/lz-vending/bicep or https://aka.ms/lz-vending/tf for more information." -ErrorAction Stop + Write-Host "New SPN (aka Enterprise Application) created with a Display Name of '$($newSpn.DisplayName)' and an Object ID of '$($newSpn.Id)'." -ForegroundColor Green + Write-Host "" + + $finalSpnMiObjectId = $newSpn.Id + $finalSpnMiDisplayName = $newSpn.DisplayName + $finalSpnMiType = $newSpn.ServicePrincipalType + $finalSpnMiAppId = $newSpn.AppId + } + + ## convert this to a retry loop + # Start-Sleep -Seconds 15 + + # Grant SPN/MI access to the specified EA billing account + Write-Host "Pre-reqs passed and complete..." -ForegroundColor Cyan + Write-Host "Granting the 'SubscriptionCreator' role (ID: 'a0bcee42-bf30-4d1b-926a-48d21664ef71') on the EA Billing Account ID of: '$($eaBillingAccountResourceId)' to the AAD Object ID of: '$($finalSpnMiObjectId)' which has the Display Name of: '$($finalSpnMiDisplayName)'..." -ForegroundColor Yellow + + # Get the current AAD Tenant ID + $currentTenant = Get-AzTenant -ErrorAction Stop + + # Create GUID for role assignment name + $roleAssignmentName = New-Guid + + $roleAssignmentHashTable = [ordered]@{ + "properties" = @{ + "principalId" = "$finalSpnMiObjectId" + "roleDefinitionId" = "$eaBillingAccountResourceId/billingRoleDefinitions/a0bcee42-bf30-4d1b-926a-48d21664ef71" + "principalTenantId" = "$($currentTenant.TenantId)" + } + } + $roleAssignmentPayloadJson = $roleAssignmentHashTable | ConvertTo-Json -Depth 100 + + $grantRbac = Invoke-AzRestMethod -Method PUT -Path "$($eaBillingAccountResourceId)/billingRoleAssignments/$($roleAssignmentName)?api-version=2019-10-01-preview" -Payload $roleAssignmentPayloadJson -ErrorAction SilentlyContinue + + # Create variables for retry loop + $retryCount = 0 + $retryLimit = 10 + $retryDelay = 5 + + + if ($grantRbac.StatusCode -eq 400 -and $grantRbac.Content.Contains("are not valid")) { + while ($retryCount -le $retryLimit) { + Write-Host "The 'SubscriptionCreator' role has not been granted to the SPN/MI. Retrying in $retryDelay seconds to allow platform replication to occur..." -ForegroundColor Magenta + + Start-Sleep -Seconds $retryDelay + $retryCount++ + + $grantRbac = Invoke-AzRestMethod -Method PUT -Path "$($eaBillingAccountResourceId)/billingRoleAssignments/$($roleAssignmentName)?api-version=2019-10-01-preview" -Payload $roleAssignmentPayloadJson -ErrorAction SilentlyContinue + + if ($grantRbac.StatusCode -eq 200) { + break + } + } + } + if ($grantRbac.StatusCode -ne 200) { + Write-Error "HTTP Status Code: $($grantRbac.StatusCode)" + Write-Error "HTTP Repsone Content: $($grantRbac.Content)" + throw "An error occurred while attempting to grant the 'SubscriptionCreator' role to the SPN/MI. Please check the error message above and try again." + } else { + Write-Host "The 'SubscriptionCreator' role has been granted to the SPN/MI." -ForegroundColor Green + Write-Host "" + Write-Host "The SPN/MI 'Object ID' is: '$($finalSpnMiObjectId)'" -ForegroundColor Green + Write-Host "The SPN/MI 'App ID' is: '$($finalSpnMiAppId)'" -ForegroundColor Green + Write-Host "The SPN/MI 'Display Name' is: '$($finalSpnMiDisplayName)'" -ForegroundColor Green + Write-Host "The SPN/MI 'Type' is: '$($finalSpnMiType)'" -ForegroundColor Green + } + + return +} diff --git a/src/PSScriptAnalyzerSettings.psd1 b/src/PSScriptAnalyzerSettings.psd1 index 90f2ac2b..b626d6e7 100644 --- a/src/PSScriptAnalyzerSettings.psd1 +++ b/src/PSScriptAnalyzerSettings.psd1 @@ -18,9 +18,9 @@ #________________________________________ #ExcludeRules #Specify ExcludeRules when you want to exclude a certain rule from the the default set of rules. - ExcludeRules = @( - 'PSAvoidUsingWriteHost', - 'PSReviewUnusedParameter' + ExcludeRules = @( + 'PSAvoidUsingWriteHost', + 'PSReviewUnusedParameter' ) #________________________________________ #Rules diff --git a/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 b/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 index bc742ede..28f2fc2f 100644 --- a/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 +++ b/src/Tests/Unit/Private/Set-ComputedConfiguration.Tests.ps1 @@ -34,8 +34,8 @@ InModuleScope 'ALZ' { Name = "Setting2" Destination = "Environment" }) - Source = "calculated" - Value = "{%Setting1%}" + Source = "calculated" + Value = "{%Setting1%}" } } @@ -45,11 +45,11 @@ InModuleScope 'ALZ' { It 'Computed, Processed array values replace values correctly' { $configuration = [pscustomobject]@{ - Nested = [pscustomobject]@{ + Nested = [pscustomobject]@{ Source = "calculated" Description = "A Test Value" - Process = '@($args | Select-Object -Unique)' - Value = @( + Process = '@($args | Select-Object -Unique)' + Value = @( "1", "1", "3" @@ -68,11 +68,11 @@ InModuleScope 'ALZ' { It 'Computed, Processed array values replace values correctly in a case insensitive deduplication.' { $configuration = [pscustomobject]@{ - Nested = [pscustomobject]@{ + Nested = [pscustomobject]@{ Source = "calculated" Description = "A Test Value" - Process = '@($args | ForEach-Object { $_.ToLower() } | Select-Object -Unique)' - Value = @( + Process = '@($args | ForEach-Object { $_.ToLower() } | Select-Object -Unique)' + Value = @( "A", "a", "A", @@ -92,11 +92,11 @@ InModuleScope 'ALZ' { It 'Computed, Processed array values replace values correctly and keep array type when only one item remains.' { $configuration = [pscustomobject]@{ - Nested = [pscustomobject]@{ + Nested = [pscustomobject]@{ Source = "calculated" Description = "A Test Value" - Process = '@($args | Select-Object -Unique)' - Value = @( + Process = '@($args | Select-Object -Unique)' + Value = @( "1", "1", "1" @@ -115,10 +115,10 @@ InModuleScope 'ALZ' { It 'Computed, Processed values replace values correctly' { $configuration = [pscustomobject]@{ - Nested = [pscustomobject]@{ + Nested = [pscustomobject]@{ Source = "calculated" Description = "A Test Value" - Process = '($args[0] -eq "eastus") ? "eastus2" : ($args[0] -eq "eastus2") ? "eastus" : $args[0]' + Process = '($args[0] -eq "eastus") ? "eastus2" : ($args[0] -eq "eastus2") ? "eastus" : $args[0]' Value = "eastus" Targets = @( [pscustomobject]@{ @@ -134,10 +134,10 @@ InModuleScope 'ALZ' { It 'Computed, Processed values replace values correctly' { $configuration = [pscustomobject]@{ - Nested = [pscustomobject]@{ + Nested = [pscustomobject]@{ Source = "calculated" Description = "A Test Value" - Process = '($args[0] -eq "goodbye") ? "Hello" : "Goodbye"' + Process = '($args[0] -eq "goodbye") ? "Hello" : "Goodbye"' Value = "goodbye" Targets = @( [pscustomobject]@{ @@ -152,4 +152,4 @@ InModuleScope 'ALZ' { } } } -} \ No newline at end of file +} diff --git a/src/Tests/Unit/Private/Set-Config.Tests.ps1 b/src/Tests/Unit/Private/Set-Config.Tests.ps1 index ee75efdb..22e5044c 100644 --- a/src/Tests/Unit/Private/Set-Config.Tests.ps1 +++ b/src/Tests/Unit/Private/Set-Config.Tests.ps1 @@ -5,43 +5,43 @@ $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 + #if the module is already in memory, remove it + Remove-Module -Name $ModuleName -Force } Import-Module $PathToManifest -Force #------------------------------------------------------------------------- InModuleScope 'ALZ' { - Describe 'Set-Config Private Function Tests' -Tag Unit { - BeforeAll { - $WarningPreference = 'SilentlyContinue' - $ErrorActionPreference = 'SilentlyContinue' - } - Context 'Set-Config should request CLI input for configuration.' { - It 'Based on the configuration object' { + Describe 'Set-Config Private Function Tests' -Tag Unit { + BeforeAll { + $WarningPreference = 'SilentlyContinue' + $ErrorActionPreference = 'SilentlyContinue' + } + Context 'Set-Config should request CLI input for configuration.' { + It 'Based on the configuration object' { - $config = @' + $config = @' { - "parameters":{ - "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" - } - ], - "DefaultValue":"alz", - "Value":"" - } + "parameters": { + "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" + } + ], + "DefaultValue": "alz", + "Value": "" + } } - } + } '@ | ConvertFrom-Json - Set-Config -configurationParameters $config.Parameters - } + Set-Config -configurationParameters $config.Parameters + } - } - } -} \ No newline at end of file + } + } +}