From aa0f60ef6887f2d81713fe94c0c752e92a87b636 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 6 Sep 2024 18:11:10 +0100 Subject: [PATCH] feat: improvements to inputs and region validation (#182) # Pull Request ## Issues #124 ## Description This PR adds a few improvements: - The regions and availability zones are now retrieved dynamically. - The PowerShell module supports overriding the bootstrap terraform.tfvars file as one method of supplying values for hidden variables. - The PowerShell module supports supplying hidden bootstrap variables via the inputs config file, for example resource names can now be supplied there. - The override.tfvars file is now in json format, so we can support complex data types. e2e test run: https://github.com/Azure/accelerator-bootstrap-modules/actions/runs/10734976021 ## License By submitting this pull request, I confirm that my contribution is made under the terms of the projects associated license. --- .github/linters/.markdown-lint.yml | 2 +- CONTRIBUTING.md | 24 ++--- docs/wiki/Frequently-Asked-Questions.md | 52 ++++++++-- ...Guide]-Quick-Start-Phase-2-Azure-DevOps.md | 4 +- ...[User-Guide]-Quick-Start-Phase-2-GitHub.md | 4 +- .../inputs-azure-devops-bicep-complete.yaml | 2 +- .../inputs-azure-devops-terraform-basic.yaml | 2 +- ...azure-devops-terraform-complete-vnext.yaml | 2 +- ...nputs-azure-devops-terraform-complete.yaml | 2 +- ...-azure-devops-terraform-hubnetworking.yaml | 2 +- .../inputs-github-bicep-complete.yaml | 2 +- .../inputs-github-terraform-basic.yaml | 2 +- ...nputs-github-terraform-complete-vnext.yaml | 2 +- .../inputs-github-terraform-complete.yaml | 2 +- ...inputs-github-terraform-hubnetworking.yaml | 2 +- .../Convert-HCLVariablesToUserInputConfig.ps1 | 1 + ...onvert-InterfaceInputToUserInputConfig.ps1 | 1 + .../Config-Helpers/Get-AzureRegionData.ps1 | 72 ++++++++++++++ .../Request-ALZEnvironmentConfig.ps1 | 1 + .../Request-ConfigurationValue.ps1 | 62 ++++++++---- .../Config-Helpers/Write-TfvarsFile.ps1 | 97 ------------------- .../Config-Helpers/Write-TfvarsJsonFile.ps1 | 51 ++++++++++ .../Get-BootstrapAndStarterConfig.ps1 | 13 ++- .../Invoke-Terraform.ps1 | 65 ++++++++++--- .../New-Bootstrap.ps1 | 72 ++++++++++++-- src/ALZ/Public/New-ALZEnvironment.ps1 | 18 ++-- .../Request-ConfigurationValue.Tests.ps1 | 25 ++++- .../Unit/Public/New-ALZEnvironment.Tests.ps1 | 18 +++- 28 files changed, 414 insertions(+), 188 deletions(-) create mode 100644 src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 delete mode 100644 src/ALZ/Private/Config-Helpers/Write-TfvarsFile.ps1 create mode 100644 src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml index 49fb796a..76e17abc 100644 --- a/.github/linters/.markdown-lint.yml +++ b/.github/linters/.markdown-lint.yml @@ -22,7 +22,7 @@ MD004: false # ul-style - Unordered list style MD007: indent: 2 # ul-indent - Unordered list indentation MD013: - line_length: 500 # line-length - Line length + line_length: 600 # line-length - Line length MD026: punctuation: ".,;:!。,;:" # no-trailing-punctuation - Trailing punctuation in heading MD029: false # ol-prefix - Ordered list item prefix diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3b5893b..d33ad5e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,18 +48,18 @@ Example folder structure: ┗ 📂acc ┣ 📂bicep ┃ ┣ 📂config - ┃ ┃ ┣ 📜inputs-azuredevops.yaml # ./docs/wiki/examples/powershell-inputs/inputs-azure-devops-bicep.yaml - ┃ ┃ ┣ 📜inputs-github.yaml # ./docs/wiki/examples/powershell-inputs/inputs-github-bicep.yaml - ┃ ┃ ┗ 📜inputs-local.yaml # ./docs/wiki/examples/powershell-inputs/inputs-local-bicep.yaml + ┃ ┃ ┣ 📜inputs-azuredevops.yaml # ./docs/wiki/examples/powershell-inputs/inputs-azure-devops-bicep-complete.yaml + ┃ ┃ ┣ 📜inputs-github.yaml # ./docs/wiki/examples/powershell-inputs/inputs-github-bicep-complete.yaml + ┃ ┃ ┗ 📜inputs-local.yaml # ./docs/wiki/examples/powershell-inputs/inputs-local-bicep-complete.yaml ┃ ┗ 📂output ┃ ┣ 📂azuredevops ┃ ┣ 📂github ┃ ┗ 📂local ┗ 📂terraform ┣ 📂config - ┃ ┣ 📜inputs-azuredevops.yaml # ./docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform.yaml - ┃ ┣ 📜inputs-github.yaml # ./docs/wiki/examples/powershell-inputs/inputs-github-terraform.yaml - ┃ ┗ 📜inputs-local.yaml # ./docs/wiki/examples/powershell-inputs/inputs-local-terraform.yaml + ┃ ┣ 📜inputs-azuredevops.yaml # ./docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete.yaml + ┃ ┣ 📜inputs-github.yaml # ./docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete.yaml + ┃ ┗ 📜inputs-local.yaml # ./docs/wiki/examples/powershell-inputs/inputs-local-terraform-complete.yaml ┗ 📂output ┣ 📂azuredevops ┣ 📂github @@ -94,12 +94,12 @@ cd / $exampleFolder = "$targetFolder/code/ALZ-PowerShell-Module/docs/wiki/examples/powershell-inputs" -Copy-Item -Path "$exampleFolder/inputs-azure-devops-bicep.yaml" -Destination "$bicepConfigFolder/inputs-azuredevops.yaml" -Force -Copy-Item -Path "$exampleFolder/inputs-github-bicep.yaml" -Destination "$bicepConfigFolder/inputs-github.yaml" -Force -Copy-Item -Path "$exampleFolder/inputs-local-bicep.yaml" -Destination "$bicepConfigFolder/inputs-local.yaml" -Force -Copy-Item -Path "$exampleFolder/inputs-azure-devops-terraform.yaml" -Destination "$terraformConfigFolder/inputs-azuredevops.yaml" -Force -Copy-Item -Path "$exampleFolder/inputs-github-terraform.yaml" -Destination "$terraformConfigFolder/inputs-github.yaml" -Force -Copy-Item -Path "$exampleFolder/inputs-local-terraform.yaml" -Destination "$terraformConfigFolder/inputs-local.yaml" -Force +Copy-Item -Path "$exampleFolder/inputs-azure-devops-bicep-complete.yaml" -Destination "$bicepConfigFolder/inputs-azuredevops.yaml" -Force +Copy-Item -Path "$exampleFolder/inputs-github-bicep-complete.yaml" -Destination "$bicepConfigFolder/inputs-github.yaml" -Force +Copy-Item -Path "$exampleFolder/inputs-local-bicep-complete.yaml" -Destination "$bicepConfigFolder/inputs-local.yaml" -Force +Copy-Item -Path "$exampleFolder/inputs-azure-devops-terraform-complete.yaml" -Destination "$terraformConfigFolder/inputs-azuredevops.yaml" -Force +Copy-Item -Path "$exampleFolder/inputs-github-terraform-complete.yaml" -Destination "$terraformConfigFolder/inputs-github.yaml" -Force +Copy-Item -Path "$exampleFolder/inputs-local-terraform-complete.yaml" -Destination "$terraformConfigFolder/inputs-local.yaml" -Force ``` diff --git a/docs/wiki/Frequently-Asked-Questions.md b/docs/wiki/Frequently-Asked-Questions.md index f10992ac..0eedbb9b 100644 --- a/docs/wiki/Frequently-Asked-Questions.md +++ b/docs/wiki/Frequently-Asked-Questions.md @@ -9,17 +9,51 @@ This article answers frequently asked questions relating to the Azure landing zo ### How do I use my own naming convention for the resources that are deployed? -Follow these steps to customise the resource names: +You can add any hidden variables to your inputs file, including the `resource_names` map. This map is used to set the names of the resources that are deployed. You can find the default values in the `terraform.tfvars` file in the bootstrap module. -1. At step 2.2 of the quickstart, you will run the `Deploy-Accelerator` command to start the user input process. -1. Once the prompt appears for the first question open the relevant bootstrap `terraform.tfvars` file: - 1. Azure DevOps: `./bootstrap/v#.#.#/alz/azuredevops/terraform.tfvars` - 1. GitHub: `./bootstrap/v#.#.#/alz/github/terraform.tfvars` -1. Look for the variable called `resource_names`. -1. Update this map to have the names you desire and save it. -1. Continue with the user input as normal. +For example adding this to the end of your inputs file and updating to your standard: -You'll now get the names you specified instead of the default ones. +```yaml +# Extra Inputs +resource_names: + resource_group_state: "rg-{{service_name}}-{{environment_name}}-state-{{azure_location}}-{{postfix_number}}-test" + resource_group_identity: "rg-{{service_name}}-{{environment_name}}-identity-{{azure_location}}-{{postfix_number}}" + resource_group_agents: "rg-{{service_name}}-{{environment_name}}-agents-{{azure_location}}-{{postfix_number}}" + resource_group_network: "rg-{{service_name}}-{{environment_name}}-network-{{azure_location}}-{{postfix_number}}" + user_assigned_managed_identity_plan: "id-{{service_name}}-{{environment_name}}-{{azure_location}}-plan-{{postfix_number}}" + user_assigned_managed_identity_apply: "id-{{service_name}}-{{environment_name}}-{{azure_location}}-apply-{{postfix_number}}" + user_assigned_managed_identity_federated_credentials_plan: "id-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-plan" + user_assigned_managed_identity_federated_credentials_apply: "id-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-apply" + storage_account: "sto{{service_name_short}}{{environment_name_short}}{{azure_location_short}}{{postfix_number}}{{random_string}}" + storage_container: "{{environment_name}}-tfstate" + container_instance_01: "aci-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}" + container_instance_02: "aci-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number_plus_1}}" + container_instance_managed_identity: "id-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-aci" + agent_01: "agent-{{service_name}}-{{environment_name}}-{{postfix_number}}" + agent_02: "agent-{{service_name}}-{{environment_name}}-{{postfix_number_plus_1}}" + version_control_system_repository: "{{service_name}}-{{environment_name}}" + version_control_system_repository_templates: "{{service_name}}-{{environment_name}}-templates" + version_control_system_service_connection_plan: "sc-{{service_name}}-{{environment_name}}-plan" + version_control_system_service_connection_apply: "sc-{{service_name}}-{{environment_name}}-apply" + version_control_system_environment_plan: "{{service_name}}-{{environment_name}}-plan" + version_control_system_environment_apply: "{{service_name}}-{{environment_name}}-apply" + version_control_system_variable_group: "{{service_name}}-{{environment_name}}" + version_control_system_agent_pool: "{{service_name}}-{{environment_name}}" + version_control_system_group: "{{service_name}}-{{environment_name}}-approvers" + version_control_system_pipeline_name_ci: "01 Azure Landing Zones Continuous Integration" + version_control_system_pipeline_name_cd: "02 Azure Landing Zones Continuous Delivery" + virtual_network: "vnet-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}" + public_ip: "pip-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}" + nat_gateway: "nat-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}" + subnet_container_instances: "subnet-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-aci" + subnet_private_endpoints: "subnet-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-pe" + storage_account_private_endpoint: "pe-{{service_name}}-{{environment_name}}-{{azure_location}}-sto-{{postfix_number}}" + container_registry: "acr{{service_name}}{{environment_name}}{{azure_location_short}}{{postfix_number}}{{random_string}}" + container_registry_private_endpoint: "pe-{{service_name}}-{{environment_name}}-{{azure_location}}-acr-{{postfix_number}}" + container_image_name: "azure-devops-agent" +``` + +Alternatively, you can take a copy of the `terraform.tfvars` file from the bootstrap module, update it and supply it via the `-bootstrapTfVarsOverridePath` parameter as an absolute path. ## Questions about bootstrap clean up diff --git a/docs/wiki/[User-Guide]-Quick-Start-Phase-2-Azure-DevOps.md b/docs/wiki/[User-Guide]-Quick-Start-Phase-2-Azure-DevOps.md index b108c7bc..50484e94 100644 --- a/docs/wiki/[User-Guide]-Quick-Start-Phase-2-Azure-DevOps.md +++ b/docs/wiki/[User-Guide]-Quick-Start-Phase-2-Azure-DevOps.md @@ -59,7 +59,7 @@ Although you can just run `Deploy-Accelerator` and fill out the prompted inputs, | `use_self_hosted_agents` | `true` | This controls if you want to deploy self-hosted agents. This will default to `true`. | | `use_private_networking` | `true` | This controls whether private networking is deployed for your self-hosted agents and storage account. This only applies if you have `use_self_hosted_agents` set to `true`. This defaults to `true`. | | `allow_storage_access_from_my_ip` | `false` | This is not relecant to Bicep and we'll remove the need to specify it later, leave it set to `false`. | - | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is a comma-separated list like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty string `""` to disable approvals. | + | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is an array of strings like `["abc@xyz.com", "def@xyz.com", "ghi@xyz.com"]`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty array `[]` to disable approvals. Note if supplying via the user interface, use a comma separated string like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. | | `create_branch_policies` | `true` | This controls whether to create branch policies for the repository. This defaults to `true`. | 1. Now head over to your chosen starter module documentation to get the specific inputs for that module. Come back here when you are done. @@ -137,7 +137,7 @@ Although you can just run `Deploy-Accelerator` and fill out the prompted inputs, | `use_self_hosted_agents` | `true` | This controls if you want to deploy self-hosted agents. This will default to `true`. | | `use_private_networking` | `true` | This controls whether private networking is deployed for your self-hosted agents and storage account. This only applies if you have `use_self_hosted_agents` set to `true`. This defaults to `true`. | | `allow_storage_access_from_my_ip` | `false` | This controls whether to allow access to the storage account from your IP address. This is only needed for trouble shooting. This only applies if you have `use_private_networking` set to `true`. This defaults to `false`. | - | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is a comma-separated list like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty string `""` to disable approvals. | + | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is an array of strings like `["abc@xyz.com", "def@xyz.com", "ghi@xyz.com"]`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty array `[]` to disable approvals. Note if supplying via the user interface, use a comma separated string like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. | | `create_branch_policies` | `true` | This controls whether to create branch policies for the repository. This defaults to `true`. | 1. Now head over to your chosen starter module documentation to get the specific inputs for that module. Come back here when you are done. diff --git a/docs/wiki/[User-Guide]-Quick-Start-Phase-2-GitHub.md b/docs/wiki/[User-Guide]-Quick-Start-Phase-2-GitHub.md index ef6dfc73..9aca1330 100644 --- a/docs/wiki/[User-Guide]-Quick-Start-Phase-2-GitHub.md +++ b/docs/wiki/[User-Guide]-Quick-Start-Phase-2-GitHub.md @@ -53,7 +53,7 @@ Although you can just run `Deploy-Accelerator` and fill out the prompted inputs, | `use_self_hosted_agents` | `true` | This controls if you want to deploy self-hosted agents. This will default to `true`. | | `use_private_networking` | `true` | This controls whether private networking is deployed for your self-hosted agents and storage account. This only applies if you have `use_self_hosted_agents` set to `true`. This defaults to `true`. | | `allow_storage_access_from_my_ip` | `false` | This is not relevant to Bicep and we'll remove the need to specify it later, leave it set to `false`. | - | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is a comma-separated list like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty string `""` to disable approvals. | + | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is an array of strings like `["abc@xyz.com", "def@xyz.com", "ghi@xyz.com"]`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty array `[]` to disable approvals. Note if supplying via the user interface, use a comma separated string like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. | | `create_branch_policies` | `true` | This controls whether to create branch policies for the repository. This defaults to `true`. | 1. Now head over to your chosen starter module documentation to get the specific inputs for that module. Come back here when you are done. @@ -124,7 +124,7 @@ Although you can just run `Deploy-Accelerator` and fill out the prompted inputs, | `use_self_hosted_agents` | `true` | This controls if you want to deploy self-hosted agents. This will default to `true`. | | `use_private_networking` | `true` | This controls whether private networking is deployed for your self-hosted agents and storage account. This only applies if you have `use_self_hosted_agents` set to `true`. This defaults to `true`. | | `allow_storage_access_from_my_ip` | `false` | This controls whether to allow access to the storage account from your IP address. This is only needed for trouble shooting. This only applies if you have `use_private_networking` set to `true`. This defaults to `false`. | - | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is a comma-separated list like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty string `""` to disable approvals. | + | `apply_approvers` | `` | This is a list of service principal names (SPN) of people you wish to be in the group that approves apply of the Azure landing zone module. This is an array of strings like `["abc@xyz.com", "def@xyz.com", "ghi@xyz.com"]`. You may need to check what the SPN is prior to filling this out as it can vary based on identity provider. Use empty array `[]` to disable approvals. Note if supplying via the user interface, use a comma separated string like `abc@xyz.com,def@xyz.com,ghi@xyz.com`. | | `create_branch_policies` | `true` | This controls whether to create branch policies for the repository. This defaults to `true`. | 1. Now head over to your chosen starter module documentation to get the specific inputs for that module. Come back here when you are done. diff --git a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-bicep-complete.yaml b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-bicep-complete.yaml index 332709d3..f095efeb 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-bicep-complete.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-bicep-complete.yaml @@ -30,7 +30,7 @@ azure_devops_project_name: "" use_self_hosted_agents: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Complete Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-basic.yaml b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-basic.yaml index 89b883ed..4443be90 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-basic.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-basic.yaml @@ -30,7 +30,7 @@ azure_devops_project_name: "" use_self_hosted_agents: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Basic Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete-vnext.yaml b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete-vnext.yaml index c2b465f1..143e30cc 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete-vnext.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete-vnext.yaml @@ -30,7 +30,7 @@ azure_devops_project_name: "" use_self_hosted_agents: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Complete vNext Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete.yaml b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete.yaml index 8e827e98..08daa94c 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-complete.yaml @@ -30,7 +30,7 @@ azure_devops_project_name: "" use_self_hosted_agents: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Complete Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-hubnetworking.yaml b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-hubnetworking.yaml index 9f403fc4..23dbba90 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-hubnetworking.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-azure-devops-terraform-hubnetworking.yaml @@ -30,7 +30,7 @@ azure_devops_project_name: "" use_self_hosted_agents: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Hub Networking Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-github-bicep-complete.yaml b/docs/wiki/examples/powershell-inputs/inputs-github-bicep-complete.yaml index 1488082b..c862d7a0 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-github-bicep-complete.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-github-bicep-complete.yaml @@ -27,7 +27,7 @@ postfix_number: "1" use_self_hosted_runners: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-basic.yaml b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-basic.yaml index 0d739ee5..d5f7c8e0 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-basic.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-basic.yaml @@ -27,7 +27,7 @@ postfix_number: "1" use_self_hosted_runners: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Basic Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete-vnext.yaml b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete-vnext.yaml index 3bbb232d..d67ed3d5 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete-vnext.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete-vnext.yaml @@ -27,7 +27,7 @@ postfix_number: "1" use_self_hosted_runners: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Complete vNext Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete.yaml b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete.yaml index 46506973..8cdeb3c8 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-complete.yaml @@ -27,7 +27,7 @@ postfix_number: "1" use_self_hosted_runners: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Complete Starter Module Specific Variables diff --git a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-hubnetworking.yaml b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-hubnetworking.yaml index 960469a0..4eea7201 100644 --- a/docs/wiki/examples/powershell-inputs/inputs-github-terraform-hubnetworking.yaml +++ b/docs/wiki/examples/powershell-inputs/inputs-github-terraform-hubnetworking.yaml @@ -27,7 +27,7 @@ postfix_number: "1" use_self_hosted_runners: "true" use_private_networking: "true" allow_storage_access_from_my_ip: "false" -apply_approvers: "" +apply_approvers: [""] create_branch_policies: "true" # Hub Networking Starter Module Specific Variables diff --git a/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToUserInputConfig.ps1 b/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToUserInputConfig.ps1 index c5dc6d47..8a3c5269 100644 --- a/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToUserInputConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Convert-HCLVariablesToUserInputConfig.ps1 @@ -72,6 +72,7 @@ function Convert-HCLVariablesToUserInputConfig { $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Value" -NotePropertyValue "" $starterModuleConfigurationInstance | Add-Member -NotePropertyName "DataType" -NotePropertyValue $dataType $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Sensitive" -NotePropertyValue $sensitive + $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Source" -NotePropertyValue "UserInterface" if($variable.Value[0].PSObject.Properties.Name -contains "default") { $defaultValue = $variable.Value[0].default diff --git a/src/ALZ/Private/Config-Helpers/Convert-InterfaceInputToUserInputConfig.ps1 b/src/ALZ/Private/Config-Helpers/Convert-InterfaceInputToUserInputConfig.ps1 index abc5c068..b089c2a2 100644 --- a/src/ALZ/Private/Config-Helpers/Convert-InterfaceInputToUserInputConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Convert-InterfaceInputToUserInputConfig.ps1 @@ -42,6 +42,7 @@ function Convert-InterfaceInputToUserInputConfig { $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Type" -NotePropertyValue $inputType $starterModuleConfigurationInstance | Add-Member -NotePropertyName "DataType" -NotePropertyValue $dataType $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Sensitive" -NotePropertyValue $sensitive + $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Source" -NotePropertyValue "UserInterface" if($variable.Value.PSObject.Properties.Name -contains "Value") { $starterModuleConfigurationInstance | Add-Member -NotePropertyName "Value" -NotePropertyValue $variable.Value.Value diff --git a/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 b/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 new file mode 100644 index 00000000..af8a81bf --- /dev/null +++ b/src/ALZ/Private/Config-Helpers/Get-AzureRegionData.ps1 @@ -0,0 +1,72 @@ +function Get-AzureRegionData { + param( + [Parameter(Mandatory = $false)] + [string]$toolsPath = ".\region" + ) + + $terraformCode = @' +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 1.14" + } + } +} + +data "azapi_client_config" "current" {} + +data "azapi_resource_action" "locations" { + type = "Microsoft.Resources/subscriptions@2022-12-01" + action = "locations" + method = "GET" + resource_id = "/subscriptions/${data.azapi_client_config.current.subscription_id}" + response_export_values = ["value"] +} + +locals { + regions = { for region in jsondecode(data.azapi_resource_action.locations.output).value : region.name => { + display_name = region.displayName + zones = try([ for zone in region.availabilityZoneMappings : zone.logicalZone ], []) + } if region.metadata.regionType == "Physical" + } +} + +output "regions_and_zones" { + value = local.regions +} +'@ + + $regionFolder = Join-Path $toolsPath "azure-regions" + if(Test-Path $regionFolder) { + Remove-Item $regionFolder -Recurse -Force + } + + New-Item $regionFolder -ItemType "Directory" + + $regionCodeFileName = Join-Path $regionFolder "main.tf" + $terraformCode | Out-File $regionCodeFileName -Force + + $outputFilePath = Join-Path $regionFolder "output.json" + + Invoke-Terraform -moduleFolderPath $regionFolder -autoApprove -output "regions_and_zones" -outputFilePath $outputFilePath -silent + + $json = Get-Content $outputFilePath + $regionsAndZones = ConvertFrom-Json $json + + $zonesSupport = @() + $supportedRegions = @() + + foreach($region in $regionsAndZones.PSObject.Properties) { + $supportedRegions += $region.Name + $zonesSupport += @{ + region = $region.Name + zones = $region.Value.zones + } + } + + return @{ + zonesSupport = $zonesSupport + supportedRegions = $supportedRegions + } +} \ No newline at end of file diff --git a/src/ALZ/Private/Config-Helpers/Request-ALZEnvironmentConfig.ps1 b/src/ALZ/Private/Config-Helpers/Request-ALZEnvironmentConfig.ps1 index 936c92ba..38314a92 100644 --- a/src/ALZ/Private/Config-Helpers/Request-ALZEnvironmentConfig.ps1 +++ b/src/ALZ/Private/Config-Helpers/Request-ALZEnvironmentConfig.ps1 @@ -89,6 +89,7 @@ function Request-ALZEnvironmentConfig { $userInputOverride = $userInputOverrides.PsObject.Properties | Where-Object { $_.Name -eq $configurationValue.Name } if($null -ne $userInputOverride) { $configurationValue.Value.Value = $userInputOverride.Value + $configurationValue.Value.Source = "InputConfig" } else { if($configurationValue.Value.PSObject.Properties.Name -match "DefaultValue") { Write-Verbose "Input not supplied, so using default value of $($configurationValue.Value.DefaultValue) for $($configurationValue.Name)" diff --git a/src/ALZ/Private/Config-Helpers/Request-ConfigurationValue.ps1 b/src/ALZ/Private/Config-Helpers/Request-ConfigurationValue.ps1 index 12eedb02..175a96af 100644 --- a/src/ALZ/Private/Config-Helpers/Request-ConfigurationValue.ps1 +++ b/src/ALZ/Private/Config-Helpers/Request-ConfigurationValue.ps1 @@ -33,6 +33,9 @@ function Request-ConfigurationValue { Write-InformationColored "[allowed: $allowedValues] " -ForegroundColor Yellow -InformationAction Continue } + $dataType = $configValue.DataType + Write-Verbose "Data Type: $dataType" + $attempt = 0 $maxAttempts = 10 @@ -54,41 +57,58 @@ function Request-ConfigurationValue { $readValue = Read-Host } - $previousValue = $configValue.Value - if ($hasDefaultValue -and $readValue -eq "") { $configValue.Value = $configValue.defaultValue } else { $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 - $skipValidationForEmptyDefault = $treatEmptyDefaultAsValid -and $hasDefaultValue -and (($defaultValue -eq "" -and $configValue.Value -eq "") -or ($configValue.Value -eq "-")) - - # Reset the value to empty if we have a default and the user entered a dash (this is to handle cached situations and provide a method to clear a value) - if($skipValidationForEmptyDefault -and $configValue.Value -eq "-") { - $configValue.Value = "" + $valuesToCheck = @( $configValue.Value ) + if($dataType -eq "list(string)") { + $valuesToCheck = ($configValue.Value -split ",").Trim() | Where-Object {$_ -ne ''} + $configValue.Value = $valuesToCheck -join "," } - if($skipValidationForEmptyDefault) { - $isNotValid = $false - } else { - Write-Verbose "Checking '$($configValue.Value)' against '$($configValue.Valid)'" - $isNotValid = $hasValidator -and $configValue.Value -match $configValue.Valid -eq $false - } + $isValid = $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 + foreach($valueToCheck in $valuesToCheck) { + $isValid = $true + + $hasNotSpecifiedValue = ($null -eq $valueToCheck -or "" -eq $valueToCheck) -and ($valueToCheck -ne $configValue.DefaultValue) + + if($hasNotSpecifiedValue) { + Write-InformationColored "A value must be specified for this input. It cannot be left empty." -ForegroundColor Red -InformationAction Continue + $isValid = $false + break + } + + $skipValidationForEmptyDefault = $treatEmptyDefaultAsValid -and $hasDefaultValue -and (($defaultValue -eq "" -and $valueToCheck -eq "")) + if(!$skipValidationForEmptyDefault) { + if($hasAllowedValues) { + Write-Verbose "Checking '$($valueToCheck)' against list '$($allowedValues)'" + $isValid = $allowedValues.Contains($valueToCheck) + if(!$isValid) { + Write-InformationColored "The input value '$valueToCheck' is not valid. It must be in the allowed list: '$($allowedValues)'" -ForegroundColor Red -InformationAction Continue + break + } + } + + if($hasValidator) { + Write-Verbose "Checking '$($valueToCheck)' against validator '$($configValue.Valid)'" + $isValid = $valueToCheck -match $configValue.Valid + if(!$isValid) { + Write-InformationColored "The input value '$valueToCheck' is not valid. It must match to specified regular expression: '$($configValue.Valid)'" -ForegroundColor Red -InformationAction Continue + break + } + } + } } - $shouldRetry = $validationError -and $withRetries + $shouldRetry = !$isValid -and $withRetries $attempt += 1 } - while (($hasNotSpecifiedValue -or $isDisallowedValue -or $isNotValid) -and $shouldRetry -and $attempt -lt $maxAttempts) + while ($shouldRetry -and $attempt -lt $maxAttempts) if($attempt -eq $maxAttempts) { Write-InformationColored "Max attempts reached for getting input value. Exiting..." -ForegroundColor Red -InformationAction Continue diff --git a/src/ALZ/Private/Config-Helpers/Write-TfvarsFile.ps1 b/src/ALZ/Private/Config-Helpers/Write-TfvarsFile.ps1 deleted file mode 100644 index a9e86530..00000000 --- a/src/ALZ/Private/Config-Helpers/Write-TfvarsFile.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -function Write-TfvarsFile { - [CmdletBinding(SupportsShouldProcess = $true)] - param ( - [Parameter(Mandatory = $false)] - [string] $tfvarsFilePath, - - [Parameter(Mandatory = $false)] - [PSObject] $configuration - ) - - if ($PSCmdlet.ShouldProcess("Download Terraform Tools", "modify")) { - - if(Test-Path $tfvarsFilePath) { - Remove-Item -Path $tfvarsFilePath - } - - foreach($configurationProperty in $configuration.PSObject.Properties) { - $configurationValueRaw = $configurationProperty.Value.Value - - if ($configurationProperty.Value.Validator -eq "configuration_file_path") { - $configurationValueRaw = [System.IO.Path]::GetFileName($configurationValueRaw) - } - - $configurationValue = "`"$($configurationValueRaw)`"" - - if ($configurationProperty.Value.DataType -eq "list(string)") { - if (-not $configurationValueRaw -or $configurationValueRaw.Count -eq 0) { - if ($configurationProperty.Value.DefaultValue) { - $configurationValue = $configurationProperty.Value.DefaultValue - } else { - $configurationValue = "[]" - } - } else { - $split = $configurationValueRaw -split "," - $join = $split -join "`",`"" - $configurationValue = "[`"$join`"]" - } - } - - if ($configurationProperty.Value.DataType -eq "map(string)") { - if (-not $configurationValueRaw -or $configurationValueRaw.Count -eq 0) { - if ($configurationProperty.Value.DefaultValue) { - $configurationValue = $configurationProperty.Value.DefaultValue - } else { - $configurationValue = "{}" - } - } else { - $configurationValue = "{" - $entries = @() - - foreach ($key in $configurationValueRaw.Keys) { - $value = $configurationValueRaw[$key] - $entries += "`"$key`": `"$value`"" - } - - $configurationValue = $entries -join ", " - $configurationValue = "{ $configurationValue }" - } - } - - if ($configurationProperty.Value.DataType -like "list(object*") { - if (-not $configurationValueRaw -or $configurationValueRaw.Count -eq 0) { - if ($configurationProperty.Value.DefaultValue) { - $configurationValue = $configurationProperty.Value.DefaultValue - } else { - $configurationValue = "[]" - } - } else { - $configurationValue = "[" - foreach ($entry in $configurationValueRaw) { - $configurationValue += "{ " - foreach ($key in $entry.Keys) { - $value = $entry[$key] - $configurationValue += "`"$key`": `"$value`", " - } - $configurationValue = $configurationValue.TrimEnd(", ") - $configurationValue += "}, " - } - $configurationValue = $configurationValue.TrimEnd(", ") - $configurationValue += "]" - } - } - - if ($configurationProperty.Value.DataType -eq "number" -or $configurationProperty.Value.DataType -eq "bool") { - $configurationValue = $configurationValueRaw - } else { - $configurationValue = $configurationValue.Replace("\", "\\") - } - - Add-Content -Path $tfvarsFilePath -Value "$($configurationProperty.Name) = $($configurationValue)" - } - - $tfvarsFolderPath = Split-Path -Path $tfvarsFilePath -Parent - - terraform -chdir="$tfvarsFolderPath" fmt | Out-String | Write-Verbose - } -} diff --git a/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 b/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 new file mode 100644 index 00000000..ff8655d9 --- /dev/null +++ b/src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1 @@ -0,0 +1,51 @@ +function Write-TfvarsJsonFile { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $false)] + [string] $tfvarsFilePath, + + [Parameter(Mandatory = $false)] + [PSObject] $configuration + ) + + if ($PSCmdlet.ShouldProcess("Download Terraform Tools", "modify")) { + + if(Test-Path $tfvarsFilePath) { + Remove-Item -Path $tfvarsFilePath + } + + $jsonObject = @{} + + foreach($configurationProperty in $configuration.PSObject.Properties) { + $configurationValue = $configurationProperty.Value.Value + + if($configurationProperty.Value.Validator -eq "configuration_file_path") { + $configurationValue = [System.IO.Path]::GetFileName($configurationValue) + } + + if($configurationProperty.Value.Source -eq "UserInterface") { + if($configurationProperty.Value.DataType -eq "list(string)") { + if($configurationValue -eq "") { + $configurationValue = @() + } else { + $configurationValue = @($configurationValue -split ",") + } + } + + if($configurationProperty.Value.DataType -eq "number") { + $configurationValue = [int]($configurationValue) + } + + if($configurationProperty.Value.DataType -eq "bool") { + $configurationValue = [bool]($configurationValue) + } + } + + $jsonObject["$($configurationProperty.Name)"] = $configurationValue + } + + $jsonString = ConvertTo-Json $jsonObject + + $jsonString | Out-File $tfvarsFilePath + } +} \ No newline at end of file diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-BootstrapAndStarterConfig.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-BootstrapAndStarterConfig.ps1 index e2046cc2..a0340db8 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-BootstrapAndStarterConfig.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-BootstrapAndStarterConfig.ps1 @@ -11,7 +11,9 @@ function Get-BootstrapAndStarterConfig { [Parameter(Mandatory = $false)] [string]$bootstrapConfigPath, [Parameter(Mandatory = $false)] - [PSCustomObject]$userInputOverrides + [PSCustomObject]$userInputOverrides, + [Parameter(Mandatory = $false)] + [string]$toolsPath ) if ($PSCmdlet.ShouldProcess("Get Configuration for Bootstrap and Starter", "modify")) { @@ -31,7 +33,14 @@ function Get-BootstrapAndStarterConfig { Write-Verbose "Bootstrap config path $bootstrapConfigFullPath" $bootstrapConfig = Get-ALZConfig -configFilePath $bootstrapConfigFullPath $validationConfig = $bootstrapConfig.validators - $zonesSupport = $bootstrapConfig.zonesSupport + + Write-Verbose "Getting Supported Regions and Availability Zones with Terraform" + $regionsAndZones = Get-AzureRegionData -toolsPath $toolsPath + Write-Verbose "Supported Regions: $($regionsAndZones.supportedRegions)" + + $zonesSupport = $regionsAndZones.zonesSupport + $azureLocationValidator = $validationConfig.PSObject.Properties["azure_location"].Value + $azureLocationValidator.AllowedValues.Values = $regionsAndZones.supportedRegions # Get the available bootstrap modules $bootstrapModules = $bootstrapConfig.bootstrap_modules diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 index 8b6bc3d2..ba7b31ce 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Terraform.ps1 @@ -5,13 +5,22 @@ function Invoke-Terraform { [string] $moduleFolderPath, [Parameter(Mandatory = $false)] - [string] $tfvarsFileName, + [string] $tfvarsFileName = "", [Parameter(Mandatory = $false)] [switch] $autoApprove, [Parameter(Mandatory = $false)] - [switch] $destroy + [switch] $destroy, + + [Parameter(Mandatory = $false)] + [string] $output = "", + + [Parameter(Mandatory = $false)] + [string] $outputFilePath = "", + + [Parameter(Mandatory = $false)] + [switch] $silent ) if ($PSCmdlet.ShouldProcess("Apply Terraform", "modify")) { @@ -21,7 +30,9 @@ function Invoke-Terraform { $action = "destroy" } - Write-InformationColored "Terraform init has completed, now running the $action..." -ForegroundColor Green -NewLineBefore -InformationAction Continue + if(!$silent) { + Write-InformationColored "Terraform init has completed, now running the $action..." -ForegroundColor Green -NewLineBefore -InformationAction Continue + } $planFileName = "tfplan" @@ -35,20 +46,28 @@ function Invoke-Terraform { $arguments += "plan" $arguments += "-out=$planFileName" $arguments += "-input=false" - $arguments += "-var-file=$tfvarsFileName" + if($tfvarsFileName -ne "") { + $arguments += "-var-file=$tfvarsFileName" + } if ($destroy) { $arguments += "-destroy" } - Write-InformationColored "Running Plan Command for $action : $command $arguments" -ForegroundColor Green -NewLineBefore -InformationAction Continue - & $command $arguments + if(!$silent) { + Write-InformationColored "Running Plan Command for $action : $command $arguments" -ForegroundColor Green -NewLineBefore -InformationAction Continue + & $command $arguments + } else { + & $command $arguments | Write-Verbose + } $exitCode = $LASTEXITCODE # Stop and display timer $StopWatch.Stop() - Write-InformationColored "Time taken to complete Terraform plan:" -ForegroundColor Green -NewLineBefore -InformationAction Continue + if(!$silent) { + Write-InformationColored "Time taken to complete Terraform plan:" -ForegroundColor Green -NewLineBefore -InformationAction Continue + } $StopWatch.Elapsed | Format-Table if($exitCode -ne 0) { @@ -80,8 +99,12 @@ function Invoke-Terraform { $arguments += "-input=false" $arguments += "$planFileName" - Write-InformationColored "Running Apply Command for $action : $command $arguments" -ForegroundColor Green -NewLineBefore -InformationAction Continue - & $command $arguments + if(!$silent) { + Write-InformationColored "Running Apply Command for $action : $command $arguments" -ForegroundColor Green -NewLineBefore -InformationAction Continue + & $command $arguments + } else { + & $command $arguments | Write-Verbose + } $exitCode = $LASTEXITCODE @@ -97,7 +120,9 @@ function Invoke-Terraform { $arguments += "apply" $arguments += "-auto-approve" $arguments += "-input=false" - $arguments += "-var-file=$tfvarsFileName" + if($tfvarsFileName -ne "") { + $arguments += "-var-file=$tfvarsFileName" + } if ($destroy) { $arguments += "-destroy" } @@ -109,12 +134,30 @@ function Invoke-Terraform { # Stop and display timer $StopWatch.Stop() - Write-InformationColored "Time taken to complete Terraform apply:" -ForegroundColor Green -NewLineBefore -InformationAction Continue + if(!$silent) { + Write-InformationColored "Time taken to complete Terraform apply:" -ForegroundColor Green -NewLineBefore -InformationAction Continue + } $StopWatch.Elapsed | Format-Table 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 "") { + $outputFilePath = Join-Path $moduleFolderPath "output.json" + } + $command = "terraform" + $arguments = @() + $arguments += "-chdir=$moduleFolderPath" + $arguments += "output" + $arguments += "-json" + $arguments += "$output" + + Write-Verbose "Outputting $output to $outputFilePath" + Write-Verbose "Running Output Command: $command $arguments" + & $command $arguments > $outputFilePath + } } } } \ 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 0b29dddd..2eee1e17 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 @@ -51,7 +51,11 @@ function New-Bootstrap { [Parameter(Mandatory = $false, HelpMessage = "An extra level of logging that is turned off by default for easier debugging.")] [switch] - $writeVerboseLogs + $writeVerboseLogs, + + [Parameter(Mandatory = $false, HelpMessage = "The path to the bootstrap terraform.tfvars file that you would like to replace the default one with. (e.g. c:\accelerator\terraform.tfvars)")] + [string] + $bootstrapTfVarsOverridePath ) if ($PSCmdlet.ShouldProcess("ALZ-Terraform module configuration", "modify")) { @@ -73,6 +77,26 @@ function New-Bootstrap { Write-Verbose "Bootstrap Module Path: $bootstrapModulePath" + # Override default tfvars file + if($bootstrapTfVarsOverridePath -ne "" -and (Test-Path $bootstrapTfVarsOverridePath)) { + $fileExtension = [System.IO.Path]::GetExtension($bootstrapTfVarsOverridePath) + $terraformTfVars = Get-Content $bootstrapTfVarsOverridePath + $targetTfVarsFileName = "terraform.tfvars" + $targetTfVarsPath = Join-Path $bootstrapModulePath $targetTfVarsFileName + + if(Test-Path $targetTfVarsPath) { + Write-Verbose "Removing $targetTfVarsPath" + Remove-Item $targetTfVarsPath -Force | Write-Verbose + } + + if($fileExtension.ToLower() -eq "json") { + $targetTfVarsPath = "$targetTfVarsPath.json" + } + + Write-Verbose "Creating $targetTfVarsPath" + $terraformTfVars | Out-File $targetTfVarsPath -Force + } + # Run upgrade Invoke-FullUpgrade ` -bootstrapModuleFolder $bootstrapDetails.Value.location ` @@ -100,7 +124,7 @@ function New-Bootstrap { Write-Verbose "Selected Starter: $starter" - $starterModulePath = Resolve-Path (Join-Path -Path $starterPath -ChildPath $starterConfig.starter_modules.$starter.location) + $starterModulePath = (Resolve-Path (Join-Path -Path $starterPath -ChildPath $starterConfig.starter_modules.$starter.location)).Path Write-Verbose "Starter Module Path: $starterModulePath" } @@ -190,7 +214,19 @@ function New-Bootstrap { $computedInputs["starter_module_name"] = $starter $computedInputs["module_folder_path"] = $starterModulePath $computedInputs["availability_zones_bootstrap"] = @(Get-AvailabilityZonesSupport -region $interfaceConfiguration.bootstrap_location.Value -zonesSupport $zonesSupport) - $computedInputs["availability_zones_starter"] = @(Get-AvailabilityZonesSupport -region $interfaceConfiguration.starter_location.Value -zonesSupport $zonesSupport) + + $starterLocations = $interfaceConfiguration.starter_location.Value + if($starterLocations.Contains(",")) { + $computedInputs["availability_zones_starter"] = @() + foreach($region in $starterLocations -split ",") { + $computedInputs["availability_zones_starter"] += @{ + region = $region + zones = @(Get-AvailabilityZonesSupport -region $region -zonesSupport $zonesSupport) + } + } + } else { + $computedInputs["availability_zones_starter"] = @(Get-AvailabilityZonesSupport -region $starterLocations -zonesSupport $zonesSupport) + } foreach($inputConfigItem in $inputConfig.inputs.PSObject.Properties) { if($inputConfigItem.Value.source -eq "powershell") { @@ -255,13 +291,31 @@ function New-Bootstrap { -computedInputs $starterComputed # Creating the tfvars files for the bootstrap and starter module - $bootstrapTfvarsPath = Join-Path -Path $bootstrapModulePath -ChildPath "override.tfvars" - $starterTfvarsPath = Join-Path -Path $starterModulePath -ChildPath "terraform.tfvars" + $tfVarsFileName = "override.tfvars.json" + $bootstrapTfvarsPath = Join-Path -Path $bootstrapModulePath -ChildPath $tfVarsFileName + $starterTfvarsPath = Join-Path -Path $starterModulePath -ChildPath "terraform.tfvars.json" $starterBicepVarsPath = Join-Path -Path $starterModulePath -ChildPath "parameters.json" - Write-TfvarsFile -tfvarsFilePath $bootstrapTfvarsPath -configuration $bootstrapConfiguration + + # Add any extra inputs to the bootstrap tfvars on the assumption they are hidden inputs + foreach($input in $userInputOverrides.PSObject.Properties) { + $inputName = $input.Name + $inputValue = $input.Value + + if($bootstrapConfiguration.PSObject.Properties.Name -notcontains $inputName -and $interfaceConfiguration.PSObject.Properties.Name -notcontains $inputName -and $starterConfiguration.PSObject.Properties.Name -notcontains $inputName -and @("bootstrap", "starter", "iac") -notcontains $inputName) { + Write-Verbose "Setting hidden bootstrap variable '$inputName' to '$inputValue'" + $configItem = [PSCustomObject]@{} + $configItem | Add-Member -NotePropertyName "Value" -NotePropertyValue $inputValue + $configItem | Add-Member -NotePropertyName "DataType" -NotePropertyValue "Any" + + $bootstrapConfiguration | Add-Member -NotePropertyName $inputName -NotePropertyValue $configItem + } + } + + # Write the tfvars file for the bootstrap and starter module + Write-TfvarsJsonFile -tfvarsFilePath $bootstrapTfvarsPath -configuration $bootstrapConfiguration if($iac -eq "terraform") { - Write-TfvarsFile -tfvarsFilePath $starterTfvarsPath -configuration $starterConfiguration + Write-TfvarsJsonFile -tfvarsFilePath $starterTfvarsPath -configuration $starterConfiguration } if($iac -eq "bicep") { @@ -294,10 +348,10 @@ function New-Bootstrap { Write-InformationColored "Thank you for providing those inputs, we are now initializing and applying Terraform to bootstrap your environment..." -ForegroundColor Green -NewLineBefore -InformationAction Continue if($autoApprove) { - Invoke-Terraform -moduleFolderPath $bootstrapModulePath -tfvarsFileName "override.tfvars" -autoApprove -destroy:$destroy.IsPresent + Invoke-Terraform -moduleFolderPath $bootstrapModulePath -tfvarsFileName $tfVarsFileName -autoApprove -destroy:$destroy.IsPresent } else { Write-InformationColored "Once the plan is complete you will be prompted to confirm the apply." -ForegroundColor Green -NewLineBefore -InformationAction Continue - Invoke-Terraform -moduleFolderPath $bootstrapModulePath -tfvarsFileName "override.tfvars" -destroy:$destroy.IsPresent + Invoke-Terraform -moduleFolderPath $bootstrapModulePath -tfvarsFileName $tfVarsFileName -destroy:$destroy.IsPresent } 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 diff --git a/src/ALZ/Public/New-ALZEnvironment.ps1 b/src/ALZ/Public/New-ALZEnvironment.ps1 index 1c600022..06730e09 100644 --- a/src/ALZ/Public/New-ALZEnvironment.ps1 +++ b/src/ALZ/Public/New-ALZEnvironment.ps1 @@ -112,7 +112,11 @@ function New-ALZEnvironment { [Parameter(Mandatory = $false, HelpMessage = "An extra level of logging that is turned off by default for easier debugging.")] [switch] - $writeVerboseLogs + $writeVerboseLogs, + + [Parameter(Mandatory = $false, HelpMessage = "The path to the bootstrap terraform.tfvars file that you would like to replace the default one with. (e.g. c:\accelerator\terraform.tfvars). This file can also be in json format.")] + [string] + $bootstrapTfVarsOverridePath ) $ProgressPreference = "SilentlyContinue" @@ -146,15 +150,15 @@ function New-ALZEnvironment { } # Check and install Terraform CLI if needed - if (!$isLegacyBicep) { - if ($skipInternetChecks) { + $toolsPath = Join-Path -Path $targetDirectory -ChildPath ".tools" + if(!$isLegacyBicep) { + if($skipInternetChecks) { Write-InformationColored "Skipping Terraform tool check as you used the skipInternetCheck parameter. Please ensure you have the most recent version of Terraform installed" -ForegroundColor Yellow -InformationAction Continue } else { Write-InformationColored "Checking you have the latest version of Terraform installed..." -ForegroundColor Green -NewLineBefore -InformationAction Continue if ($iac -eq "bicep") { Write-InformationColored "Although you have selected Bicep, the Accelerator leverages the Terraform tool to bootstrap your Version Control System and Azure. This is will not impact your choice of Bicep post this initial bootstrap. Please refer to our documentation for further details..." -ForegroundColor Yellow -InformationAction Continue } - $toolsPath = Join-Path -Path $targetDirectory -ChildPath ".tools" Get-TerraformTool -version "latest" -toolsPath $toolsPath } } @@ -209,7 +213,8 @@ function New-ALZEnvironment { -bootstrap $bootstrap ` -bootstrapPath $bootstrapPath ` -bootstrapConfigPath $bootstrapConfigPath ` - -userInputOverrides $userInputOverrides + -userInputOverrides $userInputOverrides ` + -toolsPath $toolsPath $bootstrapDetails = $bootstrapAndStarterConfig.bootstrapDetails $hasStarterModule = $bootstrapAndStarterConfig.hasStarterModule @@ -302,7 +307,8 @@ function New-ALZEnvironment { -starter $starter ` -zonesSupport $zonesSupport ` -computedInputs $computedInputs ` - -writeVerboseLogs:$writeVerboseLogs.IsPresent + -writeVerboseLogs:$writeVerboseLogs.IsPresent ` + -bootstrapTfVarsOverridePath $bootstrapTfVarsOverridePath } } diff --git a/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 b/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 index 8e342017..31138955 100644 --- a/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 +++ b/src/Tests/Unit/Private/Request-ConfigurationValue.Tests.ps1 @@ -79,7 +79,7 @@ InModuleScope 'ALZ' { $configValue.Value | Should -BeExactly "" } - It 'Prompt the user with warning text when an invalid value is specified and leave the existing value unchanged.' { + It 'Prompt the user with warning text when an invalid value is specified.' { $configValue = @{ Description = "The prefix that will be added to all resources created by this deployment." Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") @@ -91,10 +91,9 @@ InModuleScope 'ALZ' { 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.' { + It 'Prompt the user with warning text when a value is specified which isnt in the allowed list.' { Mock -CommandName Read-Host -MockWith { "notinthelist" } @@ -110,7 +109,25 @@ InModuleScope 'ALZ' { 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 for a list(string).' { + Mock -CommandName Read-Host -MockWith { + "alz,notinthelist" + } + + $configValue = @{ + Description = "The prefix that will be added to all resources created by this deployment." + Names = @("parTopLevelManagementGroupPrefix", "parCompanyPrefix") + DataType = "list(string)" + Value = "" + AllowedValues = @{ + Values = @("alz", "slz") + } + } + Request-ConfigurationValue -configName "prefix" -configValue $configValue -withRetries $false + + Should -Invoke -CommandName Write-InformationColored -ParameterFilter { $ForegroundColor -eq "Red" } -Scope It } It 'Prompt user with a calculated list of AllowedValues' { diff --git a/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 b/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 index 1fc2ce16..ccb049fe 100644 --- a/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 +++ b/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 @@ -115,8 +115,6 @@ InModuleScope 'ALZ' { ) } - Mock -CommandName Write-TfvarsFile -MockWith { } - Mock -CommandName Write-ConfigurationCache -MockWith { } Mock -CommandName Invoke-Terraform -MockWith { } @@ -139,12 +137,28 @@ InModuleScope 'ALZ' { Mock -CommandName Get-BootstrapAndStarterConfig -MockWith { @{ "hasStarterModule" = $true + "validationConfig" = @{ + "azure_location" = @{ + "AllowedValues" = @{ + "Values" = @( "uksouth", "ukwest" ) + } + } + } } } Mock -CommandName New-Bootstrap -MockWith {} Mock -CommandName New-ALZEnvironmentBicep -MockWith {} + + Mock -CommandName Get-AzureRegionData -MockWith { + @{ + "uksouth" = @{ + "display_name" = "UK South" + "zone" = @( "1", "2", "3" ) + } + } + } } It 'should call the correct functions for bicep legacy module configuration' {