diff --git a/.gitignore b/.gitignore index efa7b79a..fa711bba 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ templates/.test_github .vscode/settings.json /ALZ .tools +.cache diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AcceleratorFolderConfiguration.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AcceleratorFolderConfiguration.ps1 index 51e6fb8e..8c24ac45 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AcceleratorFolderConfiguration.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AcceleratorFolderConfiguration.ps1 @@ -30,6 +30,7 @@ function Get-AcceleratorFolderConfiguration { IacType = $null VersionControl = $null ConfigFolderPath = $null + OutputFolderPath = $null InputsYamlPath = $null InputsYaml = $null InputsContent = $null @@ -45,9 +46,11 @@ function Get-AcceleratorFolderConfiguration { $result.FolderExists = $true $configFolderPath = Join-Path $FolderPath "config" + $outputFolderPath = Join-Path $FolderPath "output" $inputsYamlPath = Join-Path $configFolderPath "inputs.yaml" $result.ConfigFolderPath = $configFolderPath + $result.OutputFolderPath = $outputFolderPath $result.InputsYamlPath = $inputsYamlPath # Check if config folder exists diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AzureContext.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AzureContext.ps1 index 779743e7..57043c32 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AzureContext.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-AzureContext.ps1 @@ -7,6 +7,11 @@ function Get-AzureContext { that the currently logged-in user has access to. The results are returned as a hashtable containing arrays for use in interactive selection prompts. Only subscriptions from the current tenant are returned. + Results are cached locally for 1 hour to improve performance. + .PARAMETER OutputDirectory + The output directory where the .cache folder will be created for storing the cached Azure context. + .PARAMETER ClearCache + When set, clears the cached Azure context and fetches fresh data from Azure. .OUTPUTS Returns a hashtable with the following keys: - ManagementGroups: Array of objects with id and displayName properties @@ -14,7 +19,40 @@ function Get-AzureContext { - Regions: Array of objects with name, displayName, and hasAvailabilityZones properties #> [CmdletBinding()] - param() + param( + [Parameter(Mandatory = $true)] + [string]$OutputDirectory, + + [Parameter(Mandatory = $false)] + [switch]$ClearCache + ) + + # Define cache file path and expiration time (1 hour) + $cacheFolder = Join-Path $OutputDirectory ".cache" + $cacheFilePath = Join-Path $cacheFolder "azure-context-cache.json" + $cacheExpirationHours = 24 + + # Clear cache if requested + if ($ClearCache.IsPresent -and (Test-Path $cacheFilePath)) { + Remove-Item -Path $cacheFilePath -Force + Write-InformationColored "Azure context cache cleared." -ForegroundColor Yellow -InformationAction Continue + } + + # Check if valid cache exists + if (Test-Path $cacheFilePath) { + $cacheFile = Get-Item $cacheFilePath + $cacheAge = (Get-Date) - $cacheFile.LastWriteTime + if ($cacheAge.TotalHours -lt $cacheExpirationHours) { + try { + $cachedContext = Get-Content -Path $cacheFilePath -Raw | ConvertFrom-Json -AsHashtable + Write-InformationColored "Using cached Azure context (cached $([math]::Round($cacheAge.TotalMinutes)) minutes ago). Use -clearCache to refresh." -ForegroundColor Gray -InformationAction Continue + Write-InformationColored " Found $($cachedContext.ManagementGroups.Count) management groups, $($cachedContext.Subscriptions.Count) subscriptions, and $($cachedContext.Regions.Count) regions" -ForegroundColor Gray -InformationAction Continue + return $cachedContext + } catch { + Write-Verbose "Failed to read cache file, will fetch fresh data." + } + } + } $azureContext = @{ ManagementGroups = @() @@ -52,6 +90,17 @@ function Get-AzureContext { } Write-InformationColored " Found $($azureContext.ManagementGroups.Count) management groups, $($azureContext.Subscriptions.Count) subscriptions, and $($azureContext.Regions.Count) regions" -ForegroundColor Gray -InformationAction Continue + + # Save to cache + try { + if (-not (Test-Path $cacheFolder)) { + New-Item -Path $cacheFolder -ItemType Directory -Force | Out-Null + } + $azureContext | ConvertTo-Json -Depth 10 | Set-Content -Path $cacheFilePath -Force + Write-Verbose "Azure context cached to $cacheFilePath" + } catch { + Write-Verbose "Failed to write cache file: $_" + } } catch { Write-InformationColored " Warning: Could not query Azure resources. You will need to enter IDs manually." -ForegroundColor Yellow -InformationAction Continue } diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 deleted file mode 100644 index 701f7f7a..00000000 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ExistingLocalRelease.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -function Get-ExistingLocalRelease { - param( - [Parameter(Mandatory = $false)] - [string] $targetDirectory, - - [Parameter(Mandatory = $false)] - [string] $targetFolder - ) - - $releaseTag = "" - $path = "" - $checkPath = Join-Path $targetDirectory $targetFolder - $checkFolders = Get-ChildItem -Path $checkPath -Directory - if ($null -ne $checkFolders) { - $checkFolders = $checkFolders | Sort-Object { $_.Name } -Descending - $mostRecentCheckFolder = $checkFolders[0] - - $releaseTag = $mostRecentCheckFolder.Name - $path = $mostRecentCheckFolder.FullName - } else { - Write-InformationColored "You have passed the skipInternetChecks parameter, but there is no existing version in the $targetFolder module, so we can't continue." - throw - } - - return @{ - releaseTag = $releaseTag - path = $path - } -} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ModuleVersionData.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ModuleVersionData.ps1 new file mode 100644 index 00000000..e839489a --- /dev/null +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Get-ModuleVersionData.ps1 @@ -0,0 +1,21 @@ +function Get-ModuleVersionData { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $targetDirectory, + + [Parameter(Mandatory = $true)] + [ValidateSet("bootstrap", "starter")] + [string] $moduleType + ) + + $dataFilePath = Join-Path $targetDirectory ".alz-version-data.json" + + if (Test-Path $dataFilePath) { + $data = Get-Content $dataFilePath | ConvertFrom-Json + $versionKey = "$($moduleType)Version" + return $data.$versionKey + } + + return $null +} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-FullUpgrade.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-FullUpgrade.ps1 deleted file mode 100644 index 05dc1854..00000000 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-FullUpgrade.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -function Invoke-FullUpgrade { - [CmdletBinding(SupportsShouldProcess = $true)] - param ( - [Parameter(Mandatory = $false)] - [string] $bootstrapRelease, - - [Parameter(Mandatory = $false)] - [string] $bootstrapPath, - - [Parameter(Mandatory = $false)] - [string] $bootstrapModuleFolder, - - [Parameter(Mandatory = $false)] - [switch] $autoApprove - ) - - if ($PSCmdlet.ShouldProcess("Upgrade Release", "Operation")) { - - # Run upgrade for bootstrap state - $bootstrapWasUpgraded = Invoke-Upgrade ` - -moduleType "bootstrap" ` - -targetDirectory $bootstrapPath ` - -targetFolder $bootstrapModuleFolder ` - -cacheFileName "terraform.tfstate" ` - -release $bootstrapRelease ` - -autoApprove:$autoApprove.IsPresent - - if($bootstrapWasUpgraded) { - Write-InformationColored "AUTOMATIC UPGRADE: Upgrade complete. If any starter files have been updated, you will need to remove branch protection in order for the Terraform apply to succeed." -NewLineBefore -ForegroundColor Yellow -InformationAction Continue - } - } -} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Upgrade.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Upgrade.ps1 deleted file mode 100644 index 164f6602..00000000 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Invoke-Upgrade.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -function Invoke-Upgrade { - [CmdletBinding(SupportsShouldProcess = $true)] - param ( - [Parameter(Mandatory = $false)] - [string] $moduleType, - - [Parameter(Mandatory = $false)] - [string] $targetDirectory, - - [Parameter(Mandatory = $false)] - [string] $targetFolder = "", - - [Parameter(Mandatory = $false)] - [string] $cacheFileName, - - [Parameter(Mandatory = $false)] - [string] $release, - - [Parameter(Mandatory = $false)] - [switch] $autoApprove - ) - - if ($PSCmdlet.ShouldProcess("Upgrade Release", "Operation")) { - - $directories = Get-ChildItem -Path $targetDirectory -Filter "v*" -Directory - $previousCachedValuesPath = $null - $previousVersion = $null - $foundPreviousRelease = $false - - Write-Verbose "UPGRADE: Checking for existing directories in $targetDirectory" - - foreach ($directory in $directories | Sort-Object -Descending -Property Name) { - $releasePath = Join-Path $targetDirectory $directory.Name - $releaseCachedValuesPath = Join-Path $releasePath $targetFolder $cacheFileName - - Write-Verbose "UPGRADE: Checking for existing file in $releasePath, specifically $releaseCachedValuesPath" - - if(Test-Path $releaseCachedValuesPath) { - $previousCachedValuesPath = $releaseCachedValuesPath - } - - if($null -ne $previousCachedValuesPath) { - if($directory.Name -eq $release) { - Write-Verbose "Latest version $release has already been run. Skipping upgrade..." - # If the current version has already been run, then skip the upgrade process - break - } - - $foundPreviousRelease = $true - $previousVersion = $directory.Name - break - } - } - - if($foundPreviousRelease) { - $upgrade = $true - if($autoApprove) { - $upgrade = $true - } else { - Write-InformationColored "AUTOMATIC UPGRADE: We found version $previousVersion of the $moduleType module that has been previously run. You can migrate your settings and state from this version to the new version $currentVersion" -NewLineBefore -ForegroundColor Yellow -InformationAction Continue - $choices = [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No") - $message = "Please confirm you wish to migrate your previous settings and state to the new version." - $title = "Confirm migrate settings and state" - $resultIndex = $host.ui.PromptForChoice($title, $message, $choices, 0) - - if($resultIndex -eq 1) { - Write-InformationColored "You have chosen not to migrate your settings and state. Please note that your state file is still in the folder for the previous version if this was a mistake." -ForegroundColor Yellow -NewLineBefore -InformationAction Continue - $upgrade = $false - } - } - - if($upgrade) { - $currentPath = Join-Path $targetDirectory $release - $currentCachedValuesPath = Join-Path $currentPath $targetFolder $cacheFileName - - # Copy the previous cached values to the current release - if($null -ne $previousCachedValuesPath) { - Write-InformationColored "AUTOMATIC UPGRADE: Copying $previousCachedValuesPath to $currentCachedValuesPath" -ForegroundColor Green -InformationAction Continue - Copy-Item -Path $previousCachedValuesPath -Destination $currentCachedValuesPath -Force | Out-String | Write-Verbose - } - - return $true - } - } - - return $false - } -} diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 index fa583cbc..e8a4d7c7 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-Bootstrap.ps1 @@ -76,13 +76,6 @@ function New-Bootstrap { Write-Verbose "Bootstrap Module Path: $bootstrapModulePath" - # Run upgrade - Invoke-FullUpgrade ` - -bootstrapModuleFolder $bootstrapDetails.Value.location ` - -bootstrapRelease $bootstrapRelease ` - -bootstrapPath $bootstrapTargetPath ` - -autoApprove:$autoApprove.IsPresent - # Get starter module $starterModulePath = "" $starterRootModuleFolder = "" diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-ModuleSetup.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-ModuleSetup.ps1 index cd59b4d4..712660c8 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/New-ModuleSetup.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/New-ModuleSetup.ps1 @@ -19,25 +19,139 @@ function New-ModuleSetup { [Parameter(Mandatory = $false)] [bool]$skipInternetChecks, [Parameter(Mandatory = $false)] - [switch]$replaceFiles + [switch]$replaceFiles, + [Parameter(Mandatory = $false)] + [switch]$upgrade, + [Parameter(Mandatory = $false)] + [switch]$autoApprove ) if ($PSCmdlet.ShouldProcess("Check and get module", "modify")) { - $versionAndPath = $null + + $currentVersion = Get-ModuleVersionData -targetDirectory $targetDirectory -moduleType $targetFolder + $versionAndPath = @{ + path = Join-Path $targetDirectory $targetFolder $currentVersion + releaseTag = $currentVersion + } + Write-Verbose "Current $targetFolder module version: $currentVersion" + Write-Verbose "Current $targetFolder module path: $($versionAndPath.path)" if($skipInternetChecks) { - $versionAndPath = Get-ExistingLocalRelease -targetDirectory $targetDirectory -targetFolder $targetFolder - } else { + return $versionAndPath + } + + $latestReleaseTag = $null + try { + $latestResult = Get-GithubReleaseTag -githubRepoUrl $url -release "latest" + $latestReleaseTag = $latestResult.ReleaseTag + Write-Verbose "Latest available $targetFolder version: $latestReleaseTag" + } catch { + Write-Verbose "Could not check for latest version: $($_.Exception.Message)" + } + + $isAutoVersion = $release -eq "latest" + $firstRun = $null -eq $currentVersion + $shouldDownload = $false + + if($isAutoVersion -and $upgrade.IsPresent -and $null -eq $latestReleaseTag) { + throw "Cannot perform upgrade to latest version as unable to determine latest release from GitHub." + } + + if($isAutoVersion -and $upgrade.IsPresent -and $currentVersion -ne $latestReleaseTag) { + Write-Verbose "Auto version upgrade requested and newer version available." + $shouldDownload = $true + } + + if(!$isAutoVersion -and $upgrade.IsPresent -and $release -ne $currentVersion -and $currentVersion -ne $latestReleaseTag) { + Write-Verbose "Specific version upgrade requested and newer version available." + $shouldDownload = $true + } + + if($firstRun) { + Write-Verbose "First run detected, will download specified version." + $shouldDownload = $true + } + + if(!$shouldDownload -or $isFirstRun) { + $newVersionAvailable = $false + $currentCalculatedVersion = $currentVersion + if($isAutoVersion -and $null -ne $latestReleaseTag -and $latestReleaseTag -ne $currentVersion) { + $newVersionAvailable = $true + } + + if(!$isAutoVersion -and $null -ne $latestReleaseTag -and $latestReleaseTag -ne $currentVersion) { + $newVersionAvailable = $true + } + + if($isFirstRun -and !$isAutoVersion -and $release -ne $latestReleaseTag) { + $currentCalculatedVersion = $release + $newVersionAvailable = $true + } + + if($newVersionAvailable) { + Write-InformationColored "INFO: A newer $targetFolder module version is available ($latestReleaseTag). You are currently using $currentCalculatedVersion." -ForegroundColor Cyan -InformationAction Continue + Write-InformationColored " To upgrade, run with the -upgrade flag." -ForegroundColor Cyan -InformationAction Continue + } else { + if(!$firstRun) { + if($upgrade.IsPresent) { + Write-InformationColored "No upgrade required for $targetFolder module; already at latest version ($currentCalculatedVersion)." -ForegroundColor Yellow -InformationAction Continue + } + Write-InformationColored "Using existing $targetFolder module version ($currentCalculatedVersion)." -ForegroundColor Green -InformationAction Continue + } else { + Write-InformationColored "Using specified $targetFolder module version ($currentCalculatedVersion) for the first run." -ForegroundColor Green -InformationAction Continue + } + } + } + + if ($shouldDownload) { + + $previousVersionPath = $versionAndPath.path + $desiredRelease = $isAutoVersion ? $latestReleaseTag : $release + Write-InformationColored "Upgrading $targetFolder module from $currentVersion to $desiredRelease" -ForegroundColor Yellow -InformationAction Continue + + if (-not $autoApprove.IsPresent) { + $confirm = Read-Host "Do you want to proceed with the upgrade? (y/n)" + if ($confirm -ne "y" -and $confirm -ne "Y") { + Write-InformationColored "Upgrade declined. Continuing with existing version $currentVersion." -ForegroundColor Yellow -InformationAction Continue + return $versionAndPath + } + } + $versionAndPath = New-FolderStructure ` -targetDirectory $targetDirectory ` -url $url ` - -release $release ` + -release $desiredRelease ` -releaseArtifactName $releaseArtifactName ` -targetFolder $targetFolder ` -sourceFolder $sourceFolder ` -overrideSourceDirectoryPath $moduleOverrideFolderPath ` -replaceFiles:$replaceFiles.IsPresent + + Write-Verbose "New version: $($versionAndPath.releaseTag) at path: $($versionAndPath.path)" + + if (!$isFirstRun) { + Write-Verbose "Checking for state files at: $previousStatePath" + $previousStateFiles = Get-ChildItem $previousVersionPath -Filter "terraform.tfstate" -Recurse | Select-Object -First 1 | ForEach-Object { $_.FullName } + + if ($previousStateFiles.Count -gt 0) { + foreach ($stateFile in $previousStateFiles) { + $previousStateFilePath = $stateFile + $newStateFilePath = $previousStateFilePath.Replace($previousVersionPath, $versionAndPath.path) + Write-InformationColored "Copying state file from $previousStateFilePath to $newStateFilePath" -ForegroundColor Green -InformationAction Continue + Copy-Item -Path $previousStateFilePath -Destination $newStateFilePath -Force | Out-String | Write-Verbose + } + } else { + Write-Verbose "No state files found at $previousVersionPath - skipping migration" + } + + Write-InformationColored "Module $targetFolder upgraded from version $currentVersion to $($versionAndPath.releaseTag)." -ForegroundColor Green -InformationAction Continue + Write-InformationColored " If any repository files have been updated in the new version, you'll need to turn off branch protection for the run to succeed..." -ForegroundColor Yellow -InformationAction Continue + } + + # Update version data + Set-ModuleVersionData -targetDirectory $targetDirectory -moduleType $targetFolder -version $versionAndPath.releaseTag | Out-Null } + return $versionAndPath } } diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Request-AcceleratorConfigurationInput.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Request-AcceleratorConfigurationInput.ps1 index a0d043c0..e1214f63 100644 --- a/src/ALZ/Private/Deploy-Accelerator-Helpers/Request-AcceleratorConfigurationInput.ps1 +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Request-AcceleratorConfigurationInput.ps1 @@ -18,7 +18,13 @@ function Request-AcceleratorConfigurationInput { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $false)] - [switch] $Destroy + [switch] $Destroy, + + [Parameter(Mandatory = $false)] + [switch] $ClearCache, + + [Parameter(Mandatory = $false)] + [string] $OutputFolderName = "output" ) if ($PSCmdlet.ShouldProcess("Accelerator folder structure setup", "prompt and create")) { @@ -99,7 +105,7 @@ function Request-AcceleratorConfigurationInput { return ConvertTo-AcceleratorResult -Continue $true ` -InputConfigFilePaths $configPaths.InputConfigFilePaths ` -StarterAdditionalFiles $configPaths.StarterAdditionalFiles ` - -OutputFolderPath "$resolvedTargetPath/output" + -OutputFolderPath $folderConfig.OutputFolderPath } # Set selected values from detected values (for use existing folder case) @@ -161,6 +167,7 @@ function Request-AcceleratorConfigurationInput { -versionControl $selectedVersionControl ` -scenarioNumber $selectedScenarioNumber ` -targetFolderPath $targetFolderPathInput ` + -outputFolderName $OutputFolderName ` -force:$forceFlag Write-InformationColored "`nFolder structure created at: $normalizedTargetPath" -ForegroundColor Green -InformationAction Continue @@ -168,6 +175,7 @@ function Request-AcceleratorConfigurationInput { # Resolve the path after folder creation or validation $resolvedTargetPath = (Resolve-Path -Path $normalizedTargetPath).Path + $outputFolderPath = Join-Path $resolvedTargetPath $OutputFolderName $configFolderPath = if ($useExistingFolder) { $folderConfig.ConfigFolderPath } else { @@ -182,7 +190,7 @@ function Request-AcceleratorConfigurationInput { # Offer to configure inputs interactively (default is Yes) $configureNowResponse = Read-Host "`nWould you like to configure the input values interactively now? (Y/n)" if ($configureNowResponse -ne "n" -and $configureNowResponse -ne "N") { - $azureContext = Get-AzureContext + $azureContext = Get-AzureContext -OutputDirectory $outputFolderPath -ClearCache:$ClearCache.IsPresent Request-ALZConfigurationValue ` -ConfigFolderPath $configFolderPath ` @@ -236,19 +244,19 @@ function Request-AcceleratorConfigurationInput { Deploy-Accelerator `` -inputs "$configFolderPath/inputs.yaml", "$configFolderPath/platform-landing-zone.tfvars" `` -starterAdditionalFiles "$configFolderPath/lib" `` - -output "$resolvedTargetPath/output" + -output "$outputFolderPath" "@ -ForegroundColor Cyan -InformationAction Continue } elseif ($selectedIacType -eq "bicep") { Write-InformationColored @" Deploy-Accelerator `` -inputs "$configFolderPath/inputs.yaml", "$configFolderPath/platform-landing-zone.yaml" `` - -output "$resolvedTargetPath/output" + -output "$outputFolderPath" "@ -ForegroundColor Cyan -InformationAction Continue } else { Write-InformationColored @" Deploy-Accelerator `` -inputs "$configFolderPath/inputs.yaml" `` - -output "$resolvedTargetPath/output" + -output "$outputFolderPath" "@ -ForegroundColor Cyan -InformationAction Continue } @@ -263,6 +271,6 @@ Deploy-Accelerator `` return ConvertTo-AcceleratorResult -Continue $true ` -InputConfigFilePaths $configPaths.InputConfigFilePaths ` -StarterAdditionalFiles $configPaths.StarterAdditionalFiles ` - -OutputFolderPath "$resolvedTargetPath/output" + -OutputFolderPath $outputFolderPath } } diff --git a/src/ALZ/Private/Deploy-Accelerator-Helpers/Set-ModuleVersionData.ps1 b/src/ALZ/Private/Deploy-Accelerator-Helpers/Set-ModuleVersionData.ps1 new file mode 100644 index 00000000..807eb926 --- /dev/null +++ b/src/ALZ/Private/Deploy-Accelerator-Helpers/Set-ModuleVersionData.ps1 @@ -0,0 +1,39 @@ +function Set-ModuleVersionData { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [string] $targetDirectory, + + [Parameter(Mandatory = $true)] + [ValidateSet("bootstrap", "starter")] + [string] $moduleType, + + [Parameter(Mandatory = $true)] + [string] $version + ) + + if ($PSCmdlet.ShouldProcess($targetDirectory, "Set module version data")) { + $dataFilePath = Join-Path $targetDirectory ".alz-version-data.json" + + # Load existing data or create new + if (Test-Path $dataFilePath) { + $data = Get-Content $dataFilePath | ConvertFrom-Json + } else { + $data = [PSCustomObject]@{ + bootstrapVersion = $null + starterVersion = $null + lastUpdated = $null + } + } + + # Update the data + $versionKey = "$($moduleType)Version" + $data.$versionKey = $version + $data.lastUpdated = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ") + + # Save data + $data | ConvertTo-Json -Depth 10 | Set-Content $dataFilePath + + return $data + } +} diff --git a/src/ALZ/Private/Shared/Get-GithubRelease.ps1 b/src/ALZ/Private/Shared/Get-GithubRelease.ps1 index 977c68a2..8469c985 100644 --- a/src/ALZ/Private/Shared/Get-GithubRelease.ps1 +++ b/src/ALZ/Private/Shared/Get-GithubRelease.ps1 @@ -26,8 +26,8 @@ function Get-GithubRelease { [string] $githubRepoUrl, - [Parameter(Mandatory = $false, Position = 2, HelpMessage = "The releases to download. Specify 'latest' to download the latest release. Defaults to the latest release.")] - [array] + [Parameter(Mandatory = $false, Position = 2, HelpMessage = "The release to download. Specify 'latest' to download the latest release. Defaults to the latest release.")] + [string] $release = "latest", [Parameter(Mandatory = $true, Position = 3, HelpMessage = "The directory to download the releases to.")] @@ -51,39 +51,12 @@ function Get-GithubRelease { $parentDirectory = $targetDirectory $targetPath = Join-Path $targetDirectory $moduleTargetFolder - # Split Repo URL into parts - $repoOrgPlusRepo = $githubRepoUrl.Split("/")[-2..-1] -join "/" - - Write-Verbose "=====> Checking for release on GitHub Repo: $repoOrgPlusRepo" - - # Get releases on repo - $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/$release" - if($release -ne "latest") { - $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/tags/$release" - } - - $releaseData = Invoke-RestMethod $repoReleaseUrl -SkipHttpErrorCheck -StatusCodeVariable "statusCode" - - Write-Verbose "Status code: $statusCode" - - if($statusCode -eq 404) { - Write-Error "The release $release does not exist in the GitHub repository $githubRepoUrl - $repoReleaseUrl" - throw "The release $release does not exist in the GitHub repository $githubRepoUrl - $repoReleaseUrl" - } - - # Handle transient errors like throttling - if($statusCode -ge 400 -and $statusCode -le 599) { - Write-InformationColored "Retrying as got the Status Code $statusCode, which may be a transient error." -ForegroundColor Yellow -InformationAction Continue - $releaseData = Invoke-RestMethod $repoReleaseUrl -RetryIntervalSec 3 -MaximumRetryCount 100 - } - - if($statusCode -ne 200) { - throw "Unable to query repository version, please check your internet connection and try again..." - } - - $releaseTag = $releaseData.tag_name + # Get the release tag and data from GitHub + $releaseResult = Get-GithubReleaseTag -githubRepoUrl $githubRepoUrl -release $release + $releaseTag = $releaseResult.ReleaseTag + $releaseData = $releaseResult.ReleaseData - if($queryOnly) { + if ($queryOnly) { return $releaseTag } diff --git a/src/ALZ/Private/Shared/Get-GithubReleaseTag.ps1 b/src/ALZ/Private/Shared/Get-GithubReleaseTag.ps1 new file mode 100644 index 00000000..aab6e40d --- /dev/null +++ b/src/ALZ/Private/Shared/Get-GithubReleaseTag.ps1 @@ -0,0 +1,70 @@ +#################################### +# Get-GithubReleaseTag.ps1 # +#################################### +# Version: 0.1.0 + +<# +.SYNOPSIS +Gets the release tag for a GitHub repository release. +.DESCRIPTION +Queries the GitHub API to get the release tag for a specific release or the latest release of a repository. + +.EXAMPLE +Get-GithubReleaseTag -githubRepoUrl "https://github.com/Azure/accelerator-bootstrap-modules" -release "latest" + +.EXAMPLE +Get-GithubReleaseTag -githubRepoUrl "https://github.com/Azure/accelerator-bootstrap-modules" -release "v1.0.0" + +.NOTES +# Release notes 09/01/2026 - V0.1.0: +- Initial release - extracted from Get-GithubRelease.ps1. +#> + +function Get-GithubReleaseTag { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 1, HelpMessage = "Please provide the full URL of the GitHub repository you wish to check for the release.")] + [string] + $githubRepoUrl, + + [Parameter(Mandatory = $false, Position = 2, HelpMessage = "The release to check. Specify 'latest' to get the latest release tag. Defaults to 'latest'.")] + [string] + $release = "latest" + ) + + # Split Repo URL into parts + $repoOrgPlusRepo = $githubRepoUrl.Split("/")[-2..-1] -join "/" + + Write-Verbose "=====> Checking for release on GitHub Repo: $repoOrgPlusRepo" + + # Build the API URL + $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/$release" + if ($release -ne "latest") { + $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/tags/$release" + } + + # Query the GitHub API + $releaseData = Invoke-RestMethod $repoReleaseUrl -SkipHttpErrorCheck -StatusCodeVariable "statusCode" + + Write-Verbose "Status code: $statusCode" + + if ($statusCode -eq 404) { + Write-Error "The release $release does not exist in the GitHub repository $githubRepoUrl - $repoReleaseUrl" + throw "The release $release does not exist in the GitHub repository $githubRepoUrl - $repoReleaseUrl" + } + + # Handle transient errors like throttling + if ($statusCode -ge 400 -and $statusCode -le 599) { + Write-InformationColored "Retrying as got the Status Code $statusCode, which may be a transient error." -ForegroundColor Yellow -InformationAction Continue + $releaseData = Invoke-RestMethod $repoReleaseUrl -RetryIntervalSec 3 -MaximumRetryCount 100 + } + + if ($statusCode -ne 200) { + throw "Unable to query repository version, please check your internet connection and try again..." + } + + return @{ + ReleaseTag = $releaseData.tag_name + ReleaseData = $releaseData + } +} diff --git a/src/ALZ/Private/Tools/Test-Tooling.ps1 b/src/ALZ/Private/Tools/Test-Tooling.ps1 index 6ab7f27a..5256d57f 100644 --- a/src/ALZ/Private/Tools/Test-Tooling.ps1 +++ b/src/ALZ/Private/Tools/Test-Tooling.ps1 @@ -8,7 +8,9 @@ function Test-Tooling { [Parameter(Mandatory = $false)] [switch]$skipYamlModuleInstall, [Parameter(Mandatory = $false)] - [switch]$skipAzureLoginCheck + [switch]$skipAzureLoginCheck, + [Parameter(Mandatory = $false)] + [switch]$destroy ) $checkResults = @() @@ -202,29 +204,50 @@ function Test-Tooling { } if($null -eq $alzModuleCurrentVersion) { - $checkResults += @{ - message = "ALZ module is not correctly installed. Please install the latest version using 'Install-PSResource -Name ALZ'." - result = "Failure" + if($destroy.IsPresent) { + $checkResults += @{ + message = "ALZ module is not correctly installed. Please install the latest version using 'Install-PSResource -Name ALZ'. Continuing as -destroy flag is set." + result = "Warning" + } + } else { + $checkResults += @{ + message = "ALZ module is not correctly installed. Please install the latest version using 'Install-PSResource -Name ALZ'." + result = "Failure" + } + $hasFailure = $true } - $hasFailure = $true } $alzModuleLatestVersion = Find-PSResource -Name ALZ if ($null -ne $alzModuleCurrentVersion) { if ($alzModuleCurrentVersion.Version -lt $alzModuleLatestVersion.Version) { - $checkResults += @{ - message = "ALZ module is not the latest version. Your version: $($alzModuleCurrentVersion.Version), Latest version: $($alzModuleLatestVersion.Version). Please update to the latest version using 'Update-PSResource -Name ALZ'." - result = "Failure" + if($destroy.IsPresent) { + $checkResults += @{ + message = "ALZ module is not the latest version. Your version: $($alzModuleCurrentVersion.Version), Latest version: $($alzModuleLatestVersion.Version). Please update to the latest version using 'Update-PSResource -Name ALZ'. Continuing as -destroy flag is set." + result = "Warning" + } + } else { + $checkResults += @{ + message = "ALZ module is not the latest version. Your version: $($alzModuleCurrentVersion.Version), Latest version: $($alzModuleLatestVersion.Version). Please update to the latest version using 'Update-PSResource -Name ALZ'." + result = "Failure" + } + $hasFailure = $true } - $hasFailure = $true } else { if($importedModule.Version -lt $alzModuleLatestVersion.Version) { Write-Verbose "Imported ALZ module version ($($importedModule.Version)) is older than the latest installed version ($($alzModuleLatestVersion.Version)), re-importing module" - $checkResults += @{ - message = "ALZ module has the latest version installed, but not imported. Imported version: ($($importedModule.Version)). Please re-import the module using 'Remove-Module -Name ALZ; Import-Module -Name ALZ -Global' to use the latest version." - result = "Failure" + if($destroy.IsPresent) { + $checkResults += @{ + message = "ALZ module has the latest version installed, but not imported. Imported version: ($($importedModule.Version)). Please re-import the module using 'Remove-Module -Name ALZ; Import-Module -Name ALZ -Global' to use the latest version. Continuing as -destroy flag is set." + result = "Warning" + } + } else { + $checkResults += @{ + message = "ALZ module has the latest version installed, but not imported. Imported version: ($($importedModule.Version)). Please re-import the module using 'Remove-Module -Name ALZ; Import-Module -Name ALZ -Global' to use the latest version." + result = "Failure" + } + $hasFailure = $true } - $hasFailure = $true } else { $checkResults += @{ message = "ALZ module is the latest version ($($alzModuleCurrentVersion.Version))." diff --git a/src/ALZ/Public/Deploy-Accelerator.ps1 b/src/ALZ/Public/Deploy-Accelerator.ps1 index 889623f9..2be3bcf9 100644 --- a/src/ALZ/Public/Deploy-Accelerator.ps1 +++ b/src/ALZ/Public/Deploy-Accelerator.ps1 @@ -37,6 +37,13 @@ function Deploy-Accelerator { [Alias("targetDirectory")] [string] $output_folder_path = ".", + [Parameter( + Mandatory = $false, + HelpMessage = "[OPTIONAL] The name of the output folder within the target directory. Defaults to 'output'. Environment variable: ALZ_output_folder_name. Config file input: output_folder_name." + )] + [Alias("ofn")] + [string] $output_folder_name = "output", + [Parameter( Mandatory = $false, HelpMessage = "[OPTIONAL] The version tag of the bootstrap module release to download. Defaults to latest. Environment variable: ALZ_bootstrap_module_version. Config file input: bootstrap_module_version." @@ -63,14 +70,14 @@ function Deploy-Accelerator { [Parameter( Mandatory = $false, - HelpMessage = "[OPTIONAL] Determines that this run is to destroup the bootstrap. This is used to cleanup experiments. Environment variable: ALZ_destroy. Config file input: destroy." + HelpMessage = "[OPTIONAL] Determines that this run is to destroy the bootstrap. This is used to cleanup experiments. Environment variable: ALZ_destroy. Config file input: destroy." )] [Alias("d")] [switch] $destroy, [Parameter( Mandatory = $false, - HelpMessage = "[OPTIONAL] The bootstrap modules reposiotry url. This can be overridden for custom modules. Environment variable: ALZ_bootstrap_module_url. Config file input: bootstrap_module_url." + HelpMessage = "[OPTIONAL] The bootstrap modules repository url. This can be overridden for custom modules. Environment variable: ALZ_bootstrap_module_url. Config file input: bootstrap_module_url." )] [Alias("bu")] [Alias("bootstrapModuleUrl")] @@ -94,7 +101,7 @@ function Deploy-Accelerator { [Parameter( Mandatory = $false, - HelpMessage = "[OPTIONAL] The folder that containes the bootstrap modules in the bootstrap repo. This can be overridden for custom modules. Environment variable: ALZ_bootstrap_source_folder. Config file input: bootstrap_source_folder." + HelpMessage = "[OPTIONAL] The folder that contains the bootstrap modules in the bootstrap repo. This can be overridden for custom modules. Environment variable: ALZ_bootstrap_source_folder. Config file input: bootstrap_source_folder." )] [Alias("bf")] [Alias("bootstrapSourceFolder")] @@ -126,7 +133,7 @@ function Deploy-Accelerator { [Parameter( Mandatory = $false, - HelpMessage = "[OPTIONAL] Whether to overwrite bootstrap and starter modules if they already exist. Warning, this may result in unexpected behaviour and should only be used for local development purposes. Environment variable: ALZ_replace_files. Config file input: replace_files." + HelpMessage = "[OPTIONAL] Whether to overwrite bootstrap and starter modules if they already exist. Warning, this may result in unexpected behavior and should only be used for local development purposes. Environment variable: ALZ_replace_files. Config file input: replace_files." )] [Alias("rf")] [Alias("replaceFiles")] @@ -173,7 +180,21 @@ function Deploy-Accelerator { Mandatory = $false, HelpMessage = "[OPTIONAL] Determines whether Clean the bootstrap folder of Terraform meta files. Only use for development purposes." )] - [switch] $cleanBootstrapFolder + [switch] $cleanBootstrapFolder, + + [Parameter( + Mandatory = $false, + HelpMessage = "[OPTIONAL] Determines whether to upgrade to the latest version of modules when version is set to 'latest'. Without this flag, existing versions are used. Environment variable: ALZ_upgrade. Config file input: upgrade." + )] + [Alias("u")] + [switch] $upgrade, + + [Parameter( + Mandatory = $false, + HelpMessage = "[OPTIONAL] Clears the cached Azure context (management groups, subscriptions, regions) and fetches fresh data from Azure." + )] + [Alias("cc")] + [switch] $clear_cache ) $ProgressPreference = "SilentlyContinue" @@ -216,7 +237,7 @@ function Deploy-Accelerator { Write-InformationColored "WARNING: Skipping the software requirements check..." -ForegroundColor Yellow -InformationAction Continue } else { Write-InformationColored "Checking the software requirements for the Accelerator..." -ForegroundColor Green -InformationAction Continue - $toolingResult = Test-Tooling -skipAlzModuleVersionCheck:$skip_alz_module_version_requirements_check.IsPresent -checkYamlModule:$checkYamlModule -skipYamlModuleInstall:$skip_yaml_module_install.IsPresent -skipAzureLoginCheck:$needsFolderStructureSetup + $toolingResult = Test-Tooling -skipAlzModuleVersionCheck:$skip_alz_module_version_requirements_check.IsPresent -checkYamlModule:$checkYamlModule -skipYamlModuleInstall:$skip_yaml_module_install.IsPresent -skipAzureLoginCheck:$needsFolderStructureSetup -destroy:$destroy.IsPresent } # If az cli is installed but not logged in, prompt for tenant ID and login with device code @@ -252,7 +273,7 @@ function Deploy-Accelerator { # If no inputs provided, prompt user for folder structure setup if ($needsFolderStructureSetup) { - $setupResult = Request-AcceleratorConfigurationInput -Destroy:$destroy.IsPresent + $setupResult = Request-AcceleratorConfigurationInput -Destroy:$destroy.IsPresent -ClearCache:$clear_cache.IsPresent -OutputFolderName $output_folder_name if (-not $setupResult.Continue) { return @@ -361,7 +382,9 @@ function Deploy-Accelerator { -releaseArtifactName $inputConfig.bootstrap_module_release_artifact_name.Value ` -moduleOverrideFolderPath $inputConfig.bootstrap_module_override_folder_path.Value ` -skipInternetChecks $inputConfig.skip_internet_checks.Value ` - -replaceFile:$inputConfig.replace_files.Value + -replaceFile:$inputConfig.replace_files.Value ` + -upgrade:$inputConfig.upgrade.Value ` + -autoApprove:$inputConfig.auto_approve.Value $bootstrapReleaseTag = $versionAndPath.releaseTag $bootstrapPath = $versionAndPath.path @@ -421,7 +444,9 @@ function Deploy-Accelerator { -releaseArtifactName $starterReleaseArtifactName ` -moduleOverrideFolderPath $inputConfig.starter_module_override_folder_path.Value ` -skipInternetChecks $inputConfig.skip_internet_checks.Value ` - -replaceFile:$inputConfig.replace_files.Value + -replaceFile:$inputConfig.replace_files.Value ` + -upgrade:$inputConfig.upgrade.Value ` + -autoApprove:$inputConfig.auto_approve.Value $starterReleaseTag = $versionAndPath.releaseTag $starterPath = $versionAndPath.path diff --git a/src/ALZ/Public/New-AcceleratorFolderStructure.ps1 b/src/ALZ/Public/New-AcceleratorFolderStructure.ps1 index 39596020..780764ab 100644 --- a/src/ALZ/Public/New-AcceleratorFolderStructure.ps1 +++ b/src/ALZ/Public/New-AcceleratorFolderStructure.ps1 @@ -21,6 +21,11 @@ function New-AcceleratorFolderStructure { HelpMessage = "[REQUIRED] The target folder to create the accelerator bootstrap and platform landing zone configuration files in" )] [string] $targetFolderPath = "~/accelerator", + [Parameter( + Mandatory = $false, + HelpMessage = "[OPTIONAL] The name of the output folder. Defaults to 'output'" + )] + [string] $outputFolderName = "output", [Parameter( Mandatory = $false, HelpMessage = "[OPTIONAL] Force recreate of the target folder if it already exists" @@ -53,7 +58,7 @@ function New-AcceleratorFolderStructure { $targetFolderPath = (Resolve-Path -Path $targetFolderPath).Path # Create target folder structure - $outputFolder = Join-Path $targetFolderPath "output" + $outputFolder = Join-Path $targetFolderPath $outputFolderName Write-Host "Creating output folder at $outputFolder" New-Item -ItemType "directory" $outputFolder -Force | Write-Verbose | Out-Null diff --git a/src/Tests/Unit/Public/Deploy-Accelerator.Tests.ps1 b/src/Tests/Unit/Public/Deploy-Accelerator.Tests.ps1 index 1c106ce7..e94f6fe1 100644 --- a/src/Tests/Unit/Public/Deploy-Accelerator.Tests.ps1 +++ b/src/Tests/Unit/Public/Deploy-Accelerator.Tests.ps1 @@ -124,10 +124,6 @@ InModuleScope 'ALZ' { Mock -CommandName Invoke-Terraform -MockWith { } - Mock -CommandName Invoke-Upgrade -MockWith { } - - Mock -CommandName Invoke-FullUpgrade -MockWith { } - Mock -CommandName Get-TerraformTool -MockWith {} Mock -CommandName New-FolderStructure -MockWith {}