UTCM.Tools.psm1
|
#requires -Version 7.0 <# =============================================================================== UTCM.Tools.psm1 (PowerShell 7+) Root module for the UTCM.Tools PowerShell module. Responsibilities: • Enable strict mode for safer execution • Define module-wide constants shared by functions • JSON-backed resource presets + allow-list validation • Reliably dot-source all Private\*.ps1 helpers (required) • Reliably dot-source all Public\*.ps1 functions (required) • Export ONLY the intended public functions • Validate public functions using a safe Retry Guard (no autoload) • Register dynamic arg completer for New-UTCMSnapshot -Preset Notes: • Keep Export-ModuleMember's function list in sync with Public\ files. • The loader throws clear errors if folders/files are missing. • Retry guard uses Function: drive to avoid command discovery/autoload. • For import troubleshooting, set: $VerbosePreference='Continue' then Import-Module -Force Preset JSON files: <ModuleRoot>\Presets\resource-presets.json <ModuleRoot>\Presets\supported-resource-types.json Optional overrides: %ProgramData%\UTCM.Tools\resource-presets.json %AppData%\UTCM.Tools\resource-presets.json =============================================================================== #> Set-StrictMode -Version Latest # --------------------------- # Module-wide constants (UTCM Graph preview/beta) # --------------------------- # Guard for one-time argument completer registration $script:PresetCompleterRegistered = $false # Base for all UTCM endpoints (beta) $script:GraphBase = '/beta/admin/configurationManagement' # Snapshot jobs collection (used to GET/POLL jobs and list them) $script:SnapshotJobsUri = "$script:GraphBase/configurationSnapshotJobs" # Action endpoint to START a snapshot job (required for UTCM snapshot creation) $script:CreateSnapshotActionUri = "$script:GraphBase/configurationSnapshots/createSnapshot" # Configuration monitors collection (create/manage periodic drift monitors) $script:ConfigurationMonitorsUri = "$script:GraphBase/configurationMonitors" # Configuration drifts collection (server-side property-level drift results) $script:ConfigurationDriftsUri = "$script:GraphBase/configurationDrifts" # Configuration monitoring results collection (monitor run history) $script:MonitoringResultsUri = "$script:GraphBase/configurationMonitoringResults" # --------------------------- # JSON Preset & Allow-list support # --------------------------- # NOTE: All preset/allow-list functions (Get-UTCMPresetSearchPaths, Get-UTCMAllowListPath, # Get-UTCMResourcePresets, Resolve-UTCMResources, Test-UTCMResourceTypes) and the # $script:BuiltInResourcePresets variable are defined in Private\Presets.ps1. # They are loaded when that file is dot-sourced below. # --------------------------- # Helper: return .ps1 files from a folder (relative to $PSScriptRoot) # NOTE: This function ONLY RETURNS FILES. It does NOT dot-source them. # We dot-source at MODULE SCRIPT SCOPE (below), to keep definitions. # --------------------------- function _Get-ScriptsFromFolder { [CmdletBinding()] param( [Parameter(Mandatory)][string]$FolderName, [switch]$Required ) $dir = Join-Path -Path $PSScriptRoot -ChildPath $FolderName Write-Verbose "Loading scripts from: $dir (Required=$($Required.IsPresent))" if (-not (Test-Path -LiteralPath $dir)) { if ($Required) { throw "Required folder not found: $dir" } else { Write-Verbose "Folder not found (optional): $dir — skipping." return @() } } $files = Get-ChildItem -LiteralPath $dir -Filter *.ps1 -File -ErrorAction Stop | Sort-Object Name if (-not $files -and $Required) { throw "No scripts (*.ps1) found in required folder: $dir" } return $files } # --------------------------- # Collect scripts # --------------------------- $privateFiles = _Get-ScriptsFromFolder -FolderName 'Private' -Required $publicFiles = _Get-ScriptsFromFolder -FolderName 'Public' -Required # --------------------------- # DOT-SOURCE FILES AT MODULE SCRIPT SCOPE (critical for persistence) # --------------------------- # Private helpers (may or may not declare functions) foreach ($file in $privateFiles) { Write-Verbose "Dot-sourcing: $($file.FullName)" $before = (Get-ChildItem Function:\).Name . $file.FullName $after = (Get-ChildItem Function:\).Name $added = Compare-Object $before $after -PassThru | Where-Object { $_ -like '*UTCM*' } if ($added) { Write-Verbose "Functions added by $($file.Name): $($added -join ', ')" } else { Write-Verbose "No UTCM functions detected from $($file.Name)" } } # Public cmdlets (should declare functions) foreach ($file in $publicFiles) { Write-Verbose "Dot-sourcing: $($file.FullName)" $before = (Get-ChildItem Function:\).Name . $file.FullName $after = (Get-ChildItem Function:\).Name $added = Compare-Object $before $after -PassThru | Where-Object { $_ -like '*UTCM*' } if ($added) { Write-Verbose "Functions added by $($file.Name): $($added -join ', ')" } else { Write-Verbose "No UTCM functions detected from $($file.Name)" } } # --------------------------- # Export ONLY intended public functions # --------------------------- $publicFunctions = @( 'Enable-UTCM', 'Grant-UTCMWorkloadAccess', 'Initialize-UTCM', 'Test-UTCMSetup', # (fixed missing comma) 'Get-UTCMAvailableSnapshot', 'New-UTCMSnapshot', 'Get-UTCMSnapshot', 'Remove-UTCMSnapshot', 'Compare-UTCMConfiguration', 'Export-UTCMSnapshot', 'New-UTCMDriftReport', 'Get-UTCMTenantDriftReport', 'Get-UTCMPreset', # public helper to inspect presets 'Get-UTCMDrift', 'New-UTCMMonitor', 'Get-UTCMMonitor', 'Get-UTCMMonitoringResult' ) # --------------------------- # Retry Guard (safe): validate functions after load with brief retries # Uses Function: drive to avoid command discovery or module autoload during import. # Tune via env vars; defaults are 3 tries and 80 ms delay # --------------------------- $maxTries = [Environment]::GetEnvironmentVariable('UTCM_RETRY_TRIES') ?? '3' $delayMs = [Environment]::GetEnvironmentVariable('UTCM_RETRY_DELAY_MS') ?? '80' try { $maxTries = [int]$maxTries } catch { $maxTries = 3 } try { $delayMs = [int]$delayMs } catch { $delayMs = 80 } if ($maxTries -lt 1) { $maxTries = 1 } if ($delayMs -lt 1) { $delayMs = 1 } Write-Verbose "Validation retries: maxTries=$maxTries, delayMs=$delayMs" $export = @() $missing = @() foreach ($fn in $publicFunctions) { $ok = $false for ($i = 1; $i -le $maxTries; $i++) { if (Test-Path "Function:\$fn") { $ok = $true break } if ($i -lt $maxTries) { Start-Sleep -Milliseconds $delayMs } } if ($ok) { $export += $fn } else { $missing += $fn } } if ($missing.Count -gt 0) { $msg = @() $msg += "One or more public functions were not found after loading Public\*.ps1 and $maxTries validation attempts:" $msg += " - " + ($missing -join "`n - ") $msg += "Check for file/function name mismatches, scope issues (functions wrapped in invoked blocks)," $msg += "or missing files in the Public folder." throw ($msg -join "`n") } # Final export Write-Verbose ("Export list: " + ($export -join ', ')) Export-ModuleMember -Function $export # --------------------------- # Dynamic argument completer for: New-UTCMSnapshot -Preset <TAB> # Uses Get-UTCMPreset so it reflects JSON instantly at import time. # --------------------------- if (-not $script:PresetCompleterRegistered) { try { Register-ArgumentCompleter -CommandName 'New-UTCMSnapshot' -ParameterName 'Preset' -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) try { $names = Get-UTCMPreset $names | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_, $_, 'ParameterValue', "Preset: $_" ) } } catch { @() # never block tab completion } } | Out-Null $script:PresetCompleterRegistered = $true Write-Verbose "Registered argument completer for New-UTCMSnapshot -Preset." } catch { Write-Verbose "Failed to register argument completer: $($_.Exception.Message)" } } |