diff --git a/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psd1 b/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psd1 index ec4d3a17e..67d781ecc 100644 --- a/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psd1 +++ b/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psd1 @@ -34,7 +34,7 @@ VariablesToExport = @() AliasesToExport = @() # DSC resources to export from this module -DscResourcesToExport = @('TestClassResource', 'NoExport', 'FilteredExport') +DscResourcesToExport = @('TestClassResource', 'NoExport', 'FilteredExport', 'StreamResource') # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ diff --git a/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psm1 b/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psm1 index ae0a79253..a6ea35030 100644 --- a/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psm1 +++ b/adapters/powershell/Tests/TestClassResource/0.0.1/TestClassResource.psm1 @@ -201,6 +201,36 @@ class FilteredExport : BaseTestClass } } +[DscResource()] +class StreamResource : BaseTestClass +{ + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Prop1 + + [void] Set() + { + Write-Verbose -Verbose "This is a Verbose message" + Write-Debug -Debug "This is a Debug message" + Write-Error "This is an Error message" + } + + [bool] Test() + { + Write-Host "This is a Host message" + Write-Information "This is an Information message" + return $true + } + + [StreamResource] Get() + { + Write-Warning "This is a Warning message" + return $this + } +} + function Test-World() { "Hello world from PSTestModule!" diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index c3f9ddc7b..4a00a8316 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -26,8 +26,8 @@ Describe 'PowerShell adapter resource tests' { It 'Get works on config with class-based resources' { - $r = Get-Content -Raw $pwshConfigPath | dsc config get -f - - $LASTEXITCODE | Should -Be 0 + $r = Get-Content -Raw $pwshConfigPath | dsc -l trace config get -f - 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) $res = $r | ConvertFrom-Json $res.results[0].result.actualState.result[0].properties.Prop1 | Should -BeExactly 'ValueForProp1' $res.results[0].result.actualState.result[0].properties.EnumProp | Should -BeExactly 'Expected' @@ -99,10 +99,10 @@ Describe 'PowerShell adapter resource tests' { - name: Class-resource Info type: TestClassResource/NoExport '@ - $out = $yaml | dsc config export -f - 2>&1 | Out-String - $LASTEXITCODE | Should -Be 2 - $out | Should -Not -BeNullOrEmpty - $out | Should -BeLike "*ERROR*Export method not implemented by resource 'TestClassResource/NoExport'*" + $null = $yaml | dsc -l trace config export -f - 2>$TestDrive/error.log + $logContent = Get-Content -Raw -Path $TestDrive/error.log + $LASTEXITCODE | Should -Be 2 -Because $logContent + $logContent | Should -BeLike "*ERROR*Export method not implemented by resource 'TestClassResource/NoExport'*" } It 'Export works with filtered export property' { @@ -126,7 +126,7 @@ Describe 'PowerShell adapter resource tests' { $res.resources[0].properties.result.count | Should -Be 1 $res.resources[0].properties.result[0].Name | Should -Be "FilteredExport" $res.resources[0].properties.result[0].Prop1 | Should -Be "Filtered Property for FilteredExport" - "$TestDrive/export_trace.txt" | Should -FileContentMatch "Properties provided for filtered export" + "$TestDrive/export_trace.txt" | Should -FileContentMatch "Properties provided for filtered export" -Because (Get-Content -Raw -Path $TestDrive/export_trace.txt) } It 'Export fails when filtered export is requested but not implemented' { diff --git a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 index c5170ad7f..f2d964f71 100644 --- a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 @@ -118,7 +118,7 @@ Describe 'PowerShell adapter resource tests' { $null = dsc resource list '*' -a Microsoft.DSC/PowerShell # call the ClearCache operation $scriptPath = Join-Path $PSScriptRoot '..' 'psDscAdapter' 'powershell.resource.ps1' - $null = & $scriptPath -Operation ClearCache + $null = & $scriptPath -Operation ClearCache 2>$null # verify that PSAdapter does not find the cache dsc -l debug resource list '*' -a Microsoft.DSC/PowerShell 2> $TestDrive/tracing.txt $LASTEXITCODE | Should -Be 0 @@ -377,4 +377,48 @@ Describe 'PowerShell adapter resource tests' { $res = $r | ConvertFrom-Json $res.actualState.SecureStringProp | Should -Not -BeNullOrEmpty } + + Context 'Tracing works' { + It 'Error messages come from Write-Error' { + $null = dsc -l error resource set -r TestClassResource/StreamResource -i '{"Name":"TestClassResource1"}' 2> $TestDrive/error.log + $logContent = Get-Content -Path $TestDrive/error.log -Raw + $LASTEXITCODE | Should -Be 2 -Because $logContent + $logContent | Should -Match 'ERROR .*? This is an Error message' -Because $logContent + } + + It 'Warning messages come from Write-Warning' { + $null = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc -l warn resource get -r 'TestClassResource/StreamResource' -f - 2> $TestDrive/warning.log + $logContent = Get-Content -Path $TestDrive/warning.log -Raw + $LASTEXITCODE | Should -Be 0 -Because $logContent + $logContent | Should -Match 'WARN .*? This is a Warning message' -Because $logContent + } + + It 'Info messages come from Write-Host' { + $null = "{'Name':'TestClassResource1'}" | dsc -l info resource test -r 'TestClassResource/StreamResource' -f - 2> $TestDrive/verbose.log + $logContent = Get-Content -Path $TestDrive/verbose.log -Raw + $LASTEXITCODE | Should -Be 0 -Because $logContent + $logContent | Should -Match 'INFO .*? This is a Host message' -Because $logContent + } + + It 'Debug messages come from Write-Verbose' { + $null = "{'Name':'TestClassResource1'}" | dsc -l debug resource set -r 'TestClassResource/StreamResource' -f - 2> $TestDrive/debug.log + $logContent = Get-Content -Path $TestDrive/debug.log -Raw + $LASTEXITCODE | Should -Be 2 -Because $logContent + $logContent | Should -Match 'DEBUG .*? This is a Verbose message' -Because $logContent + } + + It 'Trace messages come from Write-Debug' { + $null = dsc -l trace resource set -r TestClassResource/StreamResource -i '{"Name":"TestClassResource1"}' 2> $TestDrive/trace.log + $logContent = Get-Content -Path $TestDrive/trace.log -Raw + $LASTEXITCODE | Should -Be 2 -Because $logContent + $logContent | Should -Match 'TRACE .*? This is a Debug message' -Because $logContent + } + + It 'Trace messages come from Write-Information' { + $null = dsc -l trace resource test -r TestClassResource/StreamResource -i '{"Name":"TestClassResource1"}' 2> $TestDrive/trace_info.log + $logContent = Get-Content -Path $TestDrive/trace_info.log -Raw + $LASTEXITCODE | Should -Be 0 -Because $logContent + $logContent | Should -Match 'INFO .*? This is an Information message' -Because $logContent + } + } } diff --git a/adapters/powershell/Tests/win_powershell_cache.tests.ps1 b/adapters/powershell/Tests/win_powershell_cache.tests.ps1 index 8f2ca4b58..a7b6a724e 100644 --- a/adapters/powershell/Tests/win_powershell_cache.tests.ps1 +++ b/adapters/powershell/Tests/win_powershell_cache.tests.ps1 @@ -88,12 +88,14 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissio It 'Verify that there are no cache rebuilds for several sequential executions' { # first execution should build the cache $null = dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' + $tracingContent = Get-Content -Path $TestDrive/tracing.txt | Out-String + $tracingContent | Should -BeLike '*Constructing Get-DscResource cache*' -Because $tracingContent # next executions following shortly after should Not rebuild the cache 1..3 | ForEach-Object { $null = dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' + $tracingContent = Get-Content -Path $TestDrive/tracing.txt | Out-String + $tracingContent | Should -Not -BeLike '*Constructing Get-DscResource cache*' -Because $tracingContent } } diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index 6a98c27c7..a5e7eba40 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -11,277 +11,431 @@ param( [string]$ResourceType ) +$traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() +$hadErrors = [System.Collections.Concurrent.ConcurrentQueue[bool]]::new() + function Write-DscTrace { param( - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $true)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] - [string]$Operation = 'Debug', + [string]$Operation, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string]$Message + [string]$Message, + [switch]$Now ) - $trace = @{$Operation.ToLower() = $Message } | ConvertTo-Json -Compress - $host.ui.WriteErrorLine($trace) + $trace = @{$Operation.ToLower() = $Message } + + if ($Now) { + $host.ui.WriteErrorLine(($trace | ConvertTo-Json -Compress -Depth 10)) + } else { + $traceQueue.Enqueue($trace) + } } trap { - Write-DscTrace -Operation Debug -Message ($_ | Format-List -Force | Out-String) + Write-DscTrace -Operation Error -Message ($_ | Format-List -Force | Out-String) -Now } -if ($Operation -eq 'ClearCache') { - $cacheFilePath = if ($IsWindows) { - # PS 6+ on Windows - Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } else { - # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) { - Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - } else { - Join-Path $env:HOME ".dsc" "PSAdapterCache.json" +function Write-TraceQueue() { + $trace = $null + while (!$traceQueue.IsEmpty) { + if ($traceQueue.TryDequeue([ref] $trace)) { + $host.ui.WriteErrorLine(($trace | ConvertTo-Json -Compress -Depth 10)) } } - - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath - exit 0 } -# Adding some debug info to STDERR -'PSVersion=' + $PSVersionTable.PSVersion.ToString() | Write-DscTrace -'PSPath=' + $PSHome | Write-DscTrace -'PSModulePath=' + $env:PSModulePath | Write-DscTrace - -if ($PSVersionTable.PSVersion.Major -le 5) { - # For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath - if ($pwshPath = Get-Command 'pwsh' -ErrorAction Ignore | Select-Object -ExpandProperty Source) { - $pwshDefaultModulePaths = @( - "$HOME\Documents\PowerShell\Modules" # CurrentUser - "$Env:ProgramFiles\PowerShell\Modules" # AllUsers - Join-Path $(Split-Path $pwshPath -Parent) 'Modules' # Builtin - ) - $env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notin $pwshDefaultModulePaths }) -join ';' +$ps = [PowerShell]::Create().AddScript({ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate, ClearCache.')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'ClearCache')] + [string]$Operation, + [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] + [string]$jsonInput = '{}', + [Parameter()] + [string]$ResourceType, + [Parameter()] + [string]$ScriptRoot + ) + + trap { + Write-Error ($_ | Format-List -Force | Out-String) } -} -if ('Validate' -ne $Operation) { - Write-DscTrace -Operation Debug -Message "jsonInput=$jsonInput" + # NOTE: + # The adapter explicitly suppresses Debug and Verbose output by default to avoid + # excessively noisy logs from DSC resources that call Write-Debug / Write-Verbose. + # As a result, plain: + # Write-Verbose "message" + # Write-Debug "message" + # will NOT emit any output when invoked through this adapter. + # + # Resource authors who need to guarantee that diagnostic output is emitted MUST + # use the -Verbose / -Debug switches, for example: + # Write-Verbose -Verbose "message" + # Write-Debug -Debug "message" + # + # See the adapter's own usage below (e.g. Write-Debug -Debug 'PSVersion=...') for + # an example of this pattern. + $DebugPreference = 'SilentlyContinue' + $VerbosePreference = 'SilentlyContinue' + $ErrorActionPreference = 'Continue' + $InformationPreference = 'Continue' + $ProgressPreference = 'SilentlyContinue' + + if ($Operation -eq 'ClearCache') { + $cacheFilePath = if ($IsWindows) { + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" + } else { + # either WinPS or PS 6+ on Linux/Mac + if ($PSVersionTable.PSVersion.Major -le 5) { + Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" + } else { + Join-Path $env:HOME ".dsc" "PSAdapterCache.json" + } + } - # load private functions of psDscAdapter stub module - if ($PSVersionTable.PSVersion.Major -le 5) { - $psDscAdapter = Import-Module "$PSScriptRoot/win_psDscAdapter.psd1" -Force -PassThru + Remove-Item -Force -ErrorAction Ignore -Path $cacheFilePath + return } - else { - $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru + + # Adding some debug info to STDERR + Write-Debug -Debug ('PSVersion=' + $PSVersionTable.PSVersion.ToString()) + Write-Debug -Debug ('PSPath=' + $PSHome) + Write-Debug -Debug ('PSModulePath=' + $env:PSModulePath) + + if ($PSVersionTable.PSVersion.Major -le 5) { + # For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath + if ($pwshPath = Get-Command 'pwsh' -ErrorAction Ignore | Select-Object -ExpandProperty Source) { + $pwshDefaultModulePaths = @( + "$HOME\Documents\PowerShell\Modules" # CurrentUser + "$Env:ProgramFiles\PowerShell\Modules" # AllUsers + Join-Path $(Split-Path $pwshPath -Parent) 'Modules' # Builtin + ) + $env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notin $pwshDefaultModulePaths }) -join ';' + } } - # initialize OUTPUT as array - $result = [System.Collections.Generic.List[Object]]::new() -} + if ('Validate' -ne $Operation) { + Write-Debug -Debug ("jsonInput=$jsonInput") + + # load private functions of psDscAdapter stub module + if ($PSVersionTable.PSVersion.Major -le 5) { + $psDscAdapter = Import-Module "$ScriptRoot/win_psDscAdapter.psd1" -Force -PassThru + } + else { + $psDscAdapter = Import-Module "$ScriptRoot/psDscAdapter.psd1" -Force -PassThru + } -if ($jsonInput) { - if ($jsonInput -ne '{}') { - $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json + # initialize OUTPUT as array + $result = [System.Collections.Generic.List[Object]]::new() } - $new_psmodulepath = $inputobj_pscustomobj.psmodulepath - if ($new_psmodulepath) - { - $env:PSModulePath = $ExecutionContext.InvokeCommand.ExpandString($new_psmodulepath) + + if ($jsonInput) { + if ($jsonInput -ne '{}') { + $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json + } + $new_psmodulepath = $inputobj_pscustomobj.psmodulepath + if ($new_psmodulepath) + { + $env:PSModulePath = $ExecutionContext.InvokeCommand.ExpandString($new_psmodulepath) + } } -} -# process the operation requested to the script -switch ($Operation) { - 'List' { - $dscResourceCache = Invoke-DscCacheRefresh - - # cache was refreshed on script load - foreach ($dscResource in $dscResourceCache) { - - # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo - $DscResourceInfo = $dscResource.DscResourceInfo - - # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test - # TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module - if ($DscResourceInfo.ModuleName) { - $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 - # If the DscResourceInfo does have capabilities, use them or else use the module's capabilities - if ($DscResourceInfo.Capabilities) { - $capabilities = $DscResourceInfo.Capabilities - } elseif ($module.PrivateData.PSData.DscCapabilities) { - - $capabilities = $module.PrivateData.PSData.DscCapabilities - } else { - $capabilities = @('get', 'set', 'test') + # process the operation requested to the script + switch ($Operation) { + 'List' { + $dscResourceCache = Invoke-DscCacheRefresh + + # cache was refreshed on script load + foreach ($dscResource in $dscResourceCache) { + + # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo + $DscResourceInfo = $dscResource.DscResourceInfo + + # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test + # TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module + if ($DscResourceInfo.ModuleName) { + $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 + # If the DscResourceInfo does have capabilities, use them or else use the module's capabilities + if ($DscResourceInfo.Capabilities) { + $capabilities = $DscResourceInfo.Capabilities + } elseif ($module.PrivateData.PSData.DscCapabilities) { + + $capabilities = $module.PrivateData.PSData.DscCapabilities + } else { + $capabilities = @('get', 'set', 'test') + } } - } - # this text comes directly from the resource manifest for v3 native resources - if ($DscResourceInfo.Description) { - $description = $DscResourceInfo.Description - } - elseif ($module.Description) { - # some modules have long multi-line descriptions. to avoid issue, use only the first line. - $description = $module.Description.split("`r`n")[0] - } - else { - $description = '' - } + # this text comes directly from the resource manifest for v3 native resources + if ($DscResourceInfo.Description) { + $description = $DscResourceInfo.Description + } + elseif ($module.Description) { + # some modules have long multi-line descriptions. to avoid issue, use only the first line. + $description = $module.Description.split("`r`n")[0] + } + else { + $description = '' + } - # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) { - if ($ResourceType) { - $requireAdapter = 'Microsoft.Adapter/WindowsPowerShell' - } else { - $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + # match adapter to version of powershell + if ($PSVersionTable.PSVersion.Major -le 5) { + if ($ResourceType) { + $requireAdapter = 'Microsoft.Adapter/WindowsPowerShell' + } else { + $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + } } - } - else { - if ($ResourceType) { - $requireAdapter = 'Microsoft.Adapter/PowerShell' - } else { - $requireAdapter = 'Microsoft.DSC/PowerShell' + else { + if ($ResourceType) { + $requireAdapter = 'Microsoft.Adapter/PowerShell' + } else { + $requireAdapter = 'Microsoft.DSC/PowerShell' + } } - } - $properties = @() - foreach ($prop in $DscResourceInfo.Properties) { - $properties += $prop.Name - } + $properties = @() + foreach ($prop in $DscResourceInfo.Properties) { + $properties += $prop.Name + } - # OUTPUT dsc is expecting the following properties - [resourceOutput]@{ - type = $dscResource.Type - kind = 'resource' - version = [string]$DscResourceInfo.version - capabilities = $capabilities - path = $DscResourceInfo.Path - directory = $DscResourceInfo.ParentPath - implementedAs = $DscResourceInfo.ImplementationDetail - author = $DscResourceInfo.CompanyName - properties = $properties - requireAdapter = $requireAdapter - description = $description - } | ConvertTo-Json -Compress + # OUTPUT dsc is expecting the following properties + [resourceOutput]@{ + type = $dscResource.Type + kind = 'resource' + version = [string]$DscResourceInfo.version + capabilities = $capabilities + path = $DscResourceInfo.Path + directory = $DscResourceInfo.ParentPath + implementedAs = $DscResourceInfo.ImplementationDetail + author = $DscResourceInfo.CompanyName + properties = $properties + requireAdapter = $requireAdapter + description = $description + } + } } - } - { @('Get','Set','Test','Export') -contains $_ } { - if ($ResourceType) { - Write-DscTrace -Operation Debug -Message "Using resource type override: $ResourceType" - $dscResourceCache = Invoke-DscCacheRefresh -Module $ResourceType.Split('/')[0] - if ($null -eq $dscResourceCache) { - Write-DscTrace -Operation Error -Message ("DSC resource '{0}' module not found." -f $ResourceType) - exit 1 + { @('Get','Set','Test','Export') -contains $_ } { + if ($ResourceType) { + Write-Debug -Debug ("Using resource type override: $ResourceType") + $dscResourceCache = Invoke-DscCacheRefresh -Module $ResourceType.Split('/')[0] + if ($null -eq $dscResourceCache) { + Write-Error ("DSC resource '{0}' module not found." -f $ResourceType) + exit 1 + } + + $desiredState = $psDscAdapter.invoke( { param($jsonInput, $type) Get-DscResourceObject -jsonInput $jsonInput -type $type }, $jsonInput, $ResourceType ) + if ($null -eq $desiredState) { + Write-Error 'Failed to create configuration object from provided input JSON.' + exit 1 + } + + $desiredState.Type = $ResourceType + $inDesiredState = $true + $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $desiredState, $dscResourceCache) + if ($null -eq $actualState) { + Write-Error 'Incomplete GET for resource ' + $desiredState.Name + exit 1 + } + if ($actualState.Properties.InDesiredState -eq $false) { + $inDesiredState = $false + } + + if ($Operation -in @('Set', 'Test')) { + $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) + } + + if ($Operation -eq 'Test') { + $actualState.Properties | Add-Member -MemberType NoteProperty -Name _inDesiredState -Value $inDesiredState -Force + } + + if ($Operation -eq 'Export') { + foreach ($instance in $actualState) { + $instance + } + exit 0 + } + + $result = $actualState.Properties + return $result } - $desiredState = $psDscAdapter.invoke( { param($jsonInput, $type) Get-DscResourceObject -jsonInput $jsonInput -type $type }, $jsonInput, $ResourceType ) + $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) if ($null -eq $desiredState) { - Write-DscTrace -Operation Error -message 'Failed to create configuration object from provided input JSON.' + Write-Error 'Failed to create configuration object from provided input JSON.' exit 1 } - $desiredState.Type = $ResourceType - $inDesiredState = $true - $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $desiredState, $dscResourceCache) - if ($null -eq $actualState) { - Write-DscTrace -Operation Error -Message 'Incomplete GET for resource ' + $desiredState.Name + # only need to cache the resources that are used + $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } + if ($null -eq $dscResourceModules) { + Write-Error 'Could not get list of DSC resource types from provided JSON.' exit 1 } - if ($actualState.Properties.InDesiredState -eq $false) { - $inDesiredState = $false - } - if ($Operation -in @('Set', 'Test')) { - $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) - } + # get unique module names from the desiredState input + $moduleInput = $desiredState | Select-Object -ExpandProperty Type | Sort-Object -Unique - if ($Operation -eq 'Test') { - $actualState.Properties | Add-Member -MemberType NoteProperty -Name _inDesiredState -Value $inDesiredState -Force + # refresh the cache with the modules that are available on the system + $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules + + # check if all the desired modules are in the cache + $moduleInput | ForEach-Object { + if ($dscResourceCache.type -notcontains $_) { + Write-Error ("DSC resource '{0}' module not found." -f $_) + exit 1 + } } - if ($Operation -eq 'Export') { - foreach ($instance in $actualState) { - $instance | ConvertTo-Json -Depth 10 -Compress + $inDesiredState = $true + foreach ($ds in $desiredState) { + # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState + $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $ds, $dscResourceCache) + if ($null -eq $actualState) { + Write-Error ("Failed to invoke operation '{0}' for resource name '{1}'." -f $Operation, $ds.Name) + exit 1 + } + if ($null -ne $actualState.Properties -and $actualState.Properties.InDesiredState -eq $false) { + $inDesiredState = $false } - exit 0 + $result += $actualState } - $result = $actualState.Properties | ConvertTo-Json -Depth 10 -Compress - Write-DscTrace -Operation Debug -Message "jsonOutput=$result" + # OUTPUT json to stderr for debug, and to stdout + if ($Operation -eq 'Test') { + $result = @{ result = $result; _inDesiredState = $inDesiredState } + } + else { + $result = @{ result = $result } + } return $result } + 'Validate' { + # VALIDATE not implemented - $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) - if ($null -eq $desiredState) { - Write-DscTrace -Operation Error -message 'Failed to create configuration object from provided input JSON.' - exit 1 + # OUTPUT + @{ valid = $true } } - - # only need to cache the resources that are used - $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } - if ($null -eq $dscResourceModules) { - Write-DscTrace -Operation Error -Message 'Could not get list of DSC resource types from provided JSON.' - exit 1 + Default { + Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' } + } - # get unique module names from the desiredState input - $moduleInput = $desiredState | Select-Object -ExpandProperty Type | Sort-Object -Unique + # output format for resource list + class resourceOutput { + [string] $type + [string] $kind + [string] $version + [string[]] $capabilities + [string] $path + [string] $directory + [string] $implementedAs + [string] $author + [string[]] $properties + [string] $requireAdapter + [string] $description + } +}).AddParameter('Operation', $Operation).AddParameter('jsonInput', $jsonInput).AddParameter('ResourceType', $ResourceType).AddParameter('ScriptRoot', $PSScriptRoot) + +enum DscTraceLevel { + Error + Warn + Info + Debug + Trace +} - # refresh the cache with the modules that are available on the system - $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules +$traceLevel = if ($env:DSC_TRACE_LEVEL) { + try { + [DscTraceLevel]$env:DSC_TRACE_LEVEL + } catch { + Write-DscTrace -Operation Warn -Message ("Invalid DSC_TRACE_LEVEL value '{0}'. Defaulting to 'Warn'." -f $env:DSC_TRACE_LEVEL) -Now + [DscTraceLevel]::Warn + } +} else { + [DscTraceLevel]::Warn +} - # check if all the desired modules are in the cache - $moduleInput | ForEach-Object { - if ($dscResourceCache.type -notcontains $_) { - ("DSC resource '{0}' module not found." -f $_) | Write-DscTrace -Operation Error - exit 1 - } - } +$null = Register-ObjectEvent -InputObject $ps.Streams.Error -EventName DataAdding -MessageData @{traceQueue=$traceQueue; hadErrors=$hadErrors} -Action { + $traceQueue = $Event.MessageData.traceQueue + # convert error to string since it's an ErrorRecord + $traceQueue.Enqueue(@{ error = [string]$EventArgs.ItemAdded }) + $Event.MessageData.hadErrors.Enqueue($true) +} - $inDesiredState = $true - foreach ($ds in $desiredState) { - # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState - $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $ds, $dscResourceCache) - if ($null -eq $actualState) { - "Failed to invoke operation '{0}' for resource name '{1}'." -f $Operation, $ds.Name | Write-DscTrace -Operation Error - exit 1 - } - if ($null -ne $actualState.Properties -and $actualState.Properties.InDesiredState -eq $false) { - $inDesiredState = $false - } - $result += $actualState - } +if ($traceLevel -ge [DscTraceLevel]::Warn) { + $null = Register-ObjectEvent -InputObject $ps.Streams.Warning -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + $traceQueue.Enqueue(@{ warn = $EventArgs.ItemAdded.Message }) + } +} - # OUTPUT json to stderr for debug, and to stdout - if ($Operation -eq 'Test') { - $result = @{ result = $result; _inDesiredState = $inDesiredState } | ConvertTo-Json -Depth 10 -Compress - } - else { - $result = @{ result = $result } | ConvertTo-Json -Depth 10 -Compress +if ($traceLevel -ge [DscTraceLevel]::Info) { + $null = Register-ObjectEvent -InputObject $ps.Streams.Information -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + if ($null -ne $EventArgs.ItemAdded.MessageData) { + $traceQueue.Enqueue(@{ info = $EventArgs.ItemAdded.MessageData.ToString() }) } - Write-DscTrace -Operation Debug -Message "jsonOutput=$result" - return $result } - 'Validate' { - # VALIDATE not implemented +} + +if ($traceLevel -ge [DscTraceLevel]::Debug) { + $null = Register-ObjectEvent -InputObject $ps.Streams.Verbose -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + # Verbose messages tend to be in large quantity and more useful to developers, so log as Debug + $traceQueue.Enqueue(@{ debug = $EventArgs.ItemAdded.Message }) + } +} - # OUTPUT - @{ valid = $true } | ConvertTo-Json +if ($traceLevel -ge [DscTraceLevel]::Trace) { + $null = Register-ObjectEvent -InputObject $ps.Streams.Debug -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + # Debug messages may contain raw info, so log as Trace + $traceQueue.Enqueue(@{ trace = $EventArgs.ItemAdded.Message }) } - Default { - Write-DscTrace -Operation Error -Message 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' +} +$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 + + if ($ps.HadErrors) { + # Anything written to stderr sets this flag, so we'll write a debug trace, but not treat as error + Write-DscTrace -Now -Operation Debug -Message 'HadErrors set during script execution.' } + + foreach ($output in $outputCollection) { + $outputObjects.Add($output) + } +} +catch { + Write-DscTrace -Now -Operation Error -Message $_ + exit 1 +} +finally { + $ps.Dispose() + Get-EventSubscriber | Unregister-Event +} + +# Allow any remaining event handlers time to enqueue to $traceQueue and $hadErrors +Start-Sleep -Milliseconds 200 +if ($hadErrors.Count -gt 0) { + Write-DscTrace -Now -Operation Error -Message 'Errors were captured during script execution. Check previous error traces for details.' + exit 1 } -# output format for resource list -class resourceOutput { - [string] $type - [string] $kind - [string] $version - [string[]] $capabilities - [string] $path - [string] $directory - [string] $implementedAs - [string] $author - [string[]] $properties - [string] $requireAdapter - [string] $description +foreach ($obj in $outputObjects) { + $obj | ConvertTo-Json -Depth 10 -Compress } diff --git a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 index fc1b84f0e..244f2c67b 100644 --- a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 @@ -2,20 +2,7 @@ # Licensed under the MIT License. $script:CurrentCacheSchemaVersion = 3 - -function Write-DscTrace { - param( - [Parameter(Mandatory = $false)] - [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] - [string]$Operation = 'Debug', - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string]$Message - ) - - $trace = @{$Operation.ToLower() = $Message } | ConvertTo-Json -Compress - $host.ui.WriteErrorLine($trace) -} +$ErrorActionPreference = 'Stop' function Import-PSDSCModule { $m = Get-Module PSDesiredStateConfiguration -ListAvailable | Sort-Object -Descending | Select-Object -First 1 @@ -26,13 +13,12 @@ function Get-DSCResourceModules { $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() foreach ($folder in $listPSModuleFolders) { - if (!(Test-Path $folder)) { + if (!(Test-Path $folder -ErrorAction Ignore)) { continue } - foreach ($moduleFolder in Get-ChildItem $folder -Directory) { - $addModule = $false - foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { + foreach ($moduleFolder in Get-ChildItem $folder -Directory -ErrorAction Ignore) { + foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2 -ErrorAction Ignore) { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' if ($null -ne $containsDSCResource) { $dscModulePsd1List.Add($psd1) | Out-Null @@ -105,13 +91,13 @@ function FindAndParseResourceDefinitions { return } - "Loading resources from file '$filePath'" | Write-DscTrace -Operation Trace + Write-Debug -Debug ("Loading resources from file '$filePath'") #TODO: Ensure embedded instances in properties are working correctly [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) foreach ($e in $errors) { - $e | Out-String | Write-DscTrace -Operation Error + $e | Out-String | Write-Error } $typeDefinitions = $ast.FindAll( @@ -155,7 +141,7 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName $method = $null if ($HasFilterProperties) { - "Properties provided for filtered export" | Write-DscTrace -Operation Trace + Write-Verbose -Verbose "Properties provided for filtered export" $method = foreach ($mt in $methods) { if ($mt.GetParameters().Count -gt 0) { $mt @@ -164,12 +150,12 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName } if ($null -eq $method) { - "Export method with parameters not implemented by resource '$ResourceTypeName'. Filtered export is not supported." | Write-DscTrace -Operation Error + Write-Error ("Export method with parameters not implemented by resource '$ResourceTypeName'. Filtered export is not supported.") exit 1 } } else { - "No properties provided, using parameterless export" | Write-DscTrace -Operation Trace + Write-Verbose -Verbose "No properties provided, using parameterless export" $method = foreach ($mt in $methods) { if ($mt.GetParameters().Count -eq 0) { $mt @@ -178,7 +164,7 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName } if ($null -eq $method) { - "Export method not implemented by resource '$ResourceTypeName'" | Write-DscTrace -Operation Error + Write-Error ("Export method not implemented by resource '$ResourceTypeName'") exit 1 } } @@ -193,12 +179,12 @@ function LoadPowerShellClassResourcesFromModule { [PSModuleInfo]$moduleInfo ) - "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace + Write-Debug -Debug ("Loading resources from module '$($moduleInfo.Path)'") if ($moduleInfo.RootModule) { if (".psm1", ".ps1" -notcontains ([System.IO.Path]::GetExtension($moduleInfo.RootModule)) -and (-not $moduleInfo.NestedModules)) { - "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace + Write-Debug -Debug ("RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'") return [System.Collections.Generic.List[DscResourceInfo]]::new() } @@ -255,13 +241,13 @@ function Invoke-DscCacheRefresh { } if (Test-Path $cacheFilePath) { - "Reading from Get-DscResource cache file $cacheFilePath" | Write-DscTrace + Write-Verbose -Verbose ("Reading from Get-DscResource cache file $cacheFilePath") $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace + Write-Verbose -Verbose ("Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')") } else { $dscResourceCacheEntries = $cache.ResourceCache @@ -270,10 +256,10 @@ function Invoke-DscCacheRefresh { # if there is nothing in the cache file - refresh cache $refreshCache = $true - "Filtered DscResourceCache cache is empty" | Write-DscTrace + Write-Debug -Debug "Filtered DscResourceCache cache is empty" } else { - "Checking cache for stale entries" | Write-DscTrace + Write-Debug -Debug "Checking cache for stale entries" foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -289,13 +275,13 @@ function Invoke-DscCacheRefresh { $cache_LastWriteTime = $cache_LastWriteTime.AddTicks( - ($cache_LastWriteTime.Ticks % [TimeSpan]::TicksPerSecond)); if (-not ($file_LastWriteTime.Equals($cache_LastWriteTime))) { - "Detected stale cache entry '$($_.Name)'" | Write-DscTrace + Write-Debug -Debug ("Detected stale cache entry '$($_.Name)'") $refreshCache = $true break } } else { - "Detected non-existent cache entry '$($_.Name)'" | Write-DscTrace + Write-Debug -Debug ("Detected non-existent cache entry '$($_.Name)'") $refreshCache = $true break } @@ -305,17 +291,16 @@ function Invoke-DscCacheRefresh { } if (-not $refreshCache) { - "Checking cache for stale PSModulePath" | Write-DscTrace + Write-Debug -Debug "Checking cache for stale PSModulePath" - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ErrorAction Ignore } $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) $hs_cache.SymmetricExceptWith($hs_live) $diff = $hs_cache - "PSModulePath diff '$diff'" | Write-DscTrace - + Write-Debug -Debug ("PSModulePath diff '$diff'") if ($diff.Count -gt 0) { $refreshCache = $true } @@ -324,12 +309,12 @@ function Invoke-DscCacheRefresh { } } else { - "Cache file not found '$cacheFilePath'" | Write-DscTrace + Write-Verbose -Verbose ("Cache file not found '$cacheFilePath'") $refreshCache = $true } if ($refreshCache) { - 'Constructing Get-DscResource cache' | Write-DscTrace + Write-Verbose -Verbose "Constructing Get-DscResource cache" # create a list object to store cache of Get-DscResource [dscResourceCacheEntry[]]$dscResourceCacheEntries = [System.Collections.Generic.List[Object]]::new() @@ -337,7 +322,7 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules if ($null -ne $dscResourceModulePsd1s) { - $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) + $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) -ErrorAction Ignore $processedModuleNames = @{} foreach ($mod in $modules) { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { @@ -346,7 +331,7 @@ function Invoke-DscCacheRefresh { # from several modules with the same name select the one with the highest version $selectedMod = $modules | Where-Object Name -EQ $mod.Name if ($selectedMod.Count -gt 1) { - "Found $($selectedMod.Count) modules with name '$($mod.Name)'" | Write-DscTrace -Operation Trace + Write-Debug -Debug ("Found $($selectedMod.Count) modules with name '$($mod.Name)'") $selectedMod = $selectedMod | Sort-Object -Property Version -Descending | Select-Object -First 1 } @@ -363,7 +348,7 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*.psm1", "*.mof" -ea Ignore | ForEach-Object { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*.psm1", "*.mof" -ErrorAction Ignore | ForEach-Object { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } @@ -376,13 +361,13 @@ function Invoke-DscCacheRefresh { [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ErrorAction Ignore } $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion # save cache for future use # TODO: replace this with a high-performance serializer - "Saving Get-DscResource cache to '$cacheFilePath'" | Write-DscTrace + Write-Debug -Debug ("Saving Get-DscResource cache to '$cacheFilePath'") $jsonCache = $cache | ConvertTo-Json -Depth 90 New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null } @@ -435,10 +420,10 @@ function Invoke-DscOperation { ) $osVersion = [System.Environment]::OSVersion.VersionString - 'OS version: ' + $osVersion | Write-DscTrace + Write-Debug -Debug ('OS version: ' + $osVersion) $psVersion = $PSVersionTable.PSVersion.ToString() - 'PowerShell version: ' + $psVersion | Write-DscTrace + Write-Debug -Debug ('PowerShell version: ' + $psVersion) # get details from cache about the DSC resource, if it exists $cachedDscResourceInfo = $dscResourceCache | Where-Object Type -EQ $DesiredState.type | ForEach-Object DscResourceInfo | Select-Object -First 1 @@ -465,7 +450,7 @@ function Invoke-DscOperation { $ValidProperties = $cachedDscResourceInfo.Properties.Name - $ValidProperties | ConvertTo-Json | Write-DscTrace -Operation Trace + Write-Debug -Debug ("Valid properties: " + ($ValidProperties | ConvertTo-Json)) if ($DesiredState.properties) { # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object @@ -475,7 +460,7 @@ function Invoke-DscOperation { if ($_.Value -is [System.Management.Automation.PSCustomObject]) { if ($validateProperty -and $validateProperty.PropertyType -in @('PSCredential', 'System.Management.Automation.PSCredential')) { if (-not $_.Value.Username -or -not $_.Value.Password) { - "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + Write-Error ("Credential object '$($_.Name)' requires both 'username' and 'password' properties") exit 1 } $dscResourceInstance.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) @@ -548,22 +533,22 @@ function Invoke-DscOperation { } catch { - 'Exception: ' + $_.Exception.Message | Write-DscTrace -Operation Error + Write-Error ('Exception: ' + $_.Exception.Message) exit 1 } } Default { - 'Resource ImplementationDetail not supported: ' + $cachedDscResourceInfo.ImplementationDetail | Write-DscTrace -Operation Error + Write-Error ('Resource ImplementationDetail not supported: ' + $cachedDscResourceInfo.ImplementationDetail) exit 1 } } - "Output: $($addToActualState | ConvertTo-Json -Depth 10 -Compress)" | Write-DscTrace -Operation Trace + Write-Debug -Debug ("Output: $($addToActualState | ConvertTo-Json -Depth 10 -Compress)") return $addToActualState } else { $dsJSON = $DesiredState | ConvertTo-Json -Depth 10 - 'Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.' | Write-DscTrace -Operation Error + Write-Error ('Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.') exit 1 } } diff --git a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 index 38665e720..4cd32a98b 100644 --- a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 @@ -2,24 +2,11 @@ # Licensed under the MIT License. $global:ProgressPreference = 'SilentlyContinue' +$ErrorActionPreference = 'Stop' $script:CurrentCacheSchemaVersion = 1 trap { - Write-DscTrace -Operation Debug -Message ($_ | Format-List -Force | Out-String) -} - -function Write-DscTrace { - param( - [Parameter(Mandatory = $false)] - [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] - [string]$Operation = 'Debug', - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string]$Message - ) - - $trace = @{$Operation.ToLower() = $Message } | ConvertTo-Json -Compress - $host.ui.WriteErrorLine($trace) + Write-Error ($_ | Format-List -Force | Out-String) } # if the version of PowerShell is greater than 5, import the PSDesiredStateConfiguration module @@ -27,12 +14,15 @@ function Write-DscTrace { # In Windows PowerShell, we should always use version 1.1 that ships in Windows. if ($PSVersionTable.PSVersion.Major -gt 5) { $m = Get-Module PSDesiredStateConfiguration -ListAvailable | Sort-Object -Descending | Select-Object -First 1 + if (-not $m) { + throw "PSDesiredStateConfiguration module not found. Please install PSDesiredStateConfiguration from PowerShell Gallery or ensure it is available in the PSModulePath." + } $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } else { $env:PSModulePath = "$env:windir\System32\WindowsPowerShell\v1.0\Modules;$env:PSModulePath" $PSDesiredStateConfiguration = Import-Module -Name 'PSDesiredStateConfiguration' -RequiredVersion '1.1' -Force -PassThru -ErrorAction stop -ErrorVariable $importModuleError if (-not [string]::IsNullOrEmpty($importModuleError)) { - 'Could not import PSDesiredStateConfiguration 1.1 in Windows PowerShell. ' + $importModuleError | Write-DscTrace -Operation Error + Write-Error ('Could not import PSDesiredStateConfiguration 1.1 in Windows PowerShell. ' + $importModuleError) } } @@ -64,22 +54,21 @@ function Invoke-DscCacheRefresh { Repair-ValidPSModulePath if (Test-Path $cacheFilePath) { - "Reading from Get-DscResource cache file $cacheFilePath" | Write-DscTrace + Write-Verbose -Verbose ("Reading from Get-DscResource cache file $cacheFilePath") $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace + Write-Verbose -Verbose ("Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')") } else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { # if there is nothing in the cache file - refresh cache $refreshCache = $true - "Filtered DscResourceCache cache is empty" | Write-DscTrace + Write-Debug -Debug ("Filtered DscResourceCache cache is empty") } else { - "Checking cache for stale PSModulePath" | Write-DscTrace - + Write-Debug -Debug ("Checking cache for stale PSModulePath") $m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ErrorAction Ignore } $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) @@ -87,14 +76,14 @@ function Invoke-DscCacheRefresh { $hs_cache.SymmetricExceptWith($hs_live) $diff = $hs_cache - "PSModulePath diff '$diff'" | Write-DscTrace + Write-Debug -Debug ("PSModulePath diff '$diff'") # TODO: Optimise for named module refresh if ($diff.Count -gt 0) { $refreshCache = $true } if (-not $refreshCache) { - "Checking cache for stale entries" | Write-DscTrace + Write-Debug -Debug ("Checking cache for stale entries") foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -105,12 +94,12 @@ function Invoke-DscCacheRefresh { $cache_LastWriteTime = [long]$_.Value if ($file_LastWriteTime -ne $cache_LastWriteTime) { - "Detected stale cache entry '$($_.Name)'" | Write-DscTrace + Write-Debug -Debug ("Detected stale cache entry '$($_.Name)'") $namedModules.Add($cacheEntry.DscResourceInfo.ModuleName) break } } else { - "Detected non-existent cache entry '$($_.Name)'" | Write-DscTrace + Write-Debug -Debug ("Detected non-existent cache entry '$($_.Name)'") $namedModules.Add($cacheEntry.DscResourceInfo.ModuleName) break } @@ -123,31 +112,31 @@ function Invoke-DscCacheRefresh { $namedModules.AddRange(@($Module)) } $namedModules = $namedModules | Sort-Object -Unique - "Module list: $($namedModules -join ', ')" | Write-DscTrace + Write-Debug -Debug ("Module list: $($namedModules -join ', ')") } } } } else { - "Cache file not found '$cacheFilePath'" | Write-DscTrace + Write-Verbose -Verbose ("Cache file not found '$cacheFilePath'") $refreshCache = $true } if ($refreshCache) { - 'Constructing Get-DscResource cache' | Write-DscTrace + Write-Verbose -Verbose ('Constructing Get-DscResource cache') # create a list object to store cache of Get-DscResource $dscResourceCacheEntries = [System.Collections.Generic.List[dscResourceCacheEntry]]::new() # improve by performance by having the option to only get details for named modules # workaround for File and SignatureValidation resources that ship in Windows - Write-DscTrace -Operation Debug "Named module count: $($namedModules.Count)" + Write-Debug -Debug ("Named module count: $($namedModules.Count)") if ($namedModules.Count -gt 0) { - Write-DscTrace -Operation Debug "Modules specified, getting DSC resources from modules: $($namedModules -join ', ')" + Write-Debug -Debug ("Modules specified, getting DSC resources from modules: $($namedModules -join ', ')") $DscResources = [System.Collections.Generic.List[Object]]::new() $Modules = [System.Collections.Generic.List[Object]]::new() $filteredResources = @() foreach ($m in $namedModules) { - Write-DscTrace -Operation Debug "Getting DSC resources for module '$($m | Out-String)'" + Write-Debug -Debug ("Getting DSC resources for module '$($m | Out-String)'") $DscResources.AddRange(@(Get-DscResource -Module $m)) $Modules.AddRange(@(Get-Module -Name $m -ListAvailable)) } @@ -166,7 +155,7 @@ function Invoke-DscCacheRefresh { # Exclude the one module that was passed in as a parameter $existingDscResourceCacheEntries = @($cache.ResourceCache | Where-Object -Property Type -NotIn $filteredResources) } else { - Write-DscTrace -Operation Debug "No modules specified, getting all DSC resources" + Write-Debug -Debug ("No modules specified, getting all DSC resources") $DscResources = Get-DscResource $Modules = Get-Module -ListAvailable } @@ -183,7 +172,7 @@ function Invoke-DscCacheRefresh { if ( $psdscVersion -ge '2.0.7' ) { # only support known dscResourceType if ([dscResourceType].GetEnumNames() -notcontains $dscResource.ImplementationDetail) { - 'Implementation detail not found: ' + $dscResource.ImplementationDetail | Write-DscTrace -Operation Warn + Write-Warning ('Implementation detail not found: ' + $dscResource.ImplementationDetail) continue } } @@ -230,7 +219,7 @@ function Invoke-DscCacheRefresh { # workaround: Use GetTypeInstanceFromModule to get the type instance from the module and validate if it is a class-based resource $classBased = GetTypeInstanceFromModule -modulename $moduleName -classname $dscResource.Name -ErrorAction Ignore if ($classBased -and ($classBased.CustomAttributes.AttributeType.Name -eq 'DscResourceAttribute')) { - "Detected class-based resource: $($dscResource.Name) => Type: $($classBased.BaseType.FullName)" | Write-DscTrace + Write-Debug -Debug ("Detected class-based resource: $($dscResource.Name) => Type: $($classBased.BaseType.FullName)") $dscResourceInfo.ImplementationDetail = 'ClassBased' $properties = GetClassBasedProperties -filePath $dscResource.Path -className $dscResource.Name if ($null -ne $properties) { @@ -262,13 +251,13 @@ function Invoke-DscCacheRefresh { [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries.ToArray() - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ErrorAction Ignore } $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion # save cache for future use # TODO: replace this with a high-performance serializer - "Saving Get-DscResource cache to '$cacheFilePath'" | Write-DscTrace + Write-Debug -Debug ("Saving Get-DscResource cache to '$cacheFilePath'") $jsonCache = $cache | ConvertTo-Json -Depth 90 New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null } @@ -321,13 +310,13 @@ function Invoke-DscOperation { ) $osVersion = [System.Environment]::OSVersion.VersionString - 'OS version: ' + $osVersion | Write-DscTrace + Write-Debug -Debug ("OS version: " + $osVersion) $psVersion = $PSVersionTable.PSVersion.ToString() - 'PowerShell version: ' + $psVersion | Write-DscTrace + Write-Debug -Debug ("PowerShell version: " + $psVersion) $moduleVersion = Get-Module PSDesiredStateConfiguration | ForEach-Object Version - 'PSDesiredStateConfiguration module version: ' + $moduleVersion | Write-DscTrace + Write-Debug -Debug ("PSDesiredStateConfiguration module version: " + $moduleVersion) # get details from cache about the DSC resource, if it exists $cachedDscResourceInfo = $dscResourceCache | Where-Object Type -EQ $DesiredState.type | ForEach-Object DscResourceInfo | Select-Object -First 1 @@ -343,7 +332,7 @@ function Invoke-DscOperation { if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) } } - 'DSC resource implementation: ' + [dscResourceType]$cachedDscResourceInfo.ImplementationDetail | Write-DscTrace + Write-Debug -Debug ("DSC resource implementation: " + [dscResourceType]$cachedDscResourceInfo.ImplementationDetail) # workaround: script based resources do not validate Get parameter consistency, so we need to remove any parameters the author chose not to include in Get-TargetResource switch ([dscResourceType]$cachedDscResourceInfo.ImplementationDetail) { @@ -351,7 +340,7 @@ function Invoke-DscOperation { # For Linux/MacOS, only class based resources are supported and are called directly. if ($IsLinux) { - 'Script based resources are only supported on Windows.' | Write-DscTrace -Operation Error + Write-Error 'Script based resources are only supported on Windows.' exit 1 } @@ -372,10 +361,10 @@ function Invoke-DscOperation { $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { if ($_.Value -is [System.Management.Automation.PSCustomObject]) { $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name - Write-DscTrace -Operation Debug -Message "Property type: $($validateProperty.PropertyType)" + Write-Debug -Debug ("Property type: $($validateProperty.PropertyType)") if ($validateProperty -and $validateProperty.PropertyType -eq '[PSCredential]') { if (-not $_.Value.Username -or -not $_.Value.Password) { - "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + Write-Error ("Credential object '$($_.Name)' requires both 'username' and 'password' properties") exit 1 } $property.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) @@ -389,7 +378,7 @@ function Invoke-DscOperation { # using the cmdlet the appropriate dsc module, and handle errors try { - Write-DscTrace -Operation Debug -Message "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property | ConvertTo-Json -Compress)" + Write-Debug -Debug ("Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property | ConvertTo-Json -Compress)") $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property -ErrorAction Stop if ($invokeResult.GetType().Name -eq 'Hashtable') { @@ -402,11 +391,11 @@ function Invoke-DscOperation { # set the properties of the OUTPUT object from the result of Get-TargetResource $addToActualState.properties = $ResultProperties } catch { - $_.Exception | Format-List * -Force | Out-String | Write-DscTrace -Operation Debug + Write-Debug -Debug ($_.Exception | Format-List * -Force | Out-String) if ($_.Exception.MessageId -eq 'DscResourceNotFound') { - Write-DscTrace -Operation Warn -Message 'For Windows PowerShell, DSC resources must be installed with scope AllUsers' + Write-Warning 'For Windows PowerShell, DSC resources must be installed with scope AllUsers' } - 'Exception: ' + $_.Exception.Message | Write-DscTrace -Operation Error + Write-Error ('Exception: ' + $_.Exception.Message) exit 1 } } @@ -418,7 +407,7 @@ function Invoke-DscOperation { $ValidProperties = $cachedDscResourceInfo.Properties.Name - $ValidProperties | ConvertTo-Json | Write-DscTrace -Operation Trace + Write-Debug -Debug ($ValidProperties | ConvertTo-Json) if ($DesiredState.properties) { # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object @@ -426,10 +415,10 @@ function Invoke-DscOperation { # handle input objects by converting them to a hash table if ($_.Value -is [System.Management.Automation.PSCustomObject]) { $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name - Write-DscTrace -Operation Debug -Message "Property type: $($validateProperty.PropertyType)" + Write-Debug -Debug ("Property type: $($validateProperty.PropertyType)") if ($validateProperty.PropertyType -eq 'PSCredential') { if (-not $_.Value.Username -or -not $_.Value.Password) { - "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + Write-Error ("Credential object '$($_.Name)' requires both 'username' and 'password' properties") exit 1 } $dscResourceInstance.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) @@ -481,22 +470,22 @@ function Invoke-DscOperation { } } } catch { - $_.Exception | Format-List * -Force | Out-String | Write-DscTrace -Operation Debug + Write-Debug -Debug ($_.Exception | Format-List * -Force | Out-String) if ($_.Exception.MessageId -eq 'DscResourceNotFound') { - Write-DscTrace -Operation Warn -Message 'For Windows PowerShell, DSC resources must be installed with scope AllUsers' + Write-Warning 'For Windows PowerShell, DSC resources must be installed with scope AllUsers' } - 'Exception: ' + $_.Exception.Message | Write-DscTrace -Operation Error + Write-Error ('Exception: ' + $_.Exception.Message) exit 1 } } 'Binary' { if ($PSVersionTable.PSVersion.Major -gt 5) { - 'To use a binary resource such as File, Log, or SignatureValidation, use the Microsoft.Windows/WindowsPowerShell adapter.' | Write-DscTrace + Write-Debug -Debug 'To use a binary resource such as File, Log, or SignatureValidation, use the Microsoft.Windows/WindowsPowerShell adapter.' exit 1 } if (-not (($cachedDscResourceInfo.ImplementedAs -eq 'Binary') -and ('File', 'Log', 'SignatureValidation' -contains $cachedDscResourceInfo.Name))) { - 'Only File, Log, and SignatureValidation are supported as Binary resources.' | Write-DscTrace + Write-Debug -Debug 'Only File, Log, and SignatureValidation are supported as Binary resources.' exit 1 } @@ -504,10 +493,10 @@ function Invoke-DscOperation { $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { if ($_.Value -is [System.Management.Automation.PSCustomObject]) { $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name - Write-DscTrace -Operation Debug -Message "Property type: $($validateProperty.PropertyType)" + Write-Debug -Debug ("Property type: $($validateProperty.PropertyType)") if ($validateProperty.PropertyType -eq '[PSCredential]') { if (-not $_.Value.Username -or -not $_.Value.Password) { - "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + Write-Error ("Credential object '$($_.Name)' requires both 'username' and 'password' properties") exit 1 } $property.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) @@ -521,7 +510,7 @@ function Invoke-DscOperation { # using the cmdlet from PSDesiredStateConfiguration module in Windows try { - Write-DscTrace -Operation Debug -Message "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property | ConvertTo-Json -Compress)" + Write-Debug -Debug "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property | ConvertTo-Json -Compress)" $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property if ($invokeResult.GetType().Name -eq 'Hashtable') { $invokeResult.keys | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ } @@ -533,12 +522,12 @@ function Invoke-DscOperation { # set the properties of the OUTPUT object from the result of Get-TargetResource $addToActualState.properties = $ResultProperties } catch { - 'Exception: ' + $_.Exception.Message | Write-DscTrace -Operation Error + Write-Error ('Exception: ' + $_.Exception.Message) exit 1 } } Default { - 'Can not find implementation of type: ' + $cachedDscResourceInfo.ImplementationDetail | Write-DscTrace + Write-Error ('Can not find implementation of type: ' + $cachedDscResourceInfo.ImplementationDetail) exit 1 } } @@ -546,7 +535,7 @@ function Invoke-DscOperation { return $addToActualState } else { $dsJSON = $DesiredState | ConvertTo-Json -Depth 10 - 'Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.' | Write-DscTrace -Operation Error + Write-Error ('Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.') exit 1 } } @@ -583,7 +572,7 @@ function ValidateMethod { } if ($null -eq $method) { - "Method '$operation' not implemented by resource '$($t.Name)'" | Write-DscTrace -Operation Error + Write-Error ("Method '$operation' not implemented by resource '$($t.Name)'") exit 1 } @@ -612,7 +601,7 @@ function GetClassBasedProperties { [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($module.Path, [ref]$tokens, [ref]$errors) foreach ($e in $errors) { - $e | Out-String | Write-DscTrace -Operation Warn + $e | Out-String | Write-Warning } $typeDefinitions = $ast.FindAll( @@ -676,7 +665,7 @@ function GetClassBasedCapabilities { [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($module, [ref]$tokens, [ref]$errors) foreach ($e in $errors) { - $e | Out-String | Write-DscTrace -Operation Error + $e | Out-String | Write-Error } $typeDefinitions = $ast.FindAll( @@ -720,7 +709,7 @@ function Repair-ValidPSModulePath { end { if (($env:PSModulePath -split [System.IO.Path]::PathSeparator) -contains '') { - "Removing empty entry from PSModulePath: '$env:PSModulePath'" | Write-DscTrace -Operation Debug + Write-Debug -Debug "Removing empty entry from PSModulePath: '$env:PSModulePath'" $env:PSModulePath = [String]::Join([System.IO.Path]::PathSeparator, ($env:PSModulePath.Split([System.IO.Path]::PathSeparator, [System.StringSplitOptions]::RemoveEmptyEntries))).TrimEnd([System.IO.Path]::PathSeparator) } } diff --git a/dsc/examples/powershell.dsc.yaml b/dsc/examples/powershell.dsc.yaml index 3bb47d62d..1706a6da7 100644 --- a/dsc/examples/powershell.dsc.yaml +++ b/dsc/examples/powershell.dsc.yaml @@ -4,18 +4,14 @@ metadata: Microsoft.DSC: securityContext: elevated resources: -- name: Use class PowerShell resources - type: Microsoft.Windows/WindowsPowerShell +- name: OpenSSH service + type: PsDesiredStateConfiguration/Service properties: - resources: - - name: OpenSSH service - type: PsDesiredStateConfiguration/Service - properties: - Name: sshd - - name: Administrator - type: PsDesiredStateConfiguration/User - properties: - UserName: administrator + Name: sshd +- name: Administrator + type: PsDesiredStateConfiguration/User + properties: + UserName: administrator - name: current user registry type: Microsoft.Windows/Registry properties: