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-ConflictingModuleList { [CmdletBinding()] [OutputType([PSCustomObject[]])] param ( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$RequiredModules ) $conflicts = @() foreach ($moduleName in $RequiredModules.Keys) { $requiredVersion = $RequiredModules[$moduleName] $loadedModules = Get-Module -Name $moduleName -ErrorAction SilentlyContinue if ($loadedModules) { $conflictingVersions = $loadedModules | Where-Object { $_.Version -ne $requiredVersion } if ($conflictingVersions) { $conflicts += [PSCustomObject]@{ ModuleName = $moduleName LoadedVersions = $conflictingVersions.Version RequiredVersion = $requiredVersion } } } } 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.RequiredVersion)" -ForegroundColor Yellow } Write-Host "" } function Initialize-RequiredModule { <# .SYNOPSIS Initializes required PowerShell modules with version pinning. .DESCRIPTION Checks for module version conflicts, installs missing modules, and imports required modules. If version conflicts are detected, provides user instructions and returns false. .PARAMETER RequiredModules Hashtable of module names to required 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) { $requiredVersion = $RequiredModules[$moduleName] $loadedModule = Get-Module -Name $moduleName | Where-Object { $_.Version -eq $requiredVersion } if ($loadedModule) { Write-Verbose "Module $moduleName version $requiredVersion is already loaded" continue } $installedModule = Get-Module -FullyQualifiedName @{ ModuleName = $moduleName RequiredVersion = $requiredVersion } -ListAvailable -ErrorAction SilentlyContinue if (-not $installedModule) { $modulesToInstall[$moduleName] = $requiredVersion } $modulesToImport[$moduleName] = $requiredVersion } # Pass 1: Install all missing modules BEFORE importing any (avoids file-lock warnings) foreach ($moduleName in $modulesToInstall.Keys) { $requiredVersion = $modulesToInstall[$moduleName] Write-Verbose "Installing module $moduleName version $requiredVersion..." try { Install-Module -Name $moduleName -RequiredVersion $requiredVersion -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop Write-Information "Successfully installed $moduleName version $requiredVersion" } catch { Write-Error "Failed to install required module $moduleName version $requiredVersion. Error: $($_.Exception.Message)" return $false } } # Pass 2: Import all modules (no -Force; dependencies are already loaded in order) foreach ($moduleName in $modulesToImport.Keys) { $requiredVersion = $modulesToImport[$moduleName] Write-Verbose "Importing module $moduleName version $requiredVersion..." try { Import-Module -Name $moduleName -RequiredVersion $requiredVersion -ErrorAction Stop -InformationAction SilentlyContinue -Verbose:$false Write-Verbose "Successfully imported $moduleName version $requiredVersion" } catch { Write-Error "Failed to import required module $moduleName version $requiredVersion. Error: $($_.Exception.Message)" return $false } } Write-Verbose "All required PowerShell modules are initialized and ready to use" return $true } |