Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1

function New-Bootstrap {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $false)]
        [string] $iac,

        [Parameter(Mandatory = $false)]
        [PSCustomObject] $bootstrapDetails,

        [Parameter(Mandatory = $false)]
        [PSCustomObject] $validationConfig,

        [Parameter(Mandatory = $false)]
        [PSCustomObject] $inputConfig,

        [Parameter(Mandatory = $false)]
        [PSCustomObject] $userInputOverrides = $null,

        [Parameter(Mandatory = $false)]
        [string] $bootstrapTargetPath,

        [Parameter(Mandatory = $false)]
        [switch] $hasStarter,

        [Parameter(Mandatory = $false)]
        [string] $starterTargetPath,

        [Parameter(Mandatory = $false)]
        [PSCustomObject] $starterConfig = $null,

        [Parameter(Mandatory = $false)]
        [string] $bootstrapRelease,

        [Parameter(Mandatory = $false)]
        [string] $starterRelease,

        [Parameter(Mandatory = $false)]
        [switch] $autoApprove,

        [Parameter(Mandatory = $false)]
        [switch] $destroy,

        [Parameter(Mandatory = $false)]
        [string] $starter = "",

        [Parameter(Mandatory = $false)]
        [PSCustomObject] $zonesSupport = $null,

        [Parameter(Mandatory = $false)]
        [hashtable] $computedInputs,

        [Parameter(Mandatory = $false, HelpMessage = "An extra level of logging that is turned off by default for easier debugging.")]
        [switch]
        $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")) {

        $bootstrapPath = Join-Path $bootstrapTargetPath $bootstrapRelease
        $starterPath = Join-Path $starterTargetPath $starterRelease

        # Setup tools
        $hclParserToolPath = Get-HCLParserTool -alzEnvironmentDestination $bootstrapPath -toolVersion "v0.6.0"

        # Setup Cache File Names
        $interfaceCacheFileName = "interface-cache.json"
        $bootstrapCacheFileName = "bootstrap-cache.json"
        $starterCacheFileName = "starter-cache.json"
        $interfaceCachePath = Join-Path -Path $bootstrapPath -ChildPath $interfaceCacheFileName
        $bootstrapCachePath = Join-Path -Path $bootstrapPath -ChildPath $bootstrapCacheFileName
        $starterCachePath = Join-Path -Path $starterPath -ChildPath $starterCacheFileName
        $bootstrapModulePath = Join-Path -Path $bootstrapPath -ChildPath $bootstrapDetails.Value.location

        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 `
            -bootstrapRelease $bootstrapRelease `
            -bootstrapPath $bootstrapTargetPath `
            -starterRelease $starterRelease `
            -starterPath $starterTargetPath `
            -interfaceCacheFileName $interfaceCacheFileName `
            -bootstrapCacheFileName $bootstrapCacheFileName `
            -starterCacheFileName $starterCacheFileName `
            -autoApprove:$autoApprove.IsPresent

        # Get cached inputs
        $interfaceCachedConfig = Get-ALZConfig -configFilePath $interfaceCachePath
        $bootstrapCachedConfig = Get-ALZConfig -configFilePath $bootstrapCachePath
        $starterCachedConfig = Get-ALZConfig -configFilePath $starterCachePath

        # Get starter module
        $starterModulePath = ""

        if($hasStarter) {
            if($starter -eq "") {
                $starter = Request-SpecialInput -type "starter" -starterConfig $starterConfig -userInputOverrides $userInputOverrides
            }

            Write-Verbose "Selected Starter: $starter"

            $starterModulePath = (Resolve-Path (Join-Path -Path $starterPath -ChildPath $starterConfig.starter_modules.$starter.location)).Path

            Write-Verbose "Starter Module Path: $starterModulePath"
        }

        # Getting the configuration for the interface user input
        Write-Verbose "Getting the interface configuration for user input..."
        $inputConfigMapped = Convert-InterfaceInputToUserInputConfig -inputConfig $inputConfig -validators $validationConfig

        # Getting configuration for the bootstrap module user input
        $bootstrapParameters = [PSCustomObject]@{}

        Write-Verbose "Getting the bootstrap configuration for user input..."
        foreach($inputVariablesFile in $bootstrapDetails.Value.input_variable_files) {
            $inputVariablesFilePath = Join-Path -Path $bootstrapModulePath -ChildPath $inputVariablesFile
            $bootstrapParameters = Convert-HCLVariablesToUserInputConfig -targetVariableFile $inputVariablesFilePath -hclParserToolPath $hclParserToolPath -validators $validationConfig -appendToObject $bootstrapParameters
        }
        Write-Verbose "Getting the bootstrap configuration computed interface input..."
        foreach($interfaceVariablesFile in $bootstrapDetails.Value.interface_variable_files) {
            $inputVariablesFilePath = Join-Path -Path $bootstrapModulePath -ChildPath $interfaceVariablesFile
            $bootstrapParameters = Convert-HCLVariablesToUserInputConfig -targetVariableFile $inputVariablesFilePath -hclParserToolPath $hclParserToolPath -validators $validationConfig -appendToObject $bootstrapParameters -allComputedInputs
        }

        # Getting the configuration for the starter module user input
        $starterParameters  = [PSCustomObject]@{}

        if($hasStarter) {
            if($iac -eq "terraform") {
                $targetVariableFilePath = Join-Path -Path $starterModulePath -ChildPath "variables.tf"
                $starterParameters = Convert-HCLVariablesToUserInputConfig -targetVariableFile $targetVariableFilePath -hclParserToolPath $hclParserToolPath -validators $validationConfig
            }

            if($iac -eq "bicep") {
                $starterParameters = Convert-InterfaceInputToUserInputConfig -inputConfig $starterConfig.starter_modules.$starter -validators $validationConfig
            }
        }

        # Filter interface inputs if not in bootstrap or starter
        foreach($inputConfigItem in $inputConfig.inputs.PSObject.Properties) {
            if($inputConfigItem.Value.source -ne "input" -or $inputConfigItem.Value.required -eq $true) {
                continue
            }
            $inputVariable = $inputConfigMapped.PSObject.Properties | Where-Object { $_.Name -eq $inputConfigItem.Name }
            $displayMapFilter = $inputConfigItem.Value.PSObject.Properties | Where-Object { $_.Name -eq "display_map_filter" }
            $hasDisplayMapFilter = $null -ne $displayMapFilter
            Write-Verbose "$($inputConfigItem.Name) has display map filter $hasDisplayMapFilter"

            $inBootstrapOrStarter = $false
            if("bootstrap" -in $inputConfigItem.Value.maps_to) {
                $checkFilter = !$hasDisplayMapFilter -or ($hasDisplayMapFilter -and "bootstrap" -in $displayMapFilter.Value)

                if($checkFilter) {
                    Write-Verbose "Checking bootstrap for $($inputConfigItem.Name)"
                    $boostrapParameter = $bootstrapParameters.PSObject.Properties | Where-Object { $_.Name -eq $inputVariable.Name }
                    if($null -ne $boostrapParameter) {
                        $inBootstrapOrStarter = $true
                    }
                }
            }
            if("starter" -in $inputConfigItem.Value.maps_to) {
                $checkFilter = !$hasDisplayMapFilter -or ($hasDisplayMapFilter -and "starter" -in $displayMapFilter.Value)

                if($checkFilter) {
                    Write-Verbose "Checking starter for $($inputConfigItem.Name)"
                    $starterParameter = $starterParameters.PSObject.Properties | Where-Object { $_.Name -eq $inputVariable.Name }
                    if($null -ne $starterParameter) {
                        $inBootstrapOrStarter = $true
                    }
                }
            }

            if(!$inBootstrapOrStarter) {
                $inputVariable.Value.Type = "SkippedInput"
            }
        }

        # Prompt user for interface inputs
        Write-InformationColored "The following shared inputs are for the '$($bootstrapDetails.Name)' bootstrap and '$starter' starter module that you selected:" -ForegroundColor Green -NewLineBefore -InformationAction Continue
        $interfaceConfiguration = Request-ALZEnvironmentConfig `
            -configurationParameters $inputConfigMapped `
            -respectOrdering `
            -userInputOverrides $userInputOverrides `
            -userInputDefaultOverrides $interfaceCachedConfig `
            -treatEmptyDefaultAsValid $true `
            -autoApprove:$autoApprove.IsPresent

        # Set computed interface inputs
        $computedInputs["starter_module_name"] = $starter
        $computedInputs["module_folder_path"] = $starterModulePath
        $computedInputs["availability_zones_bootstrap"] = @(Get-AvailabilityZonesSupport -region $interfaceConfiguration.bootstrap_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") {
                $inputVariable = $interfaceConfiguration.PSObject.Properties | Where-Object { $_.Name -eq $inputConfigItem.Name }
                $inputValue = $computedInputs[$inputConfigItem.Name]
                if($inputValue -is [array]) {
                    $jsonInputValue = ConvertTo-Json $inputValue -Depth 10
                    Write-Verbose "Setting computed interface input array $($inputConfigItem.Name) to $jsonInputValue"
                } else {
                    Write-Verbose "Setting computed interface input string $($inputConfigItem.Name) to $inputValue"
                }
                $inputVariable.Value.Value = $inputValue
            }
        }

        # Split interface inputs
        $bootstrapComputed = [PSCustomObject]@{}
        $starterComputed = [PSCustomObject]@{}

        foreach($inputConfigItem in $inputConfig.inputs.PSObject.Properties) {
            $inputVariable = $interfaceConfiguration.PSObject.Properties | Where-Object { $_.Name -eq $inputConfigItem.Name }
            if("bootstrap" -in $inputConfigItem.Value.maps_to) {
                $bootstrapComputed | Add-Member -NotePropertyName $inputVariable.Name -NotePropertyValue $inputVariable.Value
            }

            if("starter" -in $inputConfigItem.Value.maps_to) {
                if($iac -eq "terraform") {
                    $starterComputed | Add-Member -NotePropertyName $inputVariable.Name -NotePropertyValue $inputVariable.Value
                }

                if($iac -eq "bicep") {
                    if($inputConfigItem.Value.PSObject.Properties.Name -contains "bicep_alias") {
                        Write-Verbose "Setting computed bicep alias $($inputConfigItem.Value.bicep_alias)"
                        $starterComputed | Add-Member -NotePropertyName $inputConfigItem.Value.bicep_alias -NotePropertyValue $inputVariable.Value
                    } else {
                        $starterComputed | Add-Member -NotePropertyName $inputVariable.Name -NotePropertyValue $inputVariable.Value
                    }
                }
            }
        }

        # Getting the user input for the bootstrap module
        Write-InformationColored "The following inputs are specific to the '$($bootstrapDetails.Name)' bootstrap module that you selected:" -ForegroundColor Green -NewLineBefore -InformationAction Continue
        $bootstrapConfiguration = Request-ALZEnvironmentConfig `
            -configurationParameters $bootstrapParameters `
            -respectOrdering `
            -userInputOverrides $userInputOverrides `
            -userInputDefaultOverrides $bootstrapCachedConfig `
            -treatEmptyDefaultAsValid $true `
            -autoApprove:$autoApprove.IsPresent `
            -computedInputs $bootstrapComputed

        # Getting the user input for the starter module
        Write-InformationColored "The following inputs are specific to the '$starter' starter module that you selected:" -ForegroundColor Green -NewLineBefore -InformationAction Continue
        $starterConfiguration = Request-ALZEnvironmentConfig `
            -configurationParameters $starterParameters `
            -respectOrdering `
            -userInputOverrides $userInputOverrides `
            -userInputDefaultOverrides $starterCachedConfig `
            -treatEmptyDefaultAsValid $true `
            -autoApprove:$autoApprove.IsPresent `
            -computedInputs $starterComputed

        # Creating the tfvars files for the bootstrap and starter module
        $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"

        # 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-TfvarsJsonFile -tfvarsFilePath $starterTfvarsPath -configuration $starterConfiguration
        }

        if($iac -eq "bicep") {
            Copy-ParametersFileCollection -starterPath $starterModulePath -configFiles $starterConfig.starter_modules.$starter.deployment_files
            Set-ComputedConfiguration -configuration $starterConfiguration
            Edit-ALZConfigurationFilesInPlace -alzEnvironmentDestination $starterModulePath -configuration $starterConfiguration
            Write-JsonFile -jsonFilePath $starterBicepVarsPath -configuration $starterConfiguration

            # Remove unrequired files
            $foldersOrFilesToRetain = $starterConfig.starter_modules.$starter.folders_or_files_to_retain
            $foldersOrFilesToRetain += "parameters.json"
            $foldersOrFilesToRetain += "config"
            $foldersOrFilesToRetain += "starter-cache.json"

            foreach($deployment_file in $starterConfig.starter_modules.$starter.deployment_files) {
                $foldersOrFilesToRetain += $deployment_file.templateParametersSourceFilePath
            }

            $subFoldersOrFilesToRemove = $starterConfig.starter_modules.$starter.subfolders_or_files_to_remove

            Remove-UnrequiredFileSet -path $starterModulePath -foldersOrFilesToRetain $foldersOrFilesToRetain -subFoldersOrFilesToRemove $subFoldersOrFilesToRemove -writeVerboseLogs:$writeVerboseLogs.IsPresent
        }

        # Caching the bootstrap and starter module values paths for retry / upgrade scenarios
        Write-ConfigurationCache -filePath $interfaceCachePath -configuration $interfaceConfiguration
        Write-ConfigurationCache -filePath $bootstrapCachePath -configuration $bootstrapConfiguration
        Write-ConfigurationCache -filePath $starterCachePath -configuration $starterConfiguration

        # Running terraform init and apply
        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 $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 $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
    }
}