diff --git a/src/ALZ/Private/Get-ALZConfig.ps1 b/src/ALZ/Private/Get-ALZConfig.ps1 index 0c135f0..0c934fa 100644 --- a/src/ALZ/Private/Get-ALZConfig.ps1 +++ b/src/ALZ/Private/Get-ALZConfig.ps1 @@ -7,6 +7,10 @@ function Get-ALZConfig { [string] $configFilePath = "" ) + if(!(Test-Path $configFilePath)) { + return $null + } + # Import the config and transform it to a PowerShell object $extension = (Get-Item -Path $configFilePath).Extension.ToLower() if($extension -eq ".yml" -or $extension -eq ".yaml") { diff --git a/src/ALZ/Private/Invoke-Terraform.ps1 b/src/ALZ/Private/Invoke-Terraform.ps1 index 8f8adb0..b067e71 100644 --- a/src/ALZ/Private/Invoke-Terraform.ps1 +++ b/src/ALZ/Private/Invoke-Terraform.ps1 @@ -8,16 +8,33 @@ function Invoke-Terraform { [string] $tfvarsFileName, [Parameter(Mandatory = $false)] - [switch] $autoApprove + [switch] $autoApprove, + + [Parameter(Mandatory = $false)] + [switch] $destroy ) if ($PSCmdlet.ShouldProcess("Apply Terraform", "modify")) { terraform -chdir="$moduleFolderPath" init - Write-InformationColored "Terraform init has completed, now running the apply..." -ForegroundColor Green -InformationAction Continue - if($autoApprove) { - terraform -chdir="$moduleFolderPath" apply -var-file="$tfvarsFileName" -auto-approve + $action = "apply" + if($destroy) { + $action = "destroy" + } + + Write-InformationColored "Terraform init has completed, now running the $action..." -ForegroundColor Green -InformationAction Continue + + if($destroy) { + if($autoApprove) { + terraform -chdir="$moduleFolderPath" destroy -var-file="$tfvarsFileName" -auto-approve + } else { + terraform -chdir="$moduleFolderPath" destroy -var-file="$tfvarsFileName" + } } else { - terraform -chdir="$moduleFolderPath" apply -var-file="$tfvarsFileName" + if($autoApprove) { + terraform -chdir="$moduleFolderPath" apply -var-file="$tfvarsFileName" -auto-approve + } else { + terraform -chdir="$moduleFolderPath" apply -var-file="$tfvarsFileName" + } } } } \ No newline at end of file diff --git a/src/ALZ/Private/Invoke-Upgrade.ps1 b/src/ALZ/Private/Invoke-Upgrade.ps1 new file mode 100644 index 0000000..52bf807 --- /dev/null +++ b/src/ALZ/Private/Invoke-Upgrade.ps1 @@ -0,0 +1,95 @@ +function Invoke-Upgrade { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $false)] + [string] $alzEnvironmentDestination, + + [Parameter(Mandatory = $false)] + [string] $bootstrapCacheFileName, + + [Parameter(Mandatory = $false)] + [string] $starterCacheFileNamePattern, + + [Parameter(Mandatory = $false)] + [string] $stateFilePathAndFileName, + + [Parameter(Mandatory = $false)] + [string] $currentVersion, + + [Parameter(Mandatory = $false)] + [switch] $autoApprove + ) + + if ($PSCmdlet.ShouldProcess("Upgrade Release", "Operation")) { + + $directories = Get-ChildItem -Path $alzEnvironmentDestination -Filter "v*" -Directory + $previousBootstrapCachedValuesPath = $null + $previousStarterCachedValuesPath = $null + $previousStateFilePath = $null + $previousVersion = $null + $foundPreviousRelease = $false + + foreach ($directory in $directories | Sort-Object -Descending -Property Name) { + $releasePath = Join-Path -Path $alzEnvironmentDestination -ChildPath $directory.Name + $releaseBootstrapCachedValuesPath = Join-Path -Path $releasePath -ChildPath $bootstrapCacheFileName + $releaseStateFilePath = Join-Path -Path $releasePath -ChildPath $stateFilePathAndFileName + + if(Test-Path $releaseBootstrapCachedValuesPath) { + $previousBootstrapCachedValuesPath = $releaseBootstrapCachedValuesPath + } + + $starterCacheFiles = Get-ChildItem -Path $releasePath -Filter $starterCacheFileNamePattern -File + + if($starterCacheFiles) { + $previousStarterCachedValuesPath = $starterCacheFiles[0].FullName + } + + if(Test-Path $releaseStateFilePath) { + $previousStateFilePath = $releaseStateFilePath + } + + if($null -ne $previousStateFilePath) { + if($directory.Name -eq $currentVersion) { + # If the current version has already been run, then skip the upgrade process + break + } + + $foundPreviousRelease = $true + $previousVersion = $directory.Name + break + } + } + + if($foundPreviousRelease) { + Write-InformationColored "AUTOMATIC UPGRADE: We found version $previousVersion that has been previously run. You can upgrade from this version to the new version $currentVersion" -ForegroundColor Yellow -InformationAction Continue + $upgrade = "" + if($autoApprove) { + $upgrade = "upgrade" + } else { + $upgrade = Read-Host "If you would like to upgrade, enter 'upgrade' or just hit 'enter' to continue with a new environment. (upgrade/exit)" + } + + if($upgrade.ToLower() -eq "upgrade") { + $currentPath = Join-Path -Path $alzEnvironmentDestination -ChildPath $currentVersion + $currentBootstrapCachedValuesPath = Join-Path -Path $currentPath -ChildPath $bootstrapCacheFileName + $currentStarterCachedValuesPath = $currentPath + $currentStateFilePath = Join-Path -Path $currentPath -ChildPath $stateFilePathAndFileName + + # Copy the previous cached values to the current release + if($null -ne $previousBootstrapCachedValuesPath) { + Write-InformationColored "AUTOMATIC UPGRADE: Copying $previousBootstrapCachedValuesPath to $currentBootstrapCachedValuesPath" -ForegroundColor Green -InformationAction Continue + Copy-Item -Path $previousBootstrapCachedValuesPath -Destination $currentBootstrapCachedValuesPath -Force | Out-String | Write-Verbose + } + if($null -ne $previousStarterCachedValuesPath) { + Write-InformationColored "AUTOMATIC UPGRADE: Copying $previousStarterCachedValuesPath to $currentStarterCachedValuesPath" -ForegroundColor Green -InformationAction Continue + Copy-Item -Path $previousStarterCachedValuesPath -Destination $currentStarterCachedValuesPath -Force | Out-String | Write-Verbose + } + + Write-InformationColored "AUTOMATIC UPGRADE: Copying $previousStateFilePath to $currentStateFilePath" -ForegroundColor Green -InformationAction Continue + Copy-Item -Path $previousStateFilePath -Destination $currentStateFilePath -Force | Out-String | Write-Verbose + + Write-InformationColored "AUTOMATIC UPGRADE: Upgrade complete. If any files in the starter have been updated, you will need to remove branch protection in order for the Terraform apply to succeed..." -ForegroundColor Yellow -InformationAction Continue + } + } + } +} diff --git a/src/ALZ/Private/New-ALZEnvironmentTerraform.ps1 b/src/ALZ/Private/New-ALZEnvironmentTerraform.ps1 index 0da4c5f..33c8369 100644 --- a/src/ALZ/Private/New-ALZEnvironmentTerraform.ps1 +++ b/src/ALZ/Private/New-ALZEnvironmentTerraform.ps1 @@ -20,7 +20,10 @@ function New-ALZEnvironmentTerraform { [string] $userInputOverridePath = "", [Parameter(Mandatory = $false)] - [switch] $autoApprove + [switch] $autoApprove, + + [Parameter(Mandatory = $false)] + [switch] $destroy ) if ($PSCmdlet.ShouldProcess("ALZ-Terraform module configuration", "modify")) { @@ -36,10 +39,20 @@ function New-ALZEnvironmentTerraform { $userInputOverrides = Get-ALZConfig -configFilePath $userInputOverridePath } + # Setup Cache Paths + $bootstrapCacheFileName = "cache-bootstrap-$alzCicdPlatform.json" + $starterCacheFileNamePattern = "cache-starter-*.json" + # Downloading the latest or specified version of the alz-terraform-accelerator module + if(!($alzVersion.StartsWith("v"))) { + $alzVersion = "v$alzVersion" + } $releaseTag = Get-ALZGithubRelease -directoryForReleases $alzEnvironmentDestination -iac "terraform" -release $alzVersion $releasePath = Join-Path -Path $alzEnvironmentDestination -ChildPath $releaseTag + # Run upgrade + Invoke-Upgrade -alzEnvironmentDestination $alzEnvironmentDestination -bootstrapCacheFileName $bootstrapCacheFileName -starterCacheFileNamePattern $starterCacheFileNamePattern -stateFilePathAndFileName "bootstrap/$alzCicdPlatform/terraform.tfstate" -currentVersion $releaseTag -autoApprove:$autoApprove.IsPresent + # Getting the configuration for the initial bootstrap user input and validators $bootstrapConfigFilePath = Join-Path -Path $releasePath -ChildPath "bootstrap/.config/ALZ-Powershell.config.json" $bootstrapConfig = Get-ALZConfig -configFilePath $bootstrapConfigFilePath @@ -52,8 +65,12 @@ function New-ALZEnvironmentTerraform { Write-InformationColored "Got configuration and downloaded alz-terraform-accelerator Terraform module version $releaseTag to $alzEnvironmentDestination" -ForegroundColor Green -InformationAction Continue + #Checking for cached bootstrap values for retry / upgrade scenarios + $bootstrapCachedValuesPath = Join-Path -Path $releasePath -ChildPath $bootstrapCacheFileName + $cachedBootstrapConfig = Get-ALZConfig -configFilePath $bootstrapCachedValuesPath + # Getting the user input for the bootstrap module - $bootstrapConfiguration = Request-ALZEnvironmentConfig -configurationParameters $bootstrapParameters -respectOrdering -userInputOverrides $userInputOverrides -treatEmptyDefaultAsValid $true + $bootstrapConfiguration = Request-ALZEnvironmentConfig -configurationParameters $bootstrapParameters -respectOrdering -userInputOverrides $userInputOverrides -userInputDefaultOverrides $cachedBootstrapConfig -treatEmptyDefaultAsValid $true # Getting the configuration for the starter module user input $starterTemplate = $bootstrapConfiguration.PsObject.Properties["starter_module"].Value.Value @@ -63,8 +80,13 @@ function New-ALZEnvironmentTerraform { Write-InformationColored "The following inputs are specific to the '$starterTemplate' starter module that you selected..." -ForegroundColor Green -InformationAction Continue + # Checking for cached starter module values for retry / upgrade scenarios + $starterCacheFileName = "cache-starter-$starterTemplate.json" + $starterModuleCachedValuesPath = Join-Path -Path $releasePath -ChildPath $starterCacheFileName + $cachedStarterModuleConfig = Get-ALZConfig -configFilePath $starterModuleCachedValuesPath + # Getting the user input for the starter module - $starterModuleConfiguration = Request-ALZEnvironmentConfig -configurationParameters $starterModuleParameters -respectOrdering -userInputOverrides $userInputOverrides -treatEmptyDefaultAsValid $true + $starterModuleConfiguration = Request-ALZEnvironmentConfig -configurationParameters $starterModuleParameters -respectOrdering -userInputOverrides $userInputOverrides -userInputDefaultOverrides $cachedStarterModuleConfig -treatEmptyDefaultAsValid $true # Getting subscription ids Import-SubscriptionData -starterModuleConfiguration $starterModuleConfiguration -bootstrapConfiguration $bootstrapConfiguration @@ -75,14 +97,18 @@ function New-ALZEnvironmentTerraform { Write-TfvarsFile -tfvarsFilePath $bootstrapTfvarsPath -configuration $bootstrapConfiguration Write-TfvarsFile -tfvarsFilePath $starterModuleTfvarsPath -configuration $starterModuleConfiguration + # Caching the bootstrap and starter module values paths for retry / upgrade scenarios + Write-ConfigurationCache -filePath $bootstrapCachedValuesPath -configuration $bootstrapConfiguration + Write-ConfigurationCache -filePath $starterModuleCachedValuesPath -configuration $starterModuleConfiguration + # 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 -InformationAction Continue if($autoApprove) { - Invoke-Terraform -moduleFolderPath $bootstrapPath -tfvarsFileName "override.tfvars" -autoApprove + Invoke-Terraform -moduleFolderPath $bootstrapPath -tfvarsFileName "override.tfvars" -autoApprove -destroy:$destroy.IsPresent } else { Write-InformationColored "Once the plan is complete you will be prompted to confirm the apply. You must enter 'yes' to apply." -ForegroundColor Green -InformationAction Continue - Invoke-Terraform -moduleFolderPath $bootstrapPath -tfvarsFileName "override.tfvars" + Invoke-Terraform -moduleFolderPath $bootstrapPath -tfvarsFileName "override.tfvars" -destroy:$destroy.IsPresent } } } \ No newline at end of file diff --git a/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 b/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 index eb93f83..64b2daa 100644 --- a/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 +++ b/src/ALZ/Private/Request-ALZEnvironmentConfig.ps1 @@ -8,7 +8,11 @@ function Request-ALZEnvironmentConfig { [Parameter(Mandatory = $false)] [PSCustomObject] $userInputOverrides = $null, [Parameter(Mandatory = $false)] - [System.Boolean] $treatEmptyDefaultAsValid = $false + [PSCustomObject] $userInputDefaultOverrides = $null, + [Parameter(Mandatory = $false)] + [System.Boolean] $treatEmptyDefaultAsValid = $false, + [Parameter(Mandatory = $false)] + [switch] $autoApprove ) <# .SYNOPSIS @@ -23,6 +27,21 @@ function Request-ALZEnvironmentConfig { $configurations = $configurationParameters.PsObject.Properties + $hasDefaultOverrides = $false + if($userInputDefaultOverrides -ne $null) { + $hasDefaultOverrides = $true + Write-InformationColored "We found you have cached values from a previous run." -ForegroundColor Yellow -InformationAction Continue + $useDefaults = "" + if($autoApprove) { + $useDefaults = "use" + } else { + $useDefaults = Read-Host "Would you like to use these values or see each of them to validate and change them? Enter 'use' to use the cached value or just hit 'enter' to see and validate each value. (use/see)" + } + if($useDefaults.ToLower() -eq "use") { + $userInputOverrides = $userInputDefaultOverrides + } + } + $hasInputOverrides = $false if($userInputOverrides -ne $null) { $hasInputOverrides = $true @@ -34,6 +53,20 @@ function Request-ALZEnvironmentConfig { foreach ($configurationValue in $configurations) { if ($configurationValue.Value.Type -eq "UserInput") { + + # Check for and add cached as default + if($hasDefaultOverrides) { + $defaultOverride = $userInputDefaultOverrides.PsObject.Properties | Where-Object { $_.Name -eq $configurationValue.Name } + if($null -ne $defaultOverride) { + if(!($configurationValue.Value.PSObject.Properties.Name -match "DefaultValue")) { + $configurationValue.Value | Add-Member -NotePropertyName "DefaultValue" -NotePropertyValue $defaultOverride.Value + } else { + $configurationValue.Value.DefaultValue = $defaultOverride.Value + } + } + } + + # Check for and use override if($hasInputOverrides) { $userInputOverride = $userInputOverrides.PsObject.Properties | Where-Object { $_.Name -eq $configurationValue.Name } if($null -ne $userInputOverride) { diff --git a/src/ALZ/Private/Write-ConfigurationCache.ps1 b/src/ALZ/Private/Write-ConfigurationCache.ps1 new file mode 100644 index 0000000..b2d3d16 --- /dev/null +++ b/src/ALZ/Private/Write-ConfigurationCache.ps1 @@ -0,0 +1,24 @@ +function Write-ConfigurationCache { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $false)] + [string] $filePath, + + [Parameter(Mandatory = $false)] + [PSObject] $configuration + ) + + if ($PSCmdlet.ShouldProcess("Download Terraform Tools", "modify")) { + + if(Test-Path $filePath) { + Remove-Item -Path $filePath + } + + $cache = [PSCustomObject]@{} + foreach ($configurationItem in $configuration.PSObject.Properties) { + $cache | Add-Member -NotePropertyName $configurationItem.Name -NotePropertyValue $configurationItem.Value.Value + } + + $cache | ConvertTo-Json | Out-File -FilePath $filePath + } +} \ No newline at end of file diff --git a/src/ALZ/Public/New-ALZEnvironment.ps1 b/src/ALZ/Public/New-ALZEnvironment.ps1 index 327df43..07acf6f 100644 --- a/src/ALZ/Public/New-ALZEnvironment.ps1 +++ b/src/ALZ/Public/New-ALZEnvironment.ps1 @@ -16,6 +16,8 @@ function New-ALZEnvironment { A json file containing user input overrides for the user input prompts. This will cause the tool to by pass requesting user input for that field and use the value(s) provided. E.g { "starter_module": "basic", "azure_location": "uksouth" } .PARAMETER autoApprove Automatically approve the terraform apply. + .PARAMETER destroy + Destroy the terraform environment. .EXAMPLE New-ALZEnvironment .EXAMPLE @@ -56,7 +58,10 @@ function New-ALZEnvironment { [string] $userInputOverridePath = "", [Parameter(Mandatory = $false)] - [switch] $autoApprove + [switch] $autoApprove, + + [Parameter(Mandatory = $false)] + [switch] $destroy ) Write-InformationColored "Getting ready to create a new ALZ environment with you..." -ForegroundColor Green -InformationAction Continue @@ -67,11 +72,7 @@ function New-ALZEnvironment { } if($alzIacProvider -eq "terraform") { - if($autoApprove) { - New-ALZEnvironmentTerraform -alzEnvironmentDestination $alzEnvironmentDestination -alzVersion $alzVersion -alzCicdPlatform $alzCicdPlatform -userInputOverridePath $userInputOverridePath -autoApprove - } else { - New-ALZEnvironmentTerraform -alzEnvironmentDestination $alzEnvironmentDestination -alzVersion $alzVersion -alzCicdPlatform $alzCicdPlatform -userInputOverridePath $userInputOverridePath - } + New-ALZEnvironmentTerraform -alzEnvironmentDestination $alzEnvironmentDestination -alzVersion $alzVersion -alzCicdPlatform $alzCicdPlatform -userInputOverridePath $userInputOverridePath -autoApprove:$autoApprove.IsPresent -destroy:$destroy.IsPresent } } diff --git a/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 b/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 index d6b277e..774e121 100644 --- a/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 +++ b/src/Tests/Unit/Public/New-ALZEnvironment.Tests.ps1 @@ -108,10 +108,13 @@ InModuleScope 'ALZ' { Mock -CommandName Write-TfvarsFile -MockWith { } + Mock -CommandName Write-ConfigurationCache -MockWith { } + Mock -CommandName Invoke-Terraform -MockWith { } Mock -CommandName Import-SubscriptionData -MockWith { } + Mock -CommandName Invoke-Upgrade -MockWith { } } It 'should return the output directory on completion' { @@ -120,7 +123,7 @@ InModuleScope 'ALZ' { } It 'should clone the git repo and apply if terraform is selected' { - New-ALZEnvironment -IaC "terraform" - + New-ALZEnvironment -IaC "terraform" Assert-MockCalled -CommandName Get-ALZGithubRelease -Exactly 1 Assert-MockCalled -CommandName Invoke-Terraform -Exactly 1 }