Private/New-DscResourceAdapterScript.ps1
|
function New-DscResourceAdapterScript { <# .SYNOPSIS Generates the content of a resource.ps1 adapter script that bridges DSC v3 JSON-based invocation to PowerShell class-based DSC resources. Supports multiple resource classes. Wraps resource logic in a PowerShell runspace to capture PS streams (Error, Warning, Information, Verbose, Debug) and convert them to DSC-compatible JSON trace output on stderr. .DESCRIPTION PS stream → DSC trace mapping: Write-Error → error Write-Warning → warn Write-Information → info Write-Verbose → debug (must use -Verbose explicitly) Write-Debug → trace (must use -Debug explicitly) $env:DSC_TRACE_LEVEL controls which streams are captured. Error stream records cause a non-zero exit code. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [PSCustomObject[]]$ResourceInfos, [Parameter(Mandatory)] [string]$ModuleFileName ) $isModule = $ModuleFileName -match '\.psm1$' $allMethods = foreach ($info in $ResourceInfos) { if ($info.Methods.Count -gt 0) { $info.Methods } else { @('Get','Set','Test') } } $validOps = @($allMethods | Sort-Object -Unique | ForEach-Object { $_.ToLower() }) $opsString = "'" + ($validOps -join "', '") + "'" $classNames = @($ResourceInfos | ForEach-Object { $_.ClassName }) $typesString = "'" + ($classNames -join "', '") + "'" $switchLines = foreach ($name in $classNames) { if ($isModule) { " '$name' { GetTypeInstanceFromModule -ModulePath `$modulePath -ClassName '$name' }" } else { " '$name' { [$name]::new() }" } } $switchBlock = ($switchLines -join "`n") + "`n" + " default { throw `"Unknown resource type: `$ResourceType. Valid: $($classNames -join ', ')`" }" # Build a map of visible (non-hidden) property names per resource type. # Get-DscResourceAstInfo already excludes hidden members, so # $info.Properties contains only the properties we want in output. $propertyMapLines = foreach ($info in $ResourceInfos) { $propNames = @($info.Properties | ForEach-Object { "'$($_.Name)'" }) -join ', ' " '$($info.ClassName)' = @($propNames)" } $propertyMapBlock = " `$visibleProperties = @{`n" + ($propertyMapLines -join "`n") + "`n }" if ($isModule) { $importSection = @" `$modulePath = Join-Path `$ScriptRoot '$ModuleFileName' `$null = Import-Module `$modulePath -Force function GetTypeInstanceFromModule { param( [Parameter(Mandatory)] [string]`$ModulePath, [Parameter(Mandatory)] [string]`$ClassName ) & (Import-Module `$ModulePath -PassThru) ([scriptblock]::Create("`[`$ClassName]::new()")) } "@ } else { $importSection = " . `"`$ScriptRoot/$ModuleFileName`"" } $opCases = [System.Collections.Generic.List[string]]::new() if ($validOps -contains 'get') { $opCases.Add(@' 'get' { $resource.Get() | Select-Object -Property $visibleProperties[$ResourceType] } '@) } if ($validOps -contains 'set') { $opCases.Add(@' 'set' { $resource.Set() } '@) } if ($validOps -contains 'test') { $opCases.Add(@' 'test' { $inDesiredState = $resource.Test() $state = $resource.Get() | Select-Object -Property $visibleProperties[$ResourceType] | ConvertTo-Json -Depth 10 | ConvertFrom-Json $state | Add-Member -NotePropertyName '_inDesiredState' -NotePropertyValue $inDesiredState -Force $state } '@) } if ($validOps -contains 'delete') { $opCases.Add(@' 'delete' { $resource.Delete() } '@) } if ($validOps -contains 'export') { $opCases.Add(@' 'export' { $resource.Export() } '@) } $opCasesBlock = $opCases -join "`n" $scriptTemplate = @' # Generated by DscSchemaBuilder — DSC v3 resource adapter script [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true)] [string]$InputJson, [Parameter()] [ValidateSet(%%OPS_STRING%%)] [string]$Operation, [Parameter(Mandatory)] [ValidateSet(%%TYPES_STRING%%)] [string]$ResourceType ) $traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() $hadErrors = [System.Collections.Concurrent.ConcurrentQueue[bool]]::new() function Write-DscTrace { param( [Parameter(Mandatory)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] [string]$Operation, [Parameter(Mandatory, ValueFromPipeline)] [string]$Message, [switch]$Now ) $trace = @{ $Operation.ToLower() = $Message } if ($Now) { $host.ui.WriteErrorLine(($trace | ConvertTo-Json -Compress)) } else { $traceQueue.Enqueue($trace) } } trap { Write-DscTrace -Operation Error ` -Message ($_ | Format-List -Force | Out-String) -Now } function Write-TraceQueue { $trace = $null while (-not $traceQueue.IsEmpty) { if ($traceQueue.TryDequeue([ref]$trace)) { $host.ui.WriteErrorLine(($trace | ConvertTo-Json -Compress)) } } } $ps = [PowerShell]::Create().AddScript({ [CmdletBinding()] param( [string]$InputJson, [string]$Operation, [string]$ResourceType, [string]$ScriptRoot ) trap { Write-Error ($_ | Format-List -Force | Out-String) } $DebugPreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue' $ErrorActionPreference = 'Continue' $InformationPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' %%IMPORT_SECTION%% $resource = switch ($ResourceType) { %%SWITCH_BLOCK%% } %%PROPERTY_MAP%% $inputObject = $InputJson | ConvertFrom-Json foreach ($prop in $inputObject.PSObject.Properties) { if ($resource.PSObject.Properties.Name -contains $prop.Name) { $resource.($prop.Name) = $prop.Value } } switch ($Operation) { %%OP_CASES_BLOCK%% } }).AddParameter('InputJson', $InputJson ).AddParameter('Operation', $Operation ).AddParameter('ResourceType', $ResourceType ).AddParameter('ScriptRoot', $PSScriptRoot) enum DscTraceLevel { Error; Warn; Info; Debug; Trace } $traceLevel = if ($env:DSC_TRACE_LEVEL) { try { [DscTraceLevel]$env:DSC_TRACE_LEVEL } catch { Write-DscTrace -Operation Warn -Now ` -Message "Invalid DSC_TRACE_LEVEL '$env:DSC_TRACE_LEVEL'. Defaulting to Warn." [DscTraceLevel]::Warn } } else { [DscTraceLevel]::Warn } $null = Register-ObjectEvent -InputObject $ps.Streams.Error ` -EventName DataAdding ` -MessageData @{ traceQueue = $traceQueue; hadErrors = $hadErrors } ` -Action { $q = $Event.MessageData.traceQueue $q.Enqueue(@{ error = [string]$EventArgs.ItemAdded }) $Event.MessageData.hadErrors.Enqueue($true) } if ($traceLevel -ge [DscTraceLevel]::Warn) { $null = Register-ObjectEvent -InputObject $ps.Streams.Warning ` -EventName DataAdding -MessageData $traceQueue -Action { $q = $Event.MessageData $q.Enqueue(@{ warn = $EventArgs.ItemAdded.Message }) } } if ($traceLevel -ge [DscTraceLevel]::Info) { $null = Register-ObjectEvent -InputObject $ps.Streams.Information ` -EventName DataAdding -MessageData $traceQueue -Action { $q = $Event.MessageData if ($null -ne $EventArgs.ItemAdded.MessageData) { $q.Enqueue(@{ info = $EventArgs.ItemAdded.MessageData.ToString() }) } } } if ($traceLevel -ge [DscTraceLevel]::Debug) { $null = Register-ObjectEvent -InputObject $ps.Streams.Verbose ` -EventName DataAdding -MessageData $traceQueue -Action { $q = $Event.MessageData $q.Enqueue(@{ debug = $EventArgs.ItemAdded.Message }) } } if ($traceLevel -ge [DscTraceLevel]::Trace) { $null = Register-ObjectEvent -InputObject $ps.Streams.Debug ` -EventName DataAdding -MessageData $traceQueue -Action { $q = $Event.MessageData $q.Enqueue(@{ trace = $EventArgs.ItemAdded.Message }) } } $outputObjects = [System.Collections.Generic.List[object]]::new() try { $asyncResult = $ps.BeginInvoke() while (-not $asyncResult.IsCompleted) { Write-TraceQueue Start-Sleep -Milliseconds 100 } $outputCollection = $ps.EndInvoke($asyncResult) Write-TraceQueue foreach ($output in $outputCollection) { $outputObjects.Add($output) } } catch { Write-DscTrace -Now -Operation Error -Message $_ exit 1 } finally { $ps.Dispose() Get-EventSubscriber | Unregister-Event } Start-Sleep -Milliseconds 200 if ($hadErrors.Count -gt 0) { Write-DscTrace -Now -Operation Error ` -Message 'Errors occurred during execution. Check previous traces.' exit 1 } foreach ($obj in $outputObjects) { $obj | ConvertTo-Json -Depth 10 -Compress } '@ $script = $scriptTemplate. Replace('%%OPS_STRING%%', $opsString). Replace('%%TYPES_STRING%%', $typesString). Replace('%%IMPORT_SECTION%%', $importSection). Replace('%%SWITCH_BLOCK%%', $switchBlock). Replace('%%PROPERTY_MAP%%', $propertyMapBlock). Replace('%%OP_CASES_BLOCK%%', $opCasesBlock) return $script } |