source/Private/Common.ps1
|
function Invoke-AzCommandWithNotFoundHandling { <# .SYNOPSIS Executes an Az command and returns a default value on "not found" errors instead of throwing. .DESCRIPTION Wraps a scriptblock in try/catch. If the Az cmdlet throws with FullyQualifiedErrorId containing 'Request_ResourceNotFound', returns DefaultValue. All other errors propagate. .PARAMETER ScriptBlock The Az command to execute. Must use -ErrorAction Stop internally. .PARAMETER DefaultValue Value to return when the resource is not found. Defaults to $null. .OUTPUTS The output of the scriptblock, or DefaultValue on not-found errors. #> [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, $DefaultValue = $null ) try { & $ScriptBlock } catch { if ($_.FullyQualifiedErrorId -match 'Request_ResourceNotFound') { return $DefaultValue } throw } } function Get-RequiredModuleVersionRange { <# .SYNOPSIS Derives the accepted version range for a pinned module version. .DESCRIPTION The pinned version is the minimum; any patch release with the same major.minor is accepted. .PARAMETER PinnedVersion The tested module version, e.g. 5.5.2. .OUTPUTS [PSCustomObject] with MinimumVersion, MaximumVersion and RangeText. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [version]$PinnedVersion ) [PSCustomObject]@{ MinimumVersion = $PinnedVersion MaximumVersion = [version]"$($PinnedVersion.Major).$($PinnedVersion.Minor).99999" RangeText = "$($PinnedVersion.Major).$($PinnedVersion.Minor).x ($PinnedVersion or newer)" } } function Get-InstalledModuleInRange { <# .SYNOPSIS Returns the highest installed version of a module within the accepted range, or nothing. #> [CmdletBinding()] [OutputType([PSModuleInfo])] param( [Parameter(Mandatory = $true)] [string]$ModuleName, [Parameter(Mandatory = $true)] [PSCustomObject]$Range ) Get-Module -FullyQualifiedName @{ ModuleName = $ModuleName ModuleVersion = $Range.MinimumVersion MaximumVersion = $Range.MaximumVersion } -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 } function Get-ConflictingModuleList { [CmdletBinding()] [OutputType([PSCustomObject[]])] param ( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$RequiredModules ) $conflicts = @() foreach ($moduleName in $RequiredModules.Keys) { $range = Get-RequiredModuleVersionRange -PinnedVersion $RequiredModules[$moduleName] $loadedModules = Get-Module -Name $moduleName -ErrorAction SilentlyContinue if ($loadedModules) { $conflictingVersions = $loadedModules | Where-Object { $_.Version -lt $range.MinimumVersion -or $_.Version -gt $range.MaximumVersion } if ($conflictingVersions) { $conflicts += [PSCustomObject]@{ ModuleName = $moduleName LoadedVersions = $conflictingVersions.Version RequiredRange = $range.RangeText } } } } return $conflicts } function Show-ModuleConflictInstructions { <# .SYNOPSIS Shows user-friendly instructions to resolve module version conflicts. .PARAMETER ConflictingModules Array of conflicting module information from Get-ConflictingModuleList. #> [CmdletBinding()] [OutputType([void])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Show-ModuleConflictInstructions')] param ( [Parameter(Mandatory = $true)] [PSCustomObject[]]$ConflictingModules ) Write-Host "Module version conflicts detected - initialization cannot proceed in current session" -ForegroundColor Red Write-Host "" Write-Host "PowerShell cannot unload assemblies from the current session." -ForegroundColor Yellow Write-Host "To resolve this issue, please follow these steps:" -ForegroundColor Yellow Write-Host "" Write-Host "1. Close this PowerShell session" -ForegroundColor Cyan Write-Host "2. Open a new PowerShell session" -ForegroundColor Cyan Write-Host "3. Re-run your command" -ForegroundColor Cyan Write-Host "" Write-Host "Module version conflicts:" -ForegroundColor Yellow foreach ($conflict in $ConflictingModules) { $loadedVersions = $conflict.LoadedVersions -join ', ' Write-Host " - $($conflict.ModuleName): Loaded=$loadedVersions, Required=$($conflict.RequiredRange)" -ForegroundColor Yellow } Write-Host "" } function Initialize-RequiredModule { <# .SYNOPSIS Initializes required PowerShell modules within pinned major.minor version ranges. .DESCRIPTION Checks for module version conflicts, installs missing modules, and imports required modules. Any patch release with the same major.minor as the pinned version is accepted; the highest installed version in range is used. If version conflicts are detected, provides user instructions and returns false. .PARAMETER RequiredModules Hashtable of module names to minimum versions. Defaults to RequiredAzModules. .OUTPUTS [bool] True if initialization succeeded, False if conflicts prevent initialization. #> [CmdletBinding()] [OutputType([bool])] param( [System.Collections.IDictionary]$RequiredModules = $RequiredAzModules ) Write-Verbose "Initializing required PowerShell modules..." # Check for module version conflicts (wrong version already loaded = unrecoverable in this session) $conflictingModules = @(Get-ConflictingModuleList -RequiredModules $RequiredModules) if ($conflictingModules.Count -gt 0) { Show-ModuleConflictInstructions -ConflictingModules $conflictingModules return $false } # Determine what needs installation vs import $modulesToInstall = [ordered]@{} $modulesToImport = [ordered]@{} foreach ($moduleName in $RequiredModules.Keys) { $range = Get-RequiredModuleVersionRange -PinnedVersion $RequiredModules[$moduleName] $loadedModule = Get-Module -Name $moduleName | Where-Object { $_.Version -ge $range.MinimumVersion -and $_.Version -le $range.MaximumVersion } if ($loadedModule) { Write-Verbose "Module $moduleName version $($loadedModule.Version) is already loaded" continue } $installedModule = Get-InstalledModuleInRange -ModuleName $moduleName -Range $range if (-not $installedModule) { $modulesToInstall[$moduleName] = $range } $modulesToImport[$moduleName] = $range } # Pass 1: Install all missing modules BEFORE importing any (avoids file-lock warnings) foreach ($moduleName in $modulesToInstall.Keys) { $range = $modulesToInstall[$moduleName] Write-Verbose "Installing module $moduleName..." try { Install-Module -Name $moduleName -MinimumVersion $range.MinimumVersion -MaximumVersion $range.MaximumVersion -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop $installedModule = Get-InstalledModuleInRange -ModuleName $moduleName -Range $range Write-Information "Successfully installed $moduleName version $($installedModule.Version)" } catch { Write-Error "Failed to install required module $moduleName. Error: $($_.Exception.Message)" return $false } } # Pass 2: Import all modules (no -Force; dependencies are already loaded in order) foreach ($moduleName in $modulesToImport.Keys) { $range = $modulesToImport[$moduleName] Write-Verbose "Importing module $moduleName..." try { $importedModule = Import-Module -Name $moduleName -MinimumVersion $range.MinimumVersion -MaximumVersion $range.MaximumVersion -PassThru -ErrorAction Stop -InformationAction SilentlyContinue -Verbose:$false Write-Verbose "Successfully imported $moduleName version $($importedModule.Version)" } catch { Write-Error "Failed to import required module $moduleName. Error: $($_.Exception.Message)" return $false } } Write-Verbose "All required PowerShell modules are initialized and ready to use" return $true } |