Common.PowerShell.psm1

<#
.SYNOPSIS
    Shared PowerShell utilities for infrastructure repos.
 
.DESCRIPTION
    Provides cross-cutting utilities that are not specific to any single
    infrastructure concern (secrets, provisioning, users, etc.).
 
    Current functions:
    - Assert-RequiredProperties: validates object fields are present and
      non-empty; throws a descriptive error if not.
    - ConvertTo-Array: ensures a value is always an array regardless of
      whether PowerShell unrolled a single-item collection.
    - Invoke-ModuleInstall: installs a PSGallery module if absent or below a
      minimum version, then imports it.
    - Limit-RetainedItem: prunes items in a directory by age and/or count
      (no Windows logrotate equivalent; callers use this for at-write-time
      retention sweeps on log / diag folders).
    - Invoke-WithExitCodeRetry: exit-code counterpart to Invoke-WithRetry
      for native commands (netsh, git, docker, wsl, ...) that fail via
      $LASTEXITCODE rather than a thrown exception. Reuses the same backoff
      strategies.
    - Invoke-WithRetry: generic retry loop that consumes hashtable-shaped
      retry (ShouldRetry) and backoff (GetDelay) strategies. The classifier
      and pacing are caller-supplied; the loop just orchestrates.
    - New-TransientNetworkRetryStrategy: builds a retry-strategy hashtable
      that matches transient network failures, consumed by Invoke-WithRetry.
    - New-FileLockRetryStrategy: builds a retry-strategy hashtable that
      matches System.IO.IOException (file-lock contention, e.g. Hyper-V
      VMMS handle release after Remove-VM).
    - New-ExponentialBackoffStrategy / New-LinearBackoffStrategy /
      New-ConstantBackoffStrategy / New-CustomBackoffStrategy: build
      backoff-strategy hashtables (@{ Name; GetDelay }) consumed by
      Invoke-WithRetry. Custom is the escape hatch for cases the three
      built-ins (exponential / linear / constant) do not cover (HTTP 429
      Retry-After, jittered exponential, ...).
 
    Hyper-V VM helpers (SSH execution, host file server) were moved to the
    Infrastructure.HyperV module to keep this module focused on genuinely
    generic utilities. GitHub API helpers live in Infrastructure.GitHub.
    WSL helpers (Assert-Wsl2Ready, Assert-WslHasBash, Invoke-WslShell)
    moved to Infrastructure.Wsl in 7.0.0; consumers using WSL must now
    Install/Import that module separately.
 
    Each function lives in its own file under Public\ and is dot-sourced
    below so diffs stay focused on a single function per commit.
#>


Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# Module-internal helpers (not exported). Loaded first so functions
# dot-sourced from Public\ can rely on them at parse / call time.
. "$PSScriptRoot\Private\Retry\Assert-RetryStrategyShape.ps1"

# Top-level utilities (no domain grouping yet).
. "$PSScriptRoot\Public\Assert-RequiredProperties.ps1"
. "$PSScriptRoot\Public\ConvertTo-Array.ps1"
. "$PSScriptRoot\Public\Invoke-ModuleInstall.ps1"
. "$PSScriptRoot\Public\Limit-RetainedItem.ps1"

# Retry primitives - grouped because they form a self-contained family
# (loop + predicate strategies + backoff strategies). Subdivided by
# strategy category: TransientErrorStrategies\ for ShouldRetry classifiers
# and BackoffStrategies\ for GetDelay providers. Strategy factories are
# dot-sourced before the loop helpers so the loop's relocated classifier
# helper resolves at module-load time.
. "$PSScriptRoot\Public\Retry\TransientErrorStrategies\New-FileLockRetryStrategy.ps1"
. "$PSScriptRoot\Public\Retry\TransientErrorStrategies\New-TransientPowerShellModuleInstallRetryStrategy.ps1"
. "$PSScriptRoot\Public\Retry\TransientErrorStrategies\New-TransientNetworkRetryStrategy.ps1"
. "$PSScriptRoot\Public\Retry\BackoffStrategies\New-ConstantBackoffStrategy.ps1"
. "$PSScriptRoot\Public\Retry\BackoffStrategies\New-CustomBackoffStrategy.ps1"
. "$PSScriptRoot\Public\Retry\BackoffStrategies\New-ExponentialBackoffStrategy.ps1"
. "$PSScriptRoot\Public\Retry\BackoffStrategies\New-LinearBackoffStrategy.ps1"
. "$PSScriptRoot\Public\Retry\Invoke-WithExitCodeRetry.ps1"
. "$PSScriptRoot\Public\Retry\Invoke-WithRetry.ps1"

# Export-ModuleMember controls what is actually callable after Import-Module.
# It takes precedence over FunctionsToExport in the psd1 at runtime, so both
# must be kept in sync. FunctionsToExport serves a separate purpose: it is
# read by Get-Module -ListAvailable, Find-Module, and PSGallery for fast
# discovery without loading the module. The shared Module.Tests.ps1 in the
# run-unit-tests action enforces that every Public\*.ps1 file appears in both.
Export-ModuleMember -Function `
    Assert-RequiredProperties, `
    ConvertTo-Array, `
    Invoke-ModuleInstall, `
    Limit-RetainedItem, `
    `
    Invoke-WithExitCodeRetry, `
    Invoke-WithRetry, `
    New-FileLockRetryStrategy, `
    New-TransientPowerShellModuleInstallRetryStrategy, `
    New-TransientNetworkRetryStrategy, `
    `
    New-ConstantBackoffStrategy, `
    New-CustomBackoffStrategy, `
    New-ExponentialBackoffStrategy, `
    New-LinearBackoffStrategy