From 314c51fa78ddb2a7a73be059a3b9485482159b39 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 30 Oct 2025 17:18:44 -0700 Subject: [PATCH 01/10] Initial work for new psadapter --- ...WindowsPowerShell_single.dsc.resource.json | 114 ++++++++++++++++++ .../powershell_single.dsc.resource.json | 114 ++++++++++++++++++ adapters/powershell_single/psadapter.ps1 | 25 ++++ .../powershell_single/psadapter_helpers.psm1 | 105 ++++++++++++++++ 4 files changed, 358 insertions(+) create mode 100644 adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json create mode 100644 adapters/powershell_single/powershell_single.dsc.resource.json create mode 100644 adapters/powershell_single/psadapter.ps1 create mode 100644 adapters/powershell_single/psadapter_helpers.psm1 diff --git a/adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json b/adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json new file mode 100644 index 000000000..84af06c20 --- /dev/null +++ b/adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Adapter/WindowsPowerShell", + "version": "0.1.0", + "kind": "adapter", + "description": "Resource adapter to classic DSC Powershell resources.", + "tags": [ + "PowerShell" + ], + "adapter": { + "list": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "./psadapter.ps1 List" + ] + }, + "inputKind": "single" + }, + "get": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Get", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin" + }, + "set": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Set", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "implementsPretest": true + }, + "test": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Test", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "export": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Export", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "validate": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Validate" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error" + } +} diff --git a/adapters/powershell_single/powershell_single.dsc.resource.json b/adapters/powershell_single/powershell_single.dsc.resource.json new file mode 100644 index 000000000..328850920 --- /dev/null +++ b/adapters/powershell_single/powershell_single.dsc.resource.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Adapter/PowerShell", + "version": "0.1.0", + "kind": "adapter", + "description": "Resource adapter to classic DSC Powershell resources.", + "tags": [ + "PowerShell" + ], + "adapter": { + "list": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "./psadapter.ps1 List" + ] + }, + "inputKind": "single" + }, + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Get", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Set", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "implementsPretest": true + }, + "test": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Test", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Export", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "validate": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psadapter.ps1", + "Validate" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error" + } +} \ No newline at end of file diff --git a/adapters/powershell_single/psadapter.ps1 b/adapters/powershell_single/psadapter.ps1 new file mode 100644 index 000000000..b1841863d --- /dev/null +++ b/adapters/powershell_single/psadapter.ps1 @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +using module ./psadapter_helpers.psm1 + +[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, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] + [string]$jsonInput = '@{}', + [Parameter(Mandatory = $true)] + [string]$ResourceType +) + +switch ($Operation) { + 'List' { + # TODO: Implement List operation + }, + { @('Get','Set','Test','Export') -contains $_ } { + + } + +} diff --git a/adapters/powershell_single/psadapter_helpers.psm1 b/adapters/powershell_single/psadapter_helpers.psm1 new file mode 100644 index 000000000..e2f97e418 --- /dev/null +++ b/adapters/powershell_single/psadapter_helpers.psm1 @@ -0,0 +1,105 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Write-DscTrace { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] + [string]$Level, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Message, + [switch]$Now + ) + + $trace = @{$Level.ToLower() = $Message } | ConvertTo-Json -Compress + + if ($Now) { + $host.ui.WriteErrorLine($trace) + } else { + $traceQueue.Enqueue($trace) + } +} + +function Invoke-Script { + param( + [Parameter(Mandatory = $true)] + [string]$Script + ) + + $ps = [PowerShell]::Create().AddScript({ + $DebugPreference = 'Continue' + $VerbosePreference = 'Continue' + $ErrorActionPreference = 'Stop' + }).AddStatement().AddScript($script) + + $traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() + + $null = Register-ObjectEvent -InputObject $ps.Streams.Error -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + # convert error to string since it's an ErrorRecord + $traceQueue.Enqueue((@{ error = [string]$EventArgs.ItemAdded } | ConvertTo-Json -Compress)) + } + $null = Register-ObjectEvent -InputObject $ps.Streams.Warning -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + $traceQueue.Enqueue((@{ warn = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress)) + } + $null = Register-ObjectEvent -InputObject $ps.Streams.Information -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + if ($null -ne $EventArgs.ItemAdded.MessageData) { + if ($EventArgs.ItemAdded.Tags -contains 'PSHOST') { + $traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress)) + } else { + $traceQueue.Enqueue((@{ trace = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress)) + } + return + } + } + $null = Register-ObjectEvent -InputObject $ps.Streams.Verbose -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + $traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress)) + } + $null = Register-ObjectEvent -InputObject $ps.Streams.Debug -EventName DataAdding -MessageData $traceQueue -Action { + $traceQueue = $Event.MessageData + $traceQueue.Enqueue((@{ debug = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress)) + } + $outputObjects = [System.Collections.Generic.List[Object]]::new() + + function Write-TraceQueue() { + $trace = $null + while (!$traceQueue.IsEmpty) { + if ($traceQueue.TryDequeue([ref] $trace)) { + $host.ui.WriteErrorLine($trace) + } + } + } + + try { + $asyncResult = $ps.BeginInvoke() + while (-not $asyncResult.IsCompleted) { + Write-TraceQueue + + Start-Sleep -Milliseconds 100 + } + $outputCollection = $ps.EndInvoke($asyncResult) + Write-TraceQueue + + + if ($ps.HadErrors) { + # If there are any errors, we will exit with an error code + Write-DscTrace -Now -Level Error -Message 'Errors occurred during script execution.' + exit 1 + } + + foreach ($output in $outputCollection) { + $outputObjects.Add($output) + } + } + catch { + Write-DscTrace -Now -Level Error -Message $_ + exit 1 + } + finally { + $ps.Dispose() + Get-EventSubscriber | Unregister-Event + } +} From f4d235837b7d770329acb0d55c372a45eedcac38 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 3 Nov 2025 14:48:08 -0800 Subject: [PATCH 02/10] add list support --- adapters/powershell_single/.project.data.json | 14 + .../powershell_single.dsc.resource.json | 5 +- adapters/powershell_single/psadapter.ps1 | 126 +++- .../powershell_single/psadapter_helpers.psm1 | 587 +++++++++++++++++- lib/dsc-lib/locales/en-us.toml | 4 +- 5 files changed, 715 insertions(+), 21 deletions(-) create mode 100644 adapters/powershell_single/.project.data.json diff --git a/adapters/powershell_single/.project.data.json b/adapters/powershell_single/.project.data.json new file mode 100644 index 000000000..91c6ec65d --- /dev/null +++ b/adapters/powershell_single/.project.data.json @@ -0,0 +1,14 @@ +{ + "Name": "powershell-single-adapter", + "Kind": "Adapter", + "CopyFiles": { + "All": [ + "powershell_single.dsc.resource.json", + "psadapter.ps1", + "psadapter_helpers.psm1" + ], + "Windows": [ + "WindowsPowershell_single.dsc.resource.json" + ] + } +} diff --git a/adapters/powershell_single/powershell_single.dsc.resource.json b/adapters/powershell_single/powershell_single.dsc.resource.json index 328850920..5d390eb00 100644 --- a/adapters/powershell_single/powershell_single.dsc.resource.json +++ b/adapters/powershell_single/powershell_single.dsc.resource.json @@ -3,7 +3,7 @@ "type": "Microsoft.Adapter/PowerShell", "version": "0.1.0", "kind": "adapter", - "description": "Resource adapter to classic DSC Powershell resources.", + "description": "Resource adapter to PowerShell class based DSC resources.", "tags": [ "PowerShell" ], @@ -17,7 +17,8 @@ "-ExecutionPolicy", "Bypass", "-Command", - "./psadapter.ps1 List" + "./psadapter.ps1", + "List" ] }, "inputKind": "single" diff --git a/adapters/powershell_single/psadapter.ps1 b/adapters/powershell_single/psadapter.ps1 index b1841863d..8b51a5656 100644 --- a/adapters/powershell_single/psadapter.ps1 +++ b/adapters/powershell_single/psadapter.ps1 @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -using module ./psadapter_helpers.psm1 - [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate, ClearCache.')] @@ -10,16 +8,132 @@ param( [string]$Operation, [Parameter(Mandatory = $false, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] [string]$jsonInput = '@{}', - [Parameter(Mandatory = $true)] - [string]$ResourceType + [Parameter(Mandatory = $false)] + [string]$ResourceType, + [Parameter(Mandatory = $false)] + [string[]]$ResourcePath ) +Import-Module -Name "$PSScriptRoot/psadapter_helpers.psm1" -Force + switch ($Operation) { 'List' { - # TODO: Implement List operation - }, + $dscResourceCache = Invoke-DscCacheRefresh + + # cache was refreshed on script load + foreach ($dscResource in $dscResourceCache.Values) { + + # 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 = '' + } + + # match adapter to version of powershell + if ($PSVersionTable.PSVersion.Major -le 5) { + $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + } + else { + $requireAdapter = 'Microsoft.DSC/PowerShell' + } + + # 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 = $DscResourceInfo.Properties.Name + requireAdapter = $requireAdapter + description = $description + } | ConvertTo-Json -Compress + } + } { @('Get','Set','Test','Export') -contains $_ } { + $ds = $jsonInput | ConvertFrom-Json + + # if ResourcePath is provided, we load that module + if ($ResourcePath) { + $module = Import-Module -Name $ResourcePath -Force -ErrorAction Stop -PassThru + } else { + # refresh the cache with the modules that are available on the system + $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules + if (!$dscResourceCache.ContainsKey($ResourceType)) { + Write-DscTrace -Level Error -Message "DSC resource type '$ResourceType' not found." + exit 1 + } + } + + # 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-DscTrace -Level Error -Message 'Incomplete GET for resource ' + $ds.Name + exit 1 + } + if ($null -ne $actualState.Properties -and $actualState.Properties.InDesiredState -eq $false) { + $inDesiredState = $false + } + # 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 + } + Write-DscTrace -Level Debug -Message "jsonOutput=$result" + return $result } + 'Validate' { + # VALIDATE not implemented + + # OUTPUT + @{ valid = $true } | ConvertTo-Json + } + Default { + Write-DscTrace -Level Error -Message 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + } +} +# 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 } diff --git a/adapters/powershell_single/psadapter_helpers.psm1 b/adapters/powershell_single/psadapter_helpers.psm1 index e2f97e418..dd972dff5 100644 --- a/adapters/powershell_single/psadapter_helpers.psm1 +++ b/adapters/powershell_single/psadapter_helpers.psm1 @@ -1,23 +1,20 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +$script:CurrentCacheSchemaVersion = 4 +$script:CacheFileName = "PSAdapterCache_v$script:CurrentCacheSchemaVersion.json" + function Write-DscTrace { param( [Parameter(Mandatory = $true)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] [string]$Level, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string]$Message, - [switch]$Now + [Parameter(Mandatory = $true)] + [string]$Message ) $trace = @{$Level.ToLower() = $Message } | ConvertTo-Json -Compress - - if ($Now) { - $host.ui.WriteErrorLine($trace) - } else { - $traceQueue.Enqueue($trace) - } + $host.ui.WriteErrorLine($trace) } function Invoke-Script { @@ -86,7 +83,7 @@ function Invoke-Script { if ($ps.HadErrors) { # If there are any errors, we will exit with an error code - Write-DscTrace -Now -Level Error -Message 'Errors occurred during script execution.' + Write-DscTrace -Level Error -Message 'Errors occurred during script execution.' exit 1 } @@ -95,7 +92,7 @@ function Invoke-Script { } } catch { - Write-DscTrace -Now -Level Error -Message $_ + Write-DscTrace -Level Error -Message $_ exit 1 } finally { @@ -103,3 +100,571 @@ function Invoke-Script { Get-EventSubscriber | Unregister-Event } } + +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)) { + 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) { + $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' + if ($null -ne $containsDSCResource) { + $dscModulePsd1List.Add($psd1) | Out-Null + } + } + } + } + + return $dscModulePsd1List +} + +function Add-AstMembers { + param( + $AllTypeDefinitions, + $TypeAst, + $Properties + ) + + foreach ($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } + if ($t) { + Add-AstMembers $AllTypeDefinitions $t $Properties + } + } + + foreach ($member in $TypeAst.Members) { + $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] + if (($null -eq $property) -or ($property.IsStatic)) { + continue; + } + $skipProperty = $true + $isKeyProperty = $false + foreach ($attr in $property.Attributes) { + if ($attr.TypeName.Name -eq 'DscProperty') { + $skipProperty = $false + foreach ($attrArg in $attr.NamedArguments) { + if ($attrArg.ArgumentName -eq 'Key') { + $isKeyProperty = $true + break + } + } + } + } + if ($skipProperty) { + continue; + } + + [DscResourcePropertyInfo]$prop = [DscResourcePropertyInfo]::new() + $prop.Name = $property.Name + $prop.PropertyType = $property.PropertyType.TypeName.Name + $prop.IsMandatory = $isKeyProperty + $Properties.Add($prop) + } +} + +function FindAndParseResourceDefinitions { + [CmdletBinding(HelpUri = '')] + param( + [Parameter(Mandatory = $true)] + [string]$filePath, + [Parameter(Mandatory = $true)] + [string]$moduleVersion + ) + + if (-not (Test-Path $filePath)) { + return + } + + if (".psm1", ".ps1" -notcontains ([System.IO.Path]::GetExtension($filePath))) { + return + } + + Write-DscTrace -Level Trace -Message "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) { + Write-DscTrace -Level Error -Message ($e | Out-String) + } + + $typeDefinitions = $ast.FindAll( + { + $typeAst = $args[0] -as [System.Management.Automation.Language.TypeDefinitionAst] + return $null -ne $typeAst; + }, + $false); + + $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() + + foreach ($typeDefinitionAst in $typeDefinitions) { + foreach ($a in $typeDefinitionAst.Attributes) { + if ($a.TypeName.Name -eq 'DscResource') { + $DscResourceInfo = [DscResourceInfo]::new() + $DscResourceInfo.Name = $typeDefinitionAst.Name + $DscResourceInfo.ResourceType = $typeDefinitionAst.Name + $DscResourceInfo.FriendlyName = $typeDefinitionAst.Name + $DscResourceInfo.ImplementationDetail = 'ClassBased' + $DscResourceInfo.Module = $filePath + $DscResourceInfo.Path = $filePath + #TODO: ModuleName, Version and ParentPath should be taken from psd1 contents + $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) + $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) + $DscResourceInfo.Version = $moduleVersion + + $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() + $DscResourceInfo.Capabilities = GetClassBasedCapabilities $typeDefinitionAst.Members + Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties + + $resourceList.Add($DscResourceInfo) + } + } + } + + return $resourceList +} + +function LoadPowerShellClassResourcesFromModule { + [CmdletBinding(HelpUri = '')] + param( + [Parameter(Mandatory = $true)] + [PSModuleInfo]$moduleInfo + ) + + Write-DscTrace -Level Trace -Message "Loading resources from module '$($moduleInfo.Path)'" + + if ($moduleInfo.RootModule) { + if (".psm1", ".ps1" -notcontains ([System.IO.Path]::GetExtension($moduleInfo.RootModule)) -and + (-not $moduleInfo.NestedModules)) { + Write-DscTrace -Level Trace -Message "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" + return [System.Collections.Generic.List[DscResourceInfo]]::new() + } + + $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule + } + else { + $scriptPath = $moduleInfo.Path; + } + + $version = if ($moduleInfo.Version) { $moduleInfo.Version.ToString() } else { '0.0.0' } + $Resources = FindAndParseResourceDefinitions $scriptPath $version + + if ($moduleInfo.NestedModules) { + foreach ($nestedModule in $moduleInfo.NestedModules) { + $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule + if ($resourcesOfNestedModules) { + $Resources.AddRange($resourcesOfNestedModules) + } + } + } + + return $Resources +} + +<# public function Invoke-DscCacheRefresh +.SYNOPSIS + This function caches the results of the Get-DscResource call to optimize performance. + +.DESCRIPTION + This function is designed to improve the performance of DSC operations by caching the results of the Get-DscResource call. + By storing the results, subsequent calls to Get-DscResource can retrieve the cached data instead of making a new call each time. + This can significantly speed up operations that need to repeatedly access DSC resources. + +.EXAMPLE + Invoke-DscCacheRefresh -Module "PSDesiredStateConfiguration" +#> +function Invoke-DscCacheRefresh { + [CmdletBinding(HelpUri = '')] + param( + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Object[]] + $Module + ) + + $refreshCache = $false + + $cacheFilePath = if ($IsWindows) { + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\$script:CacheFileName" + } + else { + # PS 6+ on Linux/Mac + Join-Path $env:HOME ".dsc" $script:CacheFileName + } + + if (Test-Path $cacheFilePath) { + Write-DscTrace -Level Debug -Message "Reading from cache file $cacheFilePath" + + $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json -Depth 10 + + if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { + $refreshCache = $true + Write-DscTrace -Level Warn -Message "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" + } + else { + $dscResourceCacheEntries = $cache.ResourceCache + + if ($null -eq $dscResourceCacheEntries -or $dscResourceCacheEntries.Keys.Count -eq 0) { + # if there is nothing in the cache file - refresh cache + $refreshCache = $true + + Write-DscTrace -Level Debug -Message "Filtered DscResourceCache cache is empty" + } + else { + Write-DscTrace -Level Debug -Message "Checking cache for stale entries" + + foreach ($cacheEntry in $dscResourceCacheEntries.Values) { + + $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { + + if (Test-Path $_.Name) { + $file_LastWriteTime = (Get-Item $_.Name).LastWriteTime + # Truncate DateTime to seconds + $file_LastWriteTime = $file_LastWriteTime.AddTicks( - ($file_LastWriteTime.Ticks % [TimeSpan]::TicksPerSecond)); + + $cache_LastWriteTime = [DateTime]$_.Value + # Truncate DateTime to seconds + $cache_LastWriteTime = $cache_LastWriteTime.AddTicks( - ($cache_LastWriteTime.Ticks % [TimeSpan]::TicksPerSecond)); + + if (-not ($file_LastWriteTime.Equals($cache_LastWriteTime))) { + Write-DscTrace -Level Debug -Message "Detected stale cache entry '$($_.Name)'" + $refreshCache = $true + break + } + } + else { + Write-DscTrace -Level Debug -Message "Detected non-existent cache entry '$($_.Name)'" + $refreshCache = $true + break + } + } + + if ($refreshCache) { break } + } + + if (-not $refreshCache) { + Write-DscTrace -Level Debug -Message "Checking cache for stale PSModulePath" + + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + + $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 + + Write-DscTrace -Level Debug -Message "PSModulePath diff '$diff'" + + if ($diff.Count -gt 0) { + $refreshCache = $true + } + } + } + } + } + else { + Write-DscTrace -Level Debug -Message "Cache file not found '$cacheFilePath'" + $refreshCache = $true + } + + if ($refreshCache) { + Write-DscTrace -Level Debug -Message 'Constructing Get-DscResource cache' + + # create a list object to store cache of Get-DscResource + $dscResourceCacheEntries = @{} + + $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() + $dscResourceModulePsd1s = Get-DSCResourceModules + if ($null -ne $dscResourceModulePsd1s) { + $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) + $processedModuleNames = @{} + foreach ($mod in $modules) { + if (-not ($processedModuleNames.ContainsKey($mod.Name))) { + $processedModuleNames.Add($mod.Name, $true) + + # 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) { + Write-DscTrace -Level Trace -Message "Found $($selectedMod.Count) modules with name '$($mod.Name)'" + $selectedMod = $selectedMod | Sort-Object -Property Version -Descending | Select-Object -First 1 + } + + [System.Collections.Generic.List[DscResourceInfo]]$r = LoadPowerShellClassResourcesFromModule -moduleInfo $selectedMod + if ($r) { + $DscResources.AddRange($r) + } + } + } + } + + foreach ($dscResource in $DscResources) { + $moduleName = $dscResource.ModuleName + + # 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 { + $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) + } + + $type = "$moduleName/$($dscResource.Name)" + $dscResourceCacheEntries += @{ + $type = [dscResourceCacheEntry]@{ + Type = $type + DscResourceInfo = $dscResource + LastWriteTimes = $lastWriteTimes + } + } + } + + [dscResourceCache]$cache = [dscResourceCache]::new() + $cache.ResourceCache = $dscResourceCacheEntries + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $cache.PSModulePaths = $m.FullName + $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion + + # save cache for future use + # TODO: replace this with a high-performance serializer + Write-DscTrace -Level Debug -Message "Saving Get-DscResource cache to '$cacheFilePath'" + $jsonCache = $cache | ConvertTo-Json -Depth 90 -Compress + New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null + } + + return $dscResourceCacheEntries +} + +# Get the actual state using DSC Get method from any type of DSC resource +function Invoke-DscOperation { + param( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Set', 'Test', 'Export')] + [string]$Operation, + [Parameter(Mandatory, ValueFromPipeline = $true)] + [dscResourceObject]$DesiredState, + [Parameter(Mandatory)] + [dscResourceCacheEntry[]]$dscResourceCache + ) + + $osVersion = [System.Environment]::OSVersion.VersionString + Write-DscTrace -Level Debug -Message 'OS version: ' + $osVersion + + $psVersion = $PSVersionTable.PSVersion.ToString() + Write-DscTrace -Level Debug -Message '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 + + # if the resource is found in the cache, get the actual state + if ($cachedDscResourceInfo) { + + # formated OUTPUT of each resource + $addToActualState = [dscResourceObject]@{} + + # set top level properties of the OUTPUT object from INPUT object + $DesiredState.psobject.properties | ForEach-Object -Process { + if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) } + } + + # 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) { + + 'ClassBased' { + try { + # load powershell class from external module + $resource = GetTypeInstanceFromModule -modulename $cachedDscResourceInfo.ModuleName -classname $cachedDscResourceInfo.Name + $dscResourceInstance = $resource::New() + + $ValidProperties = $cachedDscResourceInfo.Properties.Name + + Write-DscTrace -Level Trace -Message ($ValidProperties | ConvertTo-Json -Compress) + + if ($DesiredState.properties) { + # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object + $DesiredState.properties.psobject.properties | ForEach-Object -Process { + # handle input objects by converting them to a hash table + $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name + 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) { + Write-DscTrace -Level Error -Message "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)) + } + else { + $dscResourceInstance.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } + } + } + else { + if ($validateProperty -and $validateProperty.PropertyType -in @('SecureString', 'System.Security.SecureString') -and -not [string]::IsNullOrEmpty($_.Value)) { + $dscResourceInstance.$($_.Name) = ConvertTo-SecureString -AsPlainText $_.Value -Force + } else { + $dscResourceInstance.$($_.Name) = $_.Value + } + } + } + } + + switch ($Operation) { + 'Get' { + $Result = @{} + $raw_obj = $dscResourceInstance.Get() + $ValidProperties | ForEach-Object { + if ($raw_obj.$_ -is [System.Enum]) { + $Result[$_] = $raw_obj.$_.ToString() + + } + else { + $Result[$_] = $raw_obj.$_ + } + } + $addToActualState.properties = $Result + } + 'Set' { + $dscResourceInstance.Set() + } + 'Test' { + $Result = $dscResourceInstance.Test() + $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } + } + 'Export' { + $t = $dscResourceInstance.GetType() + $methods = $t.GetMethods() | Where-Object { $_.Name -eq 'Export' } + $method = foreach ($mt in $methods) { + if ($mt.GetParameters().Count -eq 0) { + $mt + break + } + } + + if ($null -eq $method) { + Write-DscTrace -Level Error -Message "Export method not implemented by resource '$($DesiredState.Type)'" + exit 1 + } + $resultArray = @() + $raw_obj_array = $method.Invoke($null, $null) + foreach ($raw_obj in $raw_obj_array) { + $Result_obj = @{} + $ValidProperties | ForEach-Object { + if ($raw_obj.$_ -is [System.Enum]) { + $Result_obj[$_] = $raw_obj.$_.ToString() + } + else { + $Result_obj[$_] = $raw_obj.$_ + } + } + $resultArray += $Result_obj + } + $addToActualState = $resultArray + } + } + } + catch { + Write-DscTrace -Level Error -Message "Exception: $($_ | Out-String)" + exit 1 + } + } + Default { + Write-DscTrace -Level Error -Message "Resource ImplementationDetail not supported: $($cachedDscResourceInfo.ImplementationDetail)" + exit 1 + } + } + + Write-DscTrace -Level Trace -Message "Output: $($addToActualState | ConvertTo-Json -Depth 10 -Compress)" + return $addToActualState + } + else { + $dsJSON = $DesiredState | ConvertTo-Json -Depth 10 + Write-DscTrace -Level Error -Message "Can not find type '$($DesiredState.type)' for resource '$dsJSON'. Please ensure that Get-DscResource returns this resource type." + exit 1 + } +} + +# GetTypeInstanceFromModule function to get the type instance from the module +function GetTypeInstanceFromModule { + param( + [Parameter(Mandatory = $true)] + [string] $modulename, + [Parameter(Mandatory = $true)] + [string] $classname + ) + $instance = & (Import-Module $modulename -PassThru) ([scriptblock]::Create("'$classname' -as 'type'")) + return $instance +} + +function GetClassBasedCapabilities ($functionMemberAst) { + $capabilities = [System.Collections.Generic.List[string[]]]::new() + # These are the methods that we can potentially expect in a class-based DSC resource. + $availableMethods = @('get', 'set', 'setHandlesExist', 'whatIf', 'test', 'delete', 'export') + $methods = $functionMemberAst | Where-Object { $_ -is [System.Management.Automation.Language.FunctionMemberAst] -and $_.Name -in $availableMethods } + + foreach ($method in $methods.Name) { + # We go through each method to properly case handle the method names. + switch ($method) { + 'Get' { $capabilities.Add('get') } + 'Set' { $capabilities.Add('set') } + 'Test' { $capabilities.Add('test') } + 'WhatIf' { $capabilities.Add('whatIf') } + 'SetHandlesExist' { $capabilities.Add('setHandlesExist') } + 'Delete' { $capabilities.Add('delete') } + 'Export' { $capabilities.Add('export') } + } + } + + return ($capabilities | Select-Object -Unique) +} + +# cached resource +class dscResourceCacheEntry { + [string] $Type + [psobject] $DscResourceInfo + [PSCustomObject] $LastWriteTimes +} + +class dscResourceCache { + [int] $CacheSchemaVersion + [string[]] $PSModulePaths + [System.Collections.Hashtable] $ResourceCache +} + +# format expected for configuration output +class dscResourceObject { + [string] $name + [string] $type + [psobject] $properties +} + +# dsc resource types +enum dscResourceType { + ScriptBased + ClassBased + Binary + Composite +} + +class DscResourcePropertyInfo { + [string] $Name + [string] $PropertyType + [bool] $IsMandatory + [System.Collections.Generic.List[string]] $Values +} + +# dsc resource type (settable clone) +class DscResourceInfo { + [dscResourceType] $ImplementationDetail + [string] $ResourceType + [string] $Name + [string] $FriendlyName + [string] $Module + [string] $ModuleName + [string] $Version + [string] $Path + [string] $ParentPath + [string] $ImplementedAs + [string] $CompanyName + [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties + [string[]] $Capabilities +} diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index d900998e7..dce048834 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -109,8 +109,8 @@ adapterFound = "Resource adapter '%{adapter}' version %{version} found" resourceFound = "Resource '%{resource}' version %{version} found" executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'" extensionInvalidVersion = "Extension '%{extension}' version '%{version}' is invalid" -invalidResourceManifest = "Invalid manifest for resource '%{resource}'" -invalidExtensionManifest = "Invalid manifest for extension '%{extension}'" +invalidResourceManifest = "Invalid manifest for resource '%{resource}': %{err}" +invalidExtensionManifest = "Invalid manifest for extension '%{extension}': %{err}" invalidManifestList = "Invalid manifest list '%{resource}': %{err}" invalidManifestFile = "Invalid manifest file '%{resource}': %{err}" extensionResourceFound = "Extension found resource '%{resource}'" From 353dbab5fbea77c85ee44dacd7a2c3b15c3f062d Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 5 Nov 2025 15:26:38 -0800 Subject: [PATCH 03/10] add tests --- .../0.0.1/PSTestClassResource.psd1 | 25 ++ .../0.0.1/PSTestClassResource.psm1 | 161 ++++++++++ .../Tests/psadapter.tests.ps1 | 301 ++++++++++++++++++ adapters/powershell_single/psadapter.ps1 | 6 +- .../powershell_single/psadapter_helpers.psm1 | 34 +- 5 files changed, 510 insertions(+), 17 deletions(-) create mode 100644 adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 create mode 100644 adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 create mode 100644 adapters/powershell_single/Tests/psadapter.tests.ps1 diff --git a/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 b/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 new file mode 100644 index 000000000..95ddbc0d8 --- /dev/null +++ b/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + RootModule = 'PSTestClassResource.psm1' + ModuleVersion = '0.0.1' + GUID = 'b267fa32-e77d-48e6-9248-676cc6f2327e' + Author = 'Microsoft' + CompanyName = 'Microsoft Corporation' + Copyright = '(c) Microsoft. All rights reserved.' + FunctionsToExport = @() + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + DscResourcesToExport = @('PSTestClassResource', 'PSNoExport') + PrivateData = @{ + PSData = @{ + DscCapabilities = @( + 'get' + 'test' + ) + } + } +} + diff --git a/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 b/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 new file mode 100644 index 000000000..47555a92d --- /dev/null +++ b/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +using namespace System.Collections.Generic + +enum EnumPropEnumeration { + Unexpected + Expected +} + +enum Ensure { + Present + Absent +} + +class BaseTestClass +{ + [DscProperty()] + [string] $BaseProperty +} + +[DscResource()] +class PSTestClassResource : BaseTestClass +{ + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Prop1 + + [DscProperty()] + [hashtable] $HashTableProp + + [DscProperty()] + [string] $EnumProp + + [DscProperty()] + [PSCredential] $Credential + + [DscProperty()] + [Ensure] $Ensure + + [DscProperty()] + [SecureString] $SecureStringProp + + [string] $NonDscProperty # This property shouldn't be in results data + + hidden + [string] $HiddenNonDscProperty # This property shouldn't be in results data + + hidden + [DscProperty()] + [string] $HiddenDscProperty # This property should be in results data, but is an anti-pattern. + + [void] Set() + { + } + + [bool] Test() + { + if (($this.Name -eq "PSTestClassResource1") -and ($this.Prop1 -eq "ValueForProp1")) + { + return $true + } + else + { + return $false + } + } + + [PSTestClassResource] Get() + { + if ($this.Name -eq "PSTestClassResource1") + { + $this.Prop1 = "ValueForProp1" + } + elseif ($this.Name -eq 'EchoBack') + { + # don't change the property, just echo it back + } + else + { + $this.Prop1 = $env:DSC_CONFIG_ROOT + } + $this.EnumProp = ([EnumPropEnumeration]::Expected).ToString() + return $this + } + + static [PSTestClassResource[]] Export() + { + $resultList = [List[PSTestClassResource]]::new() + $resultCount = 5 + if ($env:PSTestClassResourceResultCount) { + $resultCount = $env:PSTestClassResourceResultCount + } + 1..$resultCount | %{ + $obj = New-Object PSTestClassResource + $obj.Name = "Object$_" + $obj.Prop1 = "Property of object$_" + $resultList.Add($obj) + } + + return $resultList.ToArray() + } + + static [PSTestClassResource[]] Export([bool]$UseExport) + { + if ($UseExport) + { + return [PSTestClassResource]::Export() + } + else + { + $resultList = [List[PSTestClassResource]]::new() + $resultCount = 5 + if ($env:PSTestClassResourceResultCount) { + $resultCount = $env:PSTestClassResourceResultCount + } + 1..$resultCount | %{ + $obj = New-Object PSTestClassResource + $obj.Name = "Object$_" + $obj.Prop1 = "Property of object$_" + $resultList.Add($obj) + } + } + + return $resultList.ToArray() + } +} + +[DscResource()] +class PSNoExport: BaseTestClass +{ + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Prop1 + + [DscProperty()] + [string] $EnumProp + + [void] Set() + { + } + + [bool] Test() + { + return $true + } + + [PSNoExport] Get() + { + return $this + } +} + +function Test-World() +{ + "Hello world from PSTestModule!" +} diff --git a/adapters/powershell_single/Tests/psadapter.tests.ps1 b/adapters/powershell_single/Tests/psadapter.tests.ps1 new file mode 100644 index 000000000..108d4e376 --- /dev/null +++ b/adapters/powershell_single/Tests/psadapter.tests.ps1 @@ -0,0 +1,301 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'PowerShell single adapter resource tests' { + + BeforeAll { + $OldPSModulePath = $env:PSModulePath + $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot + + if ($IsLinux -or $IsMacOS) { + $cacheFilePath = Join-Path $env:HOME ".dsc" "PSAdapterCache_v4.json" + } + else { + $cacheFilePath = Join-Path $env:LocalAppData "dsc" "PSAdapterCache_v4.json" + } + } + + AfterAll { + $env:PSModulePath = $OldPSModulePath + } + + BeforeEach { + Remove-Item -Force -ErrorAction Ignore -Path $cacheFilePath + } + + It 'Discovery includes class-based resources' { + + $r = dsc resource list '*' -a Microsoft.Adapter/PowerShell + $LASTEXITCODE | Should -Be 0 + $resources = $r | ConvertFrom-Json + ($resources | Where-Object { $_.Type -eq 'PSTestClassResource/PSTestClassResource' }).Count | Should -Be 1 + ($resources | Where-Object -Property type -EQ 'PSTestClassResource/PSTestClassResource').capabilities | Should -BeIn @('get', 'set', 'test', 'export') + ($resources | Where-Object -Property type -EQ 'PSTestClassResource/PSNoExport').capabilities | Should -BeIn @('get', 'set', 'test') + } + + It 'Get works on class-based resource' { + + $r = "{'Name':'PSTestClassResource1'}" | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.Prop1 | Should -BeExactly 'ValueForProp1' + + # verify that only properties with DscProperty attribute are returned + $propertiesNames = $res.actualState | Get-Member -MemberType NoteProperty | % Name + $propertiesNames | Should -Not -Contain 'NonDscProperty' + $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' + } + + It 'Get uses enum names on class-based resource' { + + $r = "{'Name':'PSTestClassResource1'}" | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.EnumProp | Should -BeExactly 'Expected' + } + + It 'Get should return the correct properties on class-based resource' { + $r = "{'Name':'PSTestClassResource1'}" | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json -AsHashtable + $res.actualState.ContainsKey('Name') | Should -Be $True + $res.actualState.ContainsKey('Prop1') | Should -Be $True + $res.actualState.ContainsKey('HashTableProp') | Should -Be $True + $res.actualState.ContainsKey('EnumProp') | Should -Be $True + $res.actualState.ContainsKey('Credential') | Should -Be $True + $res.actualState.ContainsKey('Ensure') | Should -Be $True + $res.actualState.ContainsKey('BaseProperty') | Should -Be $True + $res.actualState.ContainsKey('HiddenDscProperty') | Should -Be $True + $res.actualState.ContainsKey('NonDscProperty') | Should -Be $False + $res.actualState.ContainsKey('HiddenNonDscProperty') | Should -Be $False + } + + It 'Test works on class-based resource' { + + $r = "{'Name':'PSTestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.InDesiredState | Should -Be $True + $res.actualState.InDesiredState.GetType().Name | Should -Be "Boolean" + + # verify that only properties with DscProperty attribute are returned + $propertiesNames = $res.actualState.InDesiredState | Get-Member -MemberType NoteProperty | % Name + $propertiesNames | Should -Not -Contain 'NonDscProperty' + $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' + } + + It 'Set works on class-based resource' { + + $r = "{'Name':'PSTestClassResource1','Prop1':'ValueForProp1'}" | dsc resource set -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.afterState.Prop1 | Should -BeExactly 'ValueForProp1' + $res.changedProperties | Should -BeNullOrEmpty + } + + It 'Export works on PS class-based resource' -Pending { + + $r = dsc resource export -r PSTestClassResource/PSTestClassResource + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.resources[0].properties.result.count | Should -Be 5 + $res.resources[0].properties.result[0].Name | Should -Be "Object1" + $res.resources[0].properties.result[0].Prop1 | Should -Be "Property of object1" + + # verify that only properties with DscProperty attribute are returned + $res.resources[0].properties.result | % { + $propertiesNames = $_ | Get-Member -MemberType NoteProperty | % Name + $propertiesNames | Should -Not -Contain 'NonDscProperty' + $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' + } + } + + It 'Get --all works on PS class-based resource' -Pending { + + $r = dsc resource get --all -r PSTestClassResource/PSTestClassResource 2>$null + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.result.count | Should -Be 5 + $res.actualState.result | % { $_.Name | Should -Not -BeNullOrEmpty } + } + + It 'Verify that ClearCache works in PSAdapter' { + # generate the cache + $null = dsc resource list '*' -a Microsoft.Adapter/PowerShell + # call the ClearCache operation + $scriptPath = Join-Path $PSScriptRoot '..' 'psadapter.ps1' + $null = & $scriptPath -Operation ClearCache + # verify that PSAdapter does not find the cache + dsc -l debug resource list '*' -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Cache file not found' + } + + It 'Verify that a new PS Cache version results in cache rebuid' { + # generate the cache + $null = dsc resource list '*' -a Microsoft.Adapter/PowerShell + # update the version in the cache file + $cacheFilePath = if ($IsWindows) { + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache_v4.json" + } + else { + # either WinPS or PS 6+ on Linux/Mac + if ($PSVersionTable.PSVersion.Major -le 5) { + Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache_v4.json" + } + else { + Join-Path $env:HOME ".dsc" "PSAdapterCache_v4.json" + } + } + $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json + $cache.CacheSchemaVersion = 0 + $jsonCache = $cache | ConvertTo-Json -Depth 90 + New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null + + # verify that a new PS Cache version results in cache rebuid + dsc -l debug resource list '*' -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Incompatible version of cache in file' + } + + It 'Verify that removing a module results in cache rebuid' { + + Copy-Item -Recurse -Force -Path "$PSScriptRoot/PSTestClassResource" -Destination $TestDrive + Copy-Item -Recurse -Force -Path "$PSScriptRoot/PSTestClassResource" -Destination "$PSScriptRoot/Backup/PSTestClassResource" + Remove-Item -Recurse -Force -Path "$PSScriptRoot/PSTestClassResource" + + $oldPath = $env:PSModulePath + try { + $env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive + + # generate the cache + $null = dsc resource list '*' -a Microsoft.Adapter/PowerShell + # remove the module files + Remove-Item -Recurse -Force -Path "$TestDrive/PSTestClassResource" + # verify that cache rebuid happened + $null = dsc -l trace resource list '*' -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt + + $LASTEXITCODE | Should -Be 0 + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Detected non-existent cache entry' + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' + } + finally { + $env:PSModulePath = $oldPath + Copy-Item -Recurse -Force -Path "$PSScriptRoot/Backup/PSTestClassResource" -Destination "$PSScriptRoot" + Remove-Item -Recurse -Force -Path "$PSScriptRoot/Backup" + } + } + + It 'Verify inheritance works in class-based resources' { + + $r = dsc resource list '*' -a Microsoft.Adapter/PowerShell + $LASTEXITCODE | Should -Be 0 + $resources = $r | ConvertFrom-Json + $t = $resources | ? { $_.Type -eq 'PSTestClassResource/PSTestClassResource' } + $t.properties | Should -Contain "BaseProperty" + } + + It 'Verify highest module version is loaded' { + + $srcPath = Join-Path $PSScriptRoot 'PSTestClassResource' + $pathRoot1 = Join-Path $TestDrive 'A' + $pathRoot2 = Join-Path $TestDrive 'B' + $path1 = Join-Path $pathRoot1 'PSTestClassResource' '1.0' + $path2 = Join-Path $pathRoot1 'PSTestClassResource' '1.1' + $path3 = Join-Path $pathRoot2 'PSTestClassResource' '2.0' + $path4 = Join-Path $pathRoot2 'PSTestClassResource' '2.0.1' + + New-Item -ItemType Directory -Force -Path $path1 | Out-Null + New-Item -ItemType Directory -Force -Path $path2 | Out-Null + New-Item -ItemType Directory -Force -Path $path3 | Out-Null + New-Item -ItemType Directory -Force -Path $path4 | Out-Null + + $files = Get-ChildItem -Recurse -File -Path $srcPath + $files | Copy-Item -Destination $path1 + $files | Copy-Item -Destination $path2 + $files | Copy-Item -Destination $path3 + $files | Copy-Item -Destination $path4 + + $filePath = Join-Path $path1 'PSTestClassResource.psd1' + (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'1.0`'") | Set-Content $filePath + $filePath = Join-Path $path2 'PSTestClassResource.psd1' + (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'1.1`'") | Set-Content $filePath + $filePath = Join-Path $path3 'PSTestClassResource.psd1' + (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'2.0`'") | Set-Content $filePath + $filePath = Join-Path $path4 'PSTestClassResource.psd1' + (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'2.0.1`'") | Set-Content $filePath + + + $oldPath = $env:PSModulePath + try { + $env:PSModulePath += [System.IO.Path]::PathSeparator + $pathRoot1 + $env:PSModulePath += [System.IO.Path]::PathSeparator + $pathRoot2 + + $r = dsc resource list '*' -a Microsoft.Adapter/PowerShell + $LASTEXITCODE | Should -Be 0 + $resources = $r | ConvertFrom-Json + $r = @($resources | ? { $_.Type -eq 'PSTestClassResource/PSTestClassResource' }) + $r.Count | Should -Be 1 + $r[0].Version | Should -Be '2.0.1' + } + finally { + $env:PSModulePath = $oldPath + } + } + + It 'Verify that there are no cache rebuilds for several sequential executions' { + + # remove cache file + $cacheFilePath = if ($IsWindows) { + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache_v4.json" + } + else { + # either WinPS or PS 6+ on Linux/Mac + if ($PSVersionTable.PSVersion.Major -le 5) { + Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache_v4.json" + } + else { + Join-Path $env:HOME ".dsc" "PSAdapterCache_v4.json" + } + } + Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore + + # first execution should build the cache + dsc -l trace resource list -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' + + # next executions following shortly after should Not rebuild the cache + 1..3 | ForEach-Object { + dsc -l trace resource list -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' + } + } + + It 'Can process a key-value pair object' { + $r = '{"HashTableProp":{"Name":"DSCv3"},"Name":"PSTestClassResource1"}' | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.HashTableProp.Name | Should -Be 'DSCv3' + } + + It 'Specifying version works' { + $out = dsc resource get -r PSTestClassResource/PSTestClassResource --version 0.0.1 | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.actualState.Ensure | Should -BeExactly 'Present' + } + + It 'Specifying a non-existent version returns an error' { + $null = dsc resource get -r PSTestClassResource/PSTestClassResource --version 0.0.2 2> $TestDrive/error.log + $LASTEXITCODE | Should -Be 7 + Get-Content -Path $TestDrive/error.log | Should -Match 'Resource not found: PSTestClassResource/PSTestClassResource 0.0.2' + } + + It 'Can process SecureString property' { + $r = '{"Name":"PSTestClassResource1","SecureStringProp":"MySecretValue"}' | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.SecureStringProp | Should -Not -BeNullOrEmpty + } +} diff --git a/adapters/powershell_single/psadapter.ps1 b/adapters/powershell_single/psadapter.ps1 index 8b51a5656..0ed1930a4 100644 --- a/adapters/powershell_single/psadapter.ps1 +++ b/adapters/powershell_single/psadapter.ps1 @@ -17,6 +17,10 @@ param( Import-Module -Name "$PSScriptRoot/psadapter_helpers.psm1" -Force switch ($Operation) { + 'ClearCache' { + Remove-Item -Path (Get-CacheFilePath) -Force -ErrorAction Ignore + exit 0 + } 'List' { $dscResourceCache = Invoke-DscCacheRefresh @@ -95,7 +99,7 @@ switch ($Operation) { # 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-DscTrace -Level Error -Message 'Incomplete GET for resource ' + $ds.Name + Write-DscTrace -Level Error -Message "Incomplete GET for resource $($ds.Name)" exit 1 } if ($null -ne $actualState.Properties -and $actualState.Properties.InDesiredState -eq $false) { diff --git a/adapters/powershell_single/psadapter_helpers.psm1 b/adapters/powershell_single/psadapter_helpers.psm1 index dd972dff5..2d78ddef6 100644 --- a/adapters/powershell_single/psadapter_helpers.psm1 +++ b/adapters/powershell_single/psadapter_helpers.psm1 @@ -4,6 +4,16 @@ $script:CurrentCacheSchemaVersion = 4 $script:CacheFileName = "PSAdapterCache_v$script:CurrentCacheSchemaVersion.json" +function Get-CacheFilePath { + if ($IsWindows) { + # PS 6+ on Windows + return Join-Path $env:LocalAppData "dsc\$script:CacheFileName" + } else { + # PS 6+ on Linux/Mac + return Join-Path $env:HOME ".dsc" $script:CacheFileName + } +} + function Write-DscTrace { param( [Parameter(Mandatory = $true)] @@ -287,15 +297,7 @@ function Invoke-DscCacheRefresh { ) $refreshCache = $false - - $cacheFilePath = if ($IsWindows) { - # PS 6+ on Windows - Join-Path $env:LocalAppData "dsc\$script:CacheFileName" - } - else { - # PS 6+ on Linux/Mac - Join-Path $env:HOME ".dsc" $script:CacheFileName - } + $cacheFilePath = Get-CacheFilePath if (Test-Path $cacheFilePath) { Write-DscTrace -Level Debug -Message "Reading from cache file $cacheFilePath" @@ -304,7 +306,7 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - Write-DscTrace -Level Warn -Message "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" + Write-DscTrace -Level Warn -Message "Incompatible version of cache in file '$($cache.CacheSchemaVersion)' (expected '$($script:CurrentCacheSchemaVersion)')" } else { $dscResourceCacheEntries = $cache.ResourceCache @@ -350,7 +352,7 @@ function Invoke-DscCacheRefresh { if (-not $refreshCache) { Write-DscTrace -Level Debug -Message "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 SilentlyContinue } $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -372,7 +374,7 @@ function Invoke-DscCacheRefresh { } if ($refreshCache) { - Write-DscTrace -Level Debug -Message 'Constructing Get-DscResource cache' + Write-DscTrace -Level Info -Message 'Constructing Get-DscResource cache' # create a list object to store cache of Get-DscResource $dscResourceCacheEntries = @{} @@ -406,7 +408,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) } @@ -422,7 +424,7 @@ 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 @@ -449,10 +451,10 @@ function Invoke-DscOperation { ) $osVersion = [System.Environment]::OSVersion.VersionString - Write-DscTrace -Level Debug -Message 'OS version: ' + $osVersion + Write-DscTrace -Level Debug -Message "OS version: $osVersion" $psVersion = $PSVersionTable.PSVersion.ToString() - Write-DscTrace -Level Debug -Message 'PowerShell version: ' + $psVersion + Write-DscTrace -Level Debug -Message "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 From bdebe1bc7b752483fd9f28c7514a951b4be80250 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 20 Nov 2025 15:20:26 -0800 Subject: [PATCH 04/10] put changes into current adapter --- adapters/powershell/.project.data.json | 6 +- .../PowerShell_adapter.dsc.resource.json} | 23 +- ...ndowsPowerShell_adapter.dsc.resource.json} | 25 +- .../psDscAdapter/powershell.resource.ps1 | 44 +- .../powershell/psDscAdapter/psDscAdapter.psm1 | 25 +- .../psDscAdapter/win_psDscAdapter.psm1 | 28 +- adapters/powershell_single/.project.data.json | 14 - .../0.0.1/PSTestClassResource.psd1 | 25 - .../0.0.1/PSTestClassResource.psm1 | 161 ----- .../Tests/psadapter.tests.ps1 | 301 -------- adapters/powershell_single/psadapter.ps1 | 143 ---- .../powershell_single/psadapter_helpers.psm1 | 672 ------------------ 12 files changed, 107 insertions(+), 1360 deletions(-) rename adapters/{powershell_single/powershell_single.dsc.resource.json => powershell/PowerShell_adapter.dsc.resource.json} (80%) rename adapters/{powershell_single/WindowsPowerShell_single.dsc.resource.json => powershell/WindowsPowerShell_adapter.dsc.resource.json} (81%) delete mode 100644 adapters/powershell_single/.project.data.json delete mode 100644 adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 delete mode 100644 adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 delete mode 100644 adapters/powershell_single/Tests/psadapter.tests.ps1 delete mode 100644 adapters/powershell_single/psadapter.ps1 delete mode 100644 adapters/powershell_single/psadapter_helpers.psm1 diff --git a/adapters/powershell/.project.data.json b/adapters/powershell/.project.data.json index fc4a3a495..f12fc15c6 100644 --- a/adapters/powershell/.project.data.json +++ b/adapters/powershell/.project.data.json @@ -6,12 +6,14 @@ "psDscAdapter/powershell.resource.ps1", "psDscAdapter/psDscAdapter.psd1", "psDscAdapter/psDscAdapter.psm1", - "powershell.dsc.resource.json" + "powershell.dsc.resource.json", + "PowerShell_adapter.dsc.resource.json" ], "Windows": [ "psDscAdapter/win_psDscAdapter.psd1", "psDscAdapter/win_psDscAdapter.psm1", - "windowspowershell.dsc.resource.json" + "windowspowershell.dsc.resource.json", + "WindowsPowerShell_adapter.dsc.resource.json" ] } } \ No newline at end of file diff --git a/adapters/powershell_single/powershell_single.dsc.resource.json b/adapters/powershell/PowerShell_adapter.dsc.resource.json similarity index 80% rename from adapters/powershell_single/powershell_single.dsc.resource.json rename to adapters/powershell/PowerShell_adapter.dsc.resource.json index 5d390eb00..9b5204652 100644 --- a/adapters/powershell_single/powershell_single.dsc.resource.json +++ b/adapters/powershell/PowerShell_adapter.dsc.resource.json @@ -3,7 +3,7 @@ "type": "Microsoft.Adapter/PowerShell", "version": "0.1.0", "kind": "adapter", - "description": "Resource adapter to PowerShell class based DSC resources.", + "description": "Resource adapter to classic DSC Powershell resources.", "tags": [ "PowerShell" ], @@ -17,11 +17,13 @@ "-ExecutionPolicy", "Bypass", "-Command", - "./psadapter.ps1", - "List" + "./psDscAdapter/powershell.resource.ps1", + "List", + "-ResourceType", + "Single" ] }, - "inputKind": "single" + "config": "full" }, "get": { "executable": "pwsh", @@ -32,7 +34,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Get", { "resourceTypeArg": "-ResourceType" @@ -49,7 +51,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Set", { "resourceTypeArg": "-ResourceType" @@ -67,7 +69,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Test", { "resourceTypeArg": "-ResourceType" @@ -85,7 +87,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Export", { "resourceTypeArg": "-ResourceType" @@ -103,8 +105,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", - "Validate" + "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" ], "input": "stdin" }, @@ -112,4 +113,4 @@ "0": "Success", "1": "Error" } -} \ No newline at end of file +} diff --git a/adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json similarity index 81% rename from adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json rename to adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json index 84af06c20..35d92cc07 100644 --- a/adapters/powershell_single/WindowsPowerShell_single.dsc.resource.json +++ b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json @@ -3,7 +3,7 @@ "type": "Microsoft.Adapter/WindowsPowerShell", "version": "0.1.0", "kind": "adapter", - "description": "Resource adapter to classic DSC Powershell resources.", + "description": "Resource adapter to classic DSC Powershell resources in Windows PowerShell.", "tags": [ "PowerShell" ], @@ -17,10 +17,13 @@ "-ExecutionPolicy", "Bypass", "-Command", - "./psadapter.ps1 List" + "./psDscAdapter/powershell.resource.ps1", + "List", + "-ResourceType", + "Single" ] }, - "inputKind": "single" + "config": "full" }, "get": { "executable": "powershell", @@ -31,7 +34,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Get", { "resourceTypeArg": "-ResourceType" @@ -48,14 +51,14 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Set", { "resourceTypeArg": "-ResourceType" } ], "input": "stdin", - "implementsPretest": true + "preTest": true }, "test": { "executable": "powershell", @@ -66,7 +69,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Test", { "resourceTypeArg": "-ResourceType" @@ -84,7 +87,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", + "$Input | ./psDscAdapter/powershell.resource.ps1", "Export", { "resourceTypeArg": "-ResourceType" @@ -102,10 +105,8 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psadapter.ps1", - "Validate" - ], - "input": "stdin" + "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" + ] }, "exitCodes": { "0": "Success", diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index aa314da5d..3aa7e25ed 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -6,7 +6,9 @@ param( [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 = '@{}' + [string]$jsonInput = '@{}', + [Parameter()] + [string]$ResourceType ) function Write-DscTrace { @@ -126,10 +128,18 @@ switch ($Operation) { # match adapter to version of powershell if ($PSVersionTable.PSVersion.Major -le 5) { - $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + if ($ResourceType) { + $requireAdapter = 'Microsoft.Adapter/WindowsPowerShell' + } else { + $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + } } else { - $requireAdapter = 'Microsoft.DSC/PowerShell' + if ($ResourceType) { + $requireAdapter = 'Microsoft.Adapter/PowerShell' + } else { + $requireAdapter = 'Microsoft.DSC/PowerShell' + } } # OUTPUT dsc is expecting the following properties @@ -149,6 +159,34 @@ switch ($Operation) { } } { @('Get','Set','Test','Export') -contains $_ } { + if ($ResourceType) { + $dscResourceCache = Invoke-DscCacheRefresh -ResourceType $ResourceType.Split('/')[0] + if ($null -eq $dscResourceCache) { + Write-DscTrace -Operation Error -Message ("DSC resource '{0}' module not found." -f $ResourceType) + exit 1 + } + + $inDesiredState = $true + $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput -single }, $jsonInput ) + $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $desiredState -dscResourceCache $dscResourceCache }, $Operation, $desiredState, $dscResourceCache) + if ($null -eq $actualState) { + Write-DscTrace -Operation Error -Message 'Incomplete GET for resource ' + $desiredState.Name + exit 1 + } + if ($actualState.InDesiredState -eq $false) { + $inDesiredState = $false + } + + if ($Operation -eq 'Test') { + $result = @{ result = $actualState; _inDesiredState = $inDesiredState } | ConvertTo-Json -Depth 10 -Compress + } + else { + $result = @{ result = $actualState } | ConvertTo-Json -Depth 10 -Compress + } + Write-DscTrace -Operation Debug -Message "jsonOutput=$result" + return $result + } + $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.' diff --git a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 index 1c22fb0b5..a82420ac7 100644 --- a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 @@ -394,17 +394,28 @@ function Invoke-DscCacheRefresh { function Get-DscResourceObject { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - $jsonInput + $jsonInput, + [Switch] + $single ) # normalize the INPUT object to an array of dscResourceObject objects $inputObj = $jsonInput | ConvertFrom-Json - $desiredState = [System.Collections.Generic.List[Object]]::new() + if ($single) { + $desiredState = [dscResourceObject]@{ + name = $inputObj.name + type = $inputObj.type + properties = $inputObj.properties + } + } + else { + $desiredState = [System.Collections.Generic.List[Object]]::new() - $inputObj.resources | ForEach-Object -Process { - $desiredState += [dscResourceObject]@{ - name = $_.name - type = $_.type - properties = $_.properties + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } } } diff --git a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 index c206d89d9..23aa5eef5 100644 --- a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 @@ -280,18 +280,28 @@ function Invoke-DscCacheRefresh { function Get-DscResourceObject { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - $jsonInput + $jsonInput, + [Switch] + $single ) # normalize the INPUT object to an array of dscResourceObject objects $inputObj = $jsonInput | ConvertFrom-Json - $desiredState = [System.Collections.Generic.List[Object]]::new() - - # change the type from pscustomobject to dscResourceObject - $inputObj.resources | ForEach-Object -Process { - $desiredState += [dscResourceObject]@{ - name = $_.name - type = $_.type - properties = $_.properties + if ($single) { + $desiredState = [dscResourceObject]@{ + name = $inputObj.name + type = $inputObj.type + properties = $inputObj.properties + } + } + else { + $desiredState = [System.Collections.Generic.List[Object]]::new() + + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } } } diff --git a/adapters/powershell_single/.project.data.json b/adapters/powershell_single/.project.data.json deleted file mode 100644 index 91c6ec65d..000000000 --- a/adapters/powershell_single/.project.data.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Name": "powershell-single-adapter", - "Kind": "Adapter", - "CopyFiles": { - "All": [ - "powershell_single.dsc.resource.json", - "psadapter.ps1", - "psadapter_helpers.psm1" - ], - "Windows": [ - "WindowsPowershell_single.dsc.resource.json" - ] - } -} diff --git a/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 b/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 deleted file mode 100644 index 95ddbc0d8..000000000 --- a/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psd1 +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -@{ - RootModule = 'PSTestClassResource.psm1' - ModuleVersion = '0.0.1' - GUID = 'b267fa32-e77d-48e6-9248-676cc6f2327e' - Author = 'Microsoft' - CompanyName = 'Microsoft Corporation' - Copyright = '(c) Microsoft. All rights reserved.' - FunctionsToExport = @() - CmdletsToExport = @() - VariablesToExport = @() - AliasesToExport = @() - DscResourcesToExport = @('PSTestClassResource', 'PSNoExport') - PrivateData = @{ - PSData = @{ - DscCapabilities = @( - 'get' - 'test' - ) - } - } -} - diff --git a/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 b/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 deleted file mode 100644 index 47555a92d..000000000 --- a/adapters/powershell_single/Tests/PSTestClassResource/0.0.1/PSTestClassResource.psm1 +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -using namespace System.Collections.Generic - -enum EnumPropEnumeration { - Unexpected - Expected -} - -enum Ensure { - Present - Absent -} - -class BaseTestClass -{ - [DscProperty()] - [string] $BaseProperty -} - -[DscResource()] -class PSTestClassResource : BaseTestClass -{ - [DscProperty(Key)] - [string] $Name - - [DscProperty()] - [string] $Prop1 - - [DscProperty()] - [hashtable] $HashTableProp - - [DscProperty()] - [string] $EnumProp - - [DscProperty()] - [PSCredential] $Credential - - [DscProperty()] - [Ensure] $Ensure - - [DscProperty()] - [SecureString] $SecureStringProp - - [string] $NonDscProperty # This property shouldn't be in results data - - hidden - [string] $HiddenNonDscProperty # This property shouldn't be in results data - - hidden - [DscProperty()] - [string] $HiddenDscProperty # This property should be in results data, but is an anti-pattern. - - [void] Set() - { - } - - [bool] Test() - { - if (($this.Name -eq "PSTestClassResource1") -and ($this.Prop1 -eq "ValueForProp1")) - { - return $true - } - else - { - return $false - } - } - - [PSTestClassResource] Get() - { - if ($this.Name -eq "PSTestClassResource1") - { - $this.Prop1 = "ValueForProp1" - } - elseif ($this.Name -eq 'EchoBack') - { - # don't change the property, just echo it back - } - else - { - $this.Prop1 = $env:DSC_CONFIG_ROOT - } - $this.EnumProp = ([EnumPropEnumeration]::Expected).ToString() - return $this - } - - static [PSTestClassResource[]] Export() - { - $resultList = [List[PSTestClassResource]]::new() - $resultCount = 5 - if ($env:PSTestClassResourceResultCount) { - $resultCount = $env:PSTestClassResourceResultCount - } - 1..$resultCount | %{ - $obj = New-Object PSTestClassResource - $obj.Name = "Object$_" - $obj.Prop1 = "Property of object$_" - $resultList.Add($obj) - } - - return $resultList.ToArray() - } - - static [PSTestClassResource[]] Export([bool]$UseExport) - { - if ($UseExport) - { - return [PSTestClassResource]::Export() - } - else - { - $resultList = [List[PSTestClassResource]]::new() - $resultCount = 5 - if ($env:PSTestClassResourceResultCount) { - $resultCount = $env:PSTestClassResourceResultCount - } - 1..$resultCount | %{ - $obj = New-Object PSTestClassResource - $obj.Name = "Object$_" - $obj.Prop1 = "Property of object$_" - $resultList.Add($obj) - } - } - - return $resultList.ToArray() - } -} - -[DscResource()] -class PSNoExport: BaseTestClass -{ - [DscProperty(Key)] - [string] $Name - - [DscProperty()] - [string] $Prop1 - - [DscProperty()] - [string] $EnumProp - - [void] Set() - { - } - - [bool] Test() - { - return $true - } - - [PSNoExport] Get() - { - return $this - } -} - -function Test-World() -{ - "Hello world from PSTestModule!" -} diff --git a/adapters/powershell_single/Tests/psadapter.tests.ps1 b/adapters/powershell_single/Tests/psadapter.tests.ps1 deleted file mode 100644 index 108d4e376..000000000 --- a/adapters/powershell_single/Tests/psadapter.tests.ps1 +++ /dev/null @@ -1,301 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -Describe 'PowerShell single adapter resource tests' { - - BeforeAll { - $OldPSModulePath = $env:PSModulePath - $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot - - if ($IsLinux -or $IsMacOS) { - $cacheFilePath = Join-Path $env:HOME ".dsc" "PSAdapterCache_v4.json" - } - else { - $cacheFilePath = Join-Path $env:LocalAppData "dsc" "PSAdapterCache_v4.json" - } - } - - AfterAll { - $env:PSModulePath = $OldPSModulePath - } - - BeforeEach { - Remove-Item -Force -ErrorAction Ignore -Path $cacheFilePath - } - - It 'Discovery includes class-based resources' { - - $r = dsc resource list '*' -a Microsoft.Adapter/PowerShell - $LASTEXITCODE | Should -Be 0 - $resources = $r | ConvertFrom-Json - ($resources | Where-Object { $_.Type -eq 'PSTestClassResource/PSTestClassResource' }).Count | Should -Be 1 - ($resources | Where-Object -Property type -EQ 'PSTestClassResource/PSTestClassResource').capabilities | Should -BeIn @('get', 'set', 'test', 'export') - ($resources | Where-Object -Property type -EQ 'PSTestClassResource/PSNoExport').capabilities | Should -BeIn @('get', 'set', 'test') - } - - It 'Get works on class-based resource' { - - $r = "{'Name':'PSTestClassResource1'}" | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.Prop1 | Should -BeExactly 'ValueForProp1' - - # verify that only properties with DscProperty attribute are returned - $propertiesNames = $res.actualState | Get-Member -MemberType NoteProperty | % Name - $propertiesNames | Should -Not -Contain 'NonDscProperty' - $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' - } - - It 'Get uses enum names on class-based resource' { - - $r = "{'Name':'PSTestClassResource1'}" | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.EnumProp | Should -BeExactly 'Expected' - } - - It 'Get should return the correct properties on class-based resource' { - $r = "{'Name':'PSTestClassResource1'}" | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json -AsHashtable - $res.actualState.ContainsKey('Name') | Should -Be $True - $res.actualState.ContainsKey('Prop1') | Should -Be $True - $res.actualState.ContainsKey('HashTableProp') | Should -Be $True - $res.actualState.ContainsKey('EnumProp') | Should -Be $True - $res.actualState.ContainsKey('Credential') | Should -Be $True - $res.actualState.ContainsKey('Ensure') | Should -Be $True - $res.actualState.ContainsKey('BaseProperty') | Should -Be $True - $res.actualState.ContainsKey('HiddenDscProperty') | Should -Be $True - $res.actualState.ContainsKey('NonDscProperty') | Should -Be $False - $res.actualState.ContainsKey('HiddenNonDscProperty') | Should -Be $False - } - - It 'Test works on class-based resource' { - - $r = "{'Name':'PSTestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.InDesiredState | Should -Be $True - $res.actualState.InDesiredState.GetType().Name | Should -Be "Boolean" - - # verify that only properties with DscProperty attribute are returned - $propertiesNames = $res.actualState.InDesiredState | Get-Member -MemberType NoteProperty | % Name - $propertiesNames | Should -Not -Contain 'NonDscProperty' - $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' - } - - It 'Set works on class-based resource' { - - $r = "{'Name':'PSTestClassResource1','Prop1':'ValueForProp1'}" | dsc resource set -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.afterState.Prop1 | Should -BeExactly 'ValueForProp1' - $res.changedProperties | Should -BeNullOrEmpty - } - - It 'Export works on PS class-based resource' -Pending { - - $r = dsc resource export -r PSTestClassResource/PSTestClassResource - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.resources[0].properties.result.count | Should -Be 5 - $res.resources[0].properties.result[0].Name | Should -Be "Object1" - $res.resources[0].properties.result[0].Prop1 | Should -Be "Property of object1" - - # verify that only properties with DscProperty attribute are returned - $res.resources[0].properties.result | % { - $propertiesNames = $_ | Get-Member -MemberType NoteProperty | % Name - $propertiesNames | Should -Not -Contain 'NonDscProperty' - $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' - } - } - - It 'Get --all works on PS class-based resource' -Pending { - - $r = dsc resource get --all -r PSTestClassResource/PSTestClassResource 2>$null - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result.count | Should -Be 5 - $res.actualState.result | % { $_.Name | Should -Not -BeNullOrEmpty } - } - - It 'Verify that ClearCache works in PSAdapter' { - # generate the cache - $null = dsc resource list '*' -a Microsoft.Adapter/PowerShell - # call the ClearCache operation - $scriptPath = Join-Path $PSScriptRoot '..' 'psadapter.ps1' - $null = & $scriptPath -Operation ClearCache - # verify that PSAdapter does not find the cache - dsc -l debug resource list '*' -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt - $LASTEXITCODE | Should -Be 0 - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Cache file not found' - } - - It 'Verify that a new PS Cache version results in cache rebuid' { - # generate the cache - $null = dsc resource list '*' -a Microsoft.Adapter/PowerShell - # update the version in the cache file - $cacheFilePath = if ($IsWindows) { - # PS 6+ on Windows - Join-Path $env:LocalAppData "dsc\PSAdapterCache_v4.json" - } - else { - # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) { - Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache_v4.json" - } - else { - Join-Path $env:HOME ".dsc" "PSAdapterCache_v4.json" - } - } - $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json - $cache.CacheSchemaVersion = 0 - $jsonCache = $cache | ConvertTo-Json -Depth 90 - New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null - - # verify that a new PS Cache version results in cache rebuid - dsc -l debug resource list '*' -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt - $LASTEXITCODE | Should -Be 0 - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Incompatible version of cache in file' - } - - It 'Verify that removing a module results in cache rebuid' { - - Copy-Item -Recurse -Force -Path "$PSScriptRoot/PSTestClassResource" -Destination $TestDrive - Copy-Item -Recurse -Force -Path "$PSScriptRoot/PSTestClassResource" -Destination "$PSScriptRoot/Backup/PSTestClassResource" - Remove-Item -Recurse -Force -Path "$PSScriptRoot/PSTestClassResource" - - $oldPath = $env:PSModulePath - try { - $env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive - - # generate the cache - $null = dsc resource list '*' -a Microsoft.Adapter/PowerShell - # remove the module files - Remove-Item -Recurse -Force -Path "$TestDrive/PSTestClassResource" - # verify that cache rebuid happened - $null = dsc -l trace resource list '*' -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt - - $LASTEXITCODE | Should -Be 0 - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Detected non-existent cache entry' - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' - } - finally { - $env:PSModulePath = $oldPath - Copy-Item -Recurse -Force -Path "$PSScriptRoot/Backup/PSTestClassResource" -Destination "$PSScriptRoot" - Remove-Item -Recurse -Force -Path "$PSScriptRoot/Backup" - } - } - - It 'Verify inheritance works in class-based resources' { - - $r = dsc resource list '*' -a Microsoft.Adapter/PowerShell - $LASTEXITCODE | Should -Be 0 - $resources = $r | ConvertFrom-Json - $t = $resources | ? { $_.Type -eq 'PSTestClassResource/PSTestClassResource' } - $t.properties | Should -Contain "BaseProperty" - } - - It 'Verify highest module version is loaded' { - - $srcPath = Join-Path $PSScriptRoot 'PSTestClassResource' - $pathRoot1 = Join-Path $TestDrive 'A' - $pathRoot2 = Join-Path $TestDrive 'B' - $path1 = Join-Path $pathRoot1 'PSTestClassResource' '1.0' - $path2 = Join-Path $pathRoot1 'PSTestClassResource' '1.1' - $path3 = Join-Path $pathRoot2 'PSTestClassResource' '2.0' - $path4 = Join-Path $pathRoot2 'PSTestClassResource' '2.0.1' - - New-Item -ItemType Directory -Force -Path $path1 | Out-Null - New-Item -ItemType Directory -Force -Path $path2 | Out-Null - New-Item -ItemType Directory -Force -Path $path3 | Out-Null - New-Item -ItemType Directory -Force -Path $path4 | Out-Null - - $files = Get-ChildItem -Recurse -File -Path $srcPath - $files | Copy-Item -Destination $path1 - $files | Copy-Item -Destination $path2 - $files | Copy-Item -Destination $path3 - $files | Copy-Item -Destination $path4 - - $filePath = Join-Path $path1 'PSTestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'1.0`'") | Set-Content $filePath - $filePath = Join-Path $path2 'PSTestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'1.1`'") | Set-Content $filePath - $filePath = Join-Path $path3 'PSTestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'2.0`'") | Set-Content $filePath - $filePath = Join-Path $path4 'PSTestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'2.0.1`'") | Set-Content $filePath - - - $oldPath = $env:PSModulePath - try { - $env:PSModulePath += [System.IO.Path]::PathSeparator + $pathRoot1 - $env:PSModulePath += [System.IO.Path]::PathSeparator + $pathRoot2 - - $r = dsc resource list '*' -a Microsoft.Adapter/PowerShell - $LASTEXITCODE | Should -Be 0 - $resources = $r | ConvertFrom-Json - $r = @($resources | ? { $_.Type -eq 'PSTestClassResource/PSTestClassResource' }) - $r.Count | Should -Be 1 - $r[0].Version | Should -Be '2.0.1' - } - finally { - $env:PSModulePath = $oldPath - } - } - - It 'Verify that there are no cache rebuilds for several sequential executions' { - - # remove cache file - $cacheFilePath = if ($IsWindows) { - # PS 6+ on Windows - Join-Path $env:LocalAppData "dsc\PSAdapterCache_v4.json" - } - else { - # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) { - Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache_v4.json" - } - else { - Join-Path $env:HOME ".dsc" "PSAdapterCache_v4.json" - } - } - Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore - - # first execution should build the cache - dsc -l trace resource list -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' - - # next executions following shortly after should Not rebuild the cache - 1..3 | ForEach-Object { - dsc -l trace resource list -a Microsoft.Adapter/PowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' - } - } - - It 'Can process a key-value pair object' { - $r = '{"HashTableProp":{"Name":"DSCv3"},"Name":"PSTestClassResource1"}' | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.HashTableProp.Name | Should -Be 'DSCv3' - } - - It 'Specifying version works' { - $out = dsc resource get -r PSTestClassResource/PSTestClassResource --version 0.0.1 | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.actualState.Ensure | Should -BeExactly 'Present' - } - - It 'Specifying a non-existent version returns an error' { - $null = dsc resource get -r PSTestClassResource/PSTestClassResource --version 0.0.2 2> $TestDrive/error.log - $LASTEXITCODE | Should -Be 7 - Get-Content -Path $TestDrive/error.log | Should -Match 'Resource not found: PSTestClassResource/PSTestClassResource 0.0.2' - } - - It 'Can process SecureString property' { - $r = '{"Name":"PSTestClassResource1","SecureStringProp":"MySecretValue"}' | dsc resource get -r 'PSTestClassResource/PSTestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.SecureStringProp | Should -Not -BeNullOrEmpty - } -} diff --git a/adapters/powershell_single/psadapter.ps1 b/adapters/powershell_single/psadapter.ps1 deleted file mode 100644 index 0ed1930a4..000000000 --- a/adapters/powershell_single/psadapter.ps1 +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -[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, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] - [string]$jsonInput = '@{}', - [Parameter(Mandatory = $false)] - [string]$ResourceType, - [Parameter(Mandatory = $false)] - [string[]]$ResourcePath -) - -Import-Module -Name "$PSScriptRoot/psadapter_helpers.psm1" -Force - -switch ($Operation) { - 'ClearCache' { - Remove-Item -Path (Get-CacheFilePath) -Force -ErrorAction Ignore - exit 0 - } - 'List' { - $dscResourceCache = Invoke-DscCacheRefresh - - # cache was refreshed on script load - foreach ($dscResource in $dscResourceCache.Values) { - - # 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 = '' - } - - # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) { - $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' - } - else { - $requireAdapter = 'Microsoft.DSC/PowerShell' - } - - # 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 = $DscResourceInfo.Properties.Name - requireAdapter = $requireAdapter - description = $description - } | ConvertTo-Json -Compress - } - } - { @('Get','Set','Test','Export') -contains $_ } { - $ds = $jsonInput | ConvertFrom-Json - - # if ResourcePath is provided, we load that module - if ($ResourcePath) { - $module = Import-Module -Name $ResourcePath -Force -ErrorAction Stop -PassThru - } else { - # refresh the cache with the modules that are available on the system - $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules - if (!$dscResourceCache.ContainsKey($ResourceType)) { - Write-DscTrace -Level Error -Message "DSC resource type '$ResourceType' not found." - exit 1 - } - } - - # 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-DscTrace -Level Error -Message "Incomplete GET for resource $($ds.Name)" - exit 1 - } - if ($null -ne $actualState.Properties -and $actualState.Properties.InDesiredState -eq $false) { - $inDesiredState = $false - } - - # 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 - } - Write-DscTrace -Level Debug -Message "jsonOutput=$result" - return $result - } - 'Validate' { - # VALIDATE not implemented - - # OUTPUT - @{ valid = $true } | ConvertTo-Json - } - Default { - Write-DscTrace -Level Error -Message 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' - } -} - -# 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 -} diff --git a/adapters/powershell_single/psadapter_helpers.psm1 b/adapters/powershell_single/psadapter_helpers.psm1 deleted file mode 100644 index 2d78ddef6..000000000 --- a/adapters/powershell_single/psadapter_helpers.psm1 +++ /dev/null @@ -1,672 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -$script:CurrentCacheSchemaVersion = 4 -$script:CacheFileName = "PSAdapterCache_v$script:CurrentCacheSchemaVersion.json" - -function Get-CacheFilePath { - if ($IsWindows) { - # PS 6+ on Windows - return Join-Path $env:LocalAppData "dsc\$script:CacheFileName" - } else { - # PS 6+ on Linux/Mac - return Join-Path $env:HOME ".dsc" $script:CacheFileName - } -} - -function Write-DscTrace { - param( - [Parameter(Mandatory = $true)] - [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] - [string]$Level, - [Parameter(Mandatory = $true)] - [string]$Message - ) - - $trace = @{$Level.ToLower() = $Message } | ConvertTo-Json -Compress - $host.ui.WriteErrorLine($trace) -} - -function Invoke-Script { - param( - [Parameter(Mandatory = $true)] - [string]$Script - ) - - $ps = [PowerShell]::Create().AddScript({ - $DebugPreference = 'Continue' - $VerbosePreference = 'Continue' - $ErrorActionPreference = 'Stop' - }).AddStatement().AddScript($script) - - $traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new() - - $null = Register-ObjectEvent -InputObject $ps.Streams.Error -EventName DataAdding -MessageData $traceQueue -Action { - $traceQueue = $Event.MessageData - # convert error to string since it's an ErrorRecord - $traceQueue.Enqueue((@{ error = [string]$EventArgs.ItemAdded } | ConvertTo-Json -Compress)) - } - $null = Register-ObjectEvent -InputObject $ps.Streams.Warning -EventName DataAdding -MessageData $traceQueue -Action { - $traceQueue = $Event.MessageData - $traceQueue.Enqueue((@{ warn = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress)) - } - $null = Register-ObjectEvent -InputObject $ps.Streams.Information -EventName DataAdding -MessageData $traceQueue -Action { - $traceQueue = $Event.MessageData - if ($null -ne $EventArgs.ItemAdded.MessageData) { - if ($EventArgs.ItemAdded.Tags -contains 'PSHOST') { - $traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress)) - } else { - $traceQueue.Enqueue((@{ trace = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress)) - } - return - } - } - $null = Register-ObjectEvent -InputObject $ps.Streams.Verbose -EventName DataAdding -MessageData $traceQueue -Action { - $traceQueue = $Event.MessageData - $traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress)) - } - $null = Register-ObjectEvent -InputObject $ps.Streams.Debug -EventName DataAdding -MessageData $traceQueue -Action { - $traceQueue = $Event.MessageData - $traceQueue.Enqueue((@{ debug = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress)) - } - $outputObjects = [System.Collections.Generic.List[Object]]::new() - - function Write-TraceQueue() { - $trace = $null - while (!$traceQueue.IsEmpty) { - if ($traceQueue.TryDequeue([ref] $trace)) { - $host.ui.WriteErrorLine($trace) - } - } - } - - try { - $asyncResult = $ps.BeginInvoke() - while (-not $asyncResult.IsCompleted) { - Write-TraceQueue - - Start-Sleep -Milliseconds 100 - } - $outputCollection = $ps.EndInvoke($asyncResult) - Write-TraceQueue - - - if ($ps.HadErrors) { - # If there are any errors, we will exit with an error code - Write-DscTrace -Level Error -Message 'Errors occurred during script execution.' - exit 1 - } - - foreach ($output in $outputCollection) { - $outputObjects.Add($output) - } - } - catch { - Write-DscTrace -Level Error -Message $_ - exit 1 - } - finally { - $ps.Dispose() - Get-EventSubscriber | Unregister-Event - } -} - -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)) { - 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) { - $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if ($null -ne $containsDSCResource) { - $dscModulePsd1List.Add($psd1) | Out-Null - } - } - } - } - - return $dscModulePsd1List -} - -function Add-AstMembers { - param( - $AllTypeDefinitions, - $TypeAst, - $Properties - ) - - foreach ($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } - if ($t) { - Add-AstMembers $AllTypeDefinitions $t $Properties - } - } - - foreach ($member in $TypeAst.Members) { - $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($null -eq $property) -or ($property.IsStatic)) { - continue; - } - $skipProperty = $true - $isKeyProperty = $false - foreach ($attr in $property.Attributes) { - if ($attr.TypeName.Name -eq 'DscProperty') { - $skipProperty = $false - foreach ($attrArg in $attr.NamedArguments) { - if ($attrArg.ArgumentName -eq 'Key') { - $isKeyProperty = $true - break - } - } - } - } - if ($skipProperty) { - continue; - } - - [DscResourcePropertyInfo]$prop = [DscResourcePropertyInfo]::new() - $prop.Name = $property.Name - $prop.PropertyType = $property.PropertyType.TypeName.Name - $prop.IsMandatory = $isKeyProperty - $Properties.Add($prop) - } -} - -function FindAndParseResourceDefinitions { - [CmdletBinding(HelpUri = '')] - param( - [Parameter(Mandatory = $true)] - [string]$filePath, - [Parameter(Mandatory = $true)] - [string]$moduleVersion - ) - - if (-not (Test-Path $filePath)) { - return - } - - if (".psm1", ".ps1" -notcontains ([System.IO.Path]::GetExtension($filePath))) { - return - } - - Write-DscTrace -Level Trace -Message "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) { - Write-DscTrace -Level Error -Message ($e | Out-String) - } - - $typeDefinitions = $ast.FindAll( - { - $typeAst = $args[0] -as [System.Management.Automation.Language.TypeDefinitionAst] - return $null -ne $typeAst; - }, - $false); - - $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - - foreach ($typeDefinitionAst in $typeDefinitions) { - foreach ($a in $typeDefinitionAst.Attributes) { - if ($a.TypeName.Name -eq 'DscResource') { - $DscResourceInfo = [DscResourceInfo]::new() - $DscResourceInfo.Name = $typeDefinitionAst.Name - $DscResourceInfo.ResourceType = $typeDefinitionAst.Name - $DscResourceInfo.FriendlyName = $typeDefinitionAst.Name - $DscResourceInfo.ImplementationDetail = 'ClassBased' - $DscResourceInfo.Module = $filePath - $DscResourceInfo.Path = $filePath - #TODO: ModuleName, Version and ParentPath should be taken from psd1 contents - $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) - $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) - $DscResourceInfo.Version = $moduleVersion - - $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() - $DscResourceInfo.Capabilities = GetClassBasedCapabilities $typeDefinitionAst.Members - Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties - - $resourceList.Add($DscResourceInfo) - } - } - } - - return $resourceList -} - -function LoadPowerShellClassResourcesFromModule { - [CmdletBinding(HelpUri = '')] - param( - [Parameter(Mandatory = $true)] - [PSModuleInfo]$moduleInfo - ) - - Write-DscTrace -Level Trace -Message "Loading resources from module '$($moduleInfo.Path)'" - - if ($moduleInfo.RootModule) { - if (".psm1", ".ps1" -notcontains ([System.IO.Path]::GetExtension($moduleInfo.RootModule)) -and - (-not $moduleInfo.NestedModules)) { - Write-DscTrace -Level Trace -Message "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" - return [System.Collections.Generic.List[DscResourceInfo]]::new() - } - - $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule - } - else { - $scriptPath = $moduleInfo.Path; - } - - $version = if ($moduleInfo.Version) { $moduleInfo.Version.ToString() } else { '0.0.0' } - $Resources = FindAndParseResourceDefinitions $scriptPath $version - - if ($moduleInfo.NestedModules) { - foreach ($nestedModule in $moduleInfo.NestedModules) { - $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule - if ($resourcesOfNestedModules) { - $Resources.AddRange($resourcesOfNestedModules) - } - } - } - - return $Resources -} - -<# public function Invoke-DscCacheRefresh -.SYNOPSIS - This function caches the results of the Get-DscResource call to optimize performance. - -.DESCRIPTION - This function is designed to improve the performance of DSC operations by caching the results of the Get-DscResource call. - By storing the results, subsequent calls to Get-DscResource can retrieve the cached data instead of making a new call each time. - This can significantly speed up operations that need to repeatedly access DSC resources. - -.EXAMPLE - Invoke-DscCacheRefresh -Module "PSDesiredStateConfiguration" -#> -function Invoke-DscCacheRefresh { - [CmdletBinding(HelpUri = '')] - param( - [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [Object[]] - $Module - ) - - $refreshCache = $false - $cacheFilePath = Get-CacheFilePath - - if (Test-Path $cacheFilePath) { - Write-DscTrace -Level Debug -Message "Reading from cache file $cacheFilePath" - - $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json -Depth 10 - - if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { - $refreshCache = $true - Write-DscTrace -Level Warn -Message "Incompatible version of cache in file '$($cache.CacheSchemaVersion)' (expected '$($script:CurrentCacheSchemaVersion)')" - } - else { - $dscResourceCacheEntries = $cache.ResourceCache - - if ($null -eq $dscResourceCacheEntries -or $dscResourceCacheEntries.Keys.Count -eq 0) { - # if there is nothing in the cache file - refresh cache - $refreshCache = $true - - Write-DscTrace -Level Debug -Message "Filtered DscResourceCache cache is empty" - } - else { - Write-DscTrace -Level Debug -Message "Checking cache for stale entries" - - foreach ($cacheEntry in $dscResourceCacheEntries.Values) { - - $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - - if (Test-Path $_.Name) { - $file_LastWriteTime = (Get-Item $_.Name).LastWriteTime - # Truncate DateTime to seconds - $file_LastWriteTime = $file_LastWriteTime.AddTicks( - ($file_LastWriteTime.Ticks % [TimeSpan]::TicksPerSecond)); - - $cache_LastWriteTime = [DateTime]$_.Value - # Truncate DateTime to seconds - $cache_LastWriteTime = $cache_LastWriteTime.AddTicks( - ($cache_LastWriteTime.Ticks % [TimeSpan]::TicksPerSecond)); - - if (-not ($file_LastWriteTime.Equals($cache_LastWriteTime))) { - Write-DscTrace -Level Debug -Message "Detected stale cache entry '$($_.Name)'" - $refreshCache = $true - break - } - } - else { - Write-DscTrace -Level Debug -Message "Detected non-existent cache entry '$($_.Name)'" - $refreshCache = $true - break - } - } - - if ($refreshCache) { break } - } - - if (-not $refreshCache) { - Write-DscTrace -Level Debug -Message "Checking cache for stale PSModulePath" - - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ErrorAction SilentlyContinue } - - $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 - - Write-DscTrace -Level Debug -Message "PSModulePath diff '$diff'" - - if ($diff.Count -gt 0) { - $refreshCache = $true - } - } - } - } - } - else { - Write-DscTrace -Level Debug -Message "Cache file not found '$cacheFilePath'" - $refreshCache = $true - } - - if ($refreshCache) { - Write-DscTrace -Level Info -Message 'Constructing Get-DscResource cache' - - # create a list object to store cache of Get-DscResource - $dscResourceCacheEntries = @{} - - $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() - $dscResourceModulePsd1s = Get-DSCResourceModules - if ($null -ne $dscResourceModulePsd1s) { - $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) - $processedModuleNames = @{} - foreach ($mod in $modules) { - if (-not ($processedModuleNames.ContainsKey($mod.Name))) { - $processedModuleNames.Add($mod.Name, $true) - - # 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) { - Write-DscTrace -Level Trace -Message "Found $($selectedMod.Count) modules with name '$($mod.Name)'" - $selectedMod = $selectedMod | Sort-Object -Property Version -Descending | Select-Object -First 1 - } - - [System.Collections.Generic.List[DscResourceInfo]]$r = LoadPowerShellClassResourcesFromModule -moduleInfo $selectedMod - if ($r) { - $DscResources.AddRange($r) - } - } - } - } - - foreach ($dscResource in $DscResources) { - $moduleName = $dscResource.ModuleName - - # 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" -ErrorAction Ignore | ForEach-Object { - $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) - } - - $type = "$moduleName/$($dscResource.Name)" - $dscResourceCacheEntries += @{ - $type = [dscResourceCacheEntry]@{ - Type = $type - DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes - } - } - } - - [dscResourceCache]$cache = [dscResourceCache]::new() - $cache.ResourceCache = $dscResourceCacheEntries - $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 - Write-DscTrace -Level Debug -Message "Saving Get-DscResource cache to '$cacheFilePath'" - $jsonCache = $cache | ConvertTo-Json -Depth 90 -Compress - New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null - } - - return $dscResourceCacheEntries -} - -# Get the actual state using DSC Get method from any type of DSC resource -function Invoke-DscOperation { - param( - [Parameter(Mandatory)] - [ValidateSet('Get', 'Set', 'Test', 'Export')] - [string]$Operation, - [Parameter(Mandatory, ValueFromPipeline = $true)] - [dscResourceObject]$DesiredState, - [Parameter(Mandatory)] - [dscResourceCacheEntry[]]$dscResourceCache - ) - - $osVersion = [System.Environment]::OSVersion.VersionString - Write-DscTrace -Level Debug -Message "OS version: $osVersion" - - $psVersion = $PSVersionTable.PSVersion.ToString() - Write-DscTrace -Level Debug -Message "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 - - # if the resource is found in the cache, get the actual state - if ($cachedDscResourceInfo) { - - # formated OUTPUT of each resource - $addToActualState = [dscResourceObject]@{} - - # set top level properties of the OUTPUT object from INPUT object - $DesiredState.psobject.properties | ForEach-Object -Process { - if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) } - } - - # 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) { - - 'ClassBased' { - try { - # load powershell class from external module - $resource = GetTypeInstanceFromModule -modulename $cachedDscResourceInfo.ModuleName -classname $cachedDscResourceInfo.Name - $dscResourceInstance = $resource::New() - - $ValidProperties = $cachedDscResourceInfo.Properties.Name - - Write-DscTrace -Level Trace -Message ($ValidProperties | ConvertTo-Json -Compress) - - if ($DesiredState.properties) { - # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object - $DesiredState.properties.psobject.properties | ForEach-Object -Process { - # handle input objects by converting them to a hash table - $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name - 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) { - Write-DscTrace -Level Error -Message "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)) - } - else { - $dscResourceInstance.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } - } - } - else { - if ($validateProperty -and $validateProperty.PropertyType -in @('SecureString', 'System.Security.SecureString') -and -not [string]::IsNullOrEmpty($_.Value)) { - $dscResourceInstance.$($_.Name) = ConvertTo-SecureString -AsPlainText $_.Value -Force - } else { - $dscResourceInstance.$($_.Name) = $_.Value - } - } - } - } - - switch ($Operation) { - 'Get' { - $Result = @{} - $raw_obj = $dscResourceInstance.Get() - $ValidProperties | ForEach-Object { - if ($raw_obj.$_ -is [System.Enum]) { - $Result[$_] = $raw_obj.$_.ToString() - - } - else { - $Result[$_] = $raw_obj.$_ - } - } - $addToActualState.properties = $Result - } - 'Set' { - $dscResourceInstance.Set() - } - 'Test' { - $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } - } - 'Export' { - $t = $dscResourceInstance.GetType() - $methods = $t.GetMethods() | Where-Object { $_.Name -eq 'Export' } - $method = foreach ($mt in $methods) { - if ($mt.GetParameters().Count -eq 0) { - $mt - break - } - } - - if ($null -eq $method) { - Write-DscTrace -Level Error -Message "Export method not implemented by resource '$($DesiredState.Type)'" - exit 1 - } - $resultArray = @() - $raw_obj_array = $method.Invoke($null, $null) - foreach ($raw_obj in $raw_obj_array) { - $Result_obj = @{} - $ValidProperties | ForEach-Object { - if ($raw_obj.$_ -is [System.Enum]) { - $Result_obj[$_] = $raw_obj.$_.ToString() - } - else { - $Result_obj[$_] = $raw_obj.$_ - } - } - $resultArray += $Result_obj - } - $addToActualState = $resultArray - } - } - } - catch { - Write-DscTrace -Level Error -Message "Exception: $($_ | Out-String)" - exit 1 - } - } - Default { - Write-DscTrace -Level Error -Message "Resource ImplementationDetail not supported: $($cachedDscResourceInfo.ImplementationDetail)" - exit 1 - } - } - - Write-DscTrace -Level Trace -Message "Output: $($addToActualState | ConvertTo-Json -Depth 10 -Compress)" - return $addToActualState - } - else { - $dsJSON = $DesiredState | ConvertTo-Json -Depth 10 - Write-DscTrace -Level Error -Message "Can not find type '$($DesiredState.type)' for resource '$dsJSON'. Please ensure that Get-DscResource returns this resource type." - exit 1 - } -} - -# GetTypeInstanceFromModule function to get the type instance from the module -function GetTypeInstanceFromModule { - param( - [Parameter(Mandatory = $true)] - [string] $modulename, - [Parameter(Mandatory = $true)] - [string] $classname - ) - $instance = & (Import-Module $modulename -PassThru) ([scriptblock]::Create("'$classname' -as 'type'")) - return $instance -} - -function GetClassBasedCapabilities ($functionMemberAst) { - $capabilities = [System.Collections.Generic.List[string[]]]::new() - # These are the methods that we can potentially expect in a class-based DSC resource. - $availableMethods = @('get', 'set', 'setHandlesExist', 'whatIf', 'test', 'delete', 'export') - $methods = $functionMemberAst | Where-Object { $_ -is [System.Management.Automation.Language.FunctionMemberAst] -and $_.Name -in $availableMethods } - - foreach ($method in $methods.Name) { - # We go through each method to properly case handle the method names. - switch ($method) { - 'Get' { $capabilities.Add('get') } - 'Set' { $capabilities.Add('set') } - 'Test' { $capabilities.Add('test') } - 'WhatIf' { $capabilities.Add('whatIf') } - 'SetHandlesExist' { $capabilities.Add('setHandlesExist') } - 'Delete' { $capabilities.Add('delete') } - 'Export' { $capabilities.Add('export') } - } - } - - return ($capabilities | Select-Object -Unique) -} - -# cached resource -class dscResourceCacheEntry { - [string] $Type - [psobject] $DscResourceInfo - [PSCustomObject] $LastWriteTimes -} - -class dscResourceCache { - [int] $CacheSchemaVersion - [string[]] $PSModulePaths - [System.Collections.Hashtable] $ResourceCache -} - -# format expected for configuration output -class dscResourceObject { - [string] $name - [string] $type - [psobject] $properties -} - -# dsc resource types -enum dscResourceType { - ScriptBased - ClassBased - Binary - Composite -} - -class DscResourcePropertyInfo { - [string] $Name - [string] $PropertyType - [bool] $IsMandatory - [System.Collections.Generic.List[string]] $Values -} - -# dsc resource type (settable clone) -class DscResourceInfo { - [dscResourceType] $ImplementationDetail - [string] $ResourceType - [string] $Name - [string] $FriendlyName - [string] $Module - [string] $ModuleName - [string] $Version - [string] $Path - [string] $ParentPath - [string] $ImplementedAs - [string] $CompanyName - [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties - [string[]] $Capabilities -} From e8b587676b5886b3a4391cd719fdcd2d8343f1b1 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 11 Dec 2025 16:10:21 -0800 Subject: [PATCH 05/10] Enable psadapter to work in single mode --- .../PowerShell_adapter.dsc.resource.json | 2 +- .../Tests/powershellgroup.config.tests.ps1 | 25 ++++++++++--- ...indowsPowerShell_adapter.dsc.resource.json | 4 +-- .../psDscAdapter/powershell.resource.ps1 | 27 +++++++------- dsc/tests/dsc_adapter.tests.ps1 | 20 +++++++++++ lib/dsc-lib/src/configure/config_doc.rs | 35 ++++++++++--------- lib/dsc-lib/src/configure/mod.rs | 23 ++++++++---- 7 files changed, 92 insertions(+), 44 deletions(-) diff --git a/adapters/powershell/PowerShell_adapter.dsc.resource.json b/adapters/powershell/PowerShell_adapter.dsc.resource.json index 9b5204652..ef063c875 100644 --- a/adapters/powershell/PowerShell_adapter.dsc.resource.json +++ b/adapters/powershell/PowerShell_adapter.dsc.resource.json @@ -23,7 +23,7 @@ "Single" ] }, - "config": "full" + "config": "single" }, "get": { "executable": "pwsh", diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index 09df8968f..b3b489e52 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -247,18 +247,27 @@ Describe 'PowerShell adapter resource tests' { $out.results.result.actualState.result.properties.HashTableProp.Name | Should -BeExactly 'DSCv3' } - It 'Config calling PS Resource directly works for ' -TestCases @( - @{ Operation = 'get' } - @{ Operation = 'set' } - @{ Operation = 'test' } + It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( + @{ Operation = 'get'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'set'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'test'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'get'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'set'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'test'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'get'; metadata = 'Ignored' } + @{ Operation = 'set'; metadata = 'Ignored' } + @{ Operation = 'test'; metadata = 'Ignored' } ) { - param($Operation) + param($Operation, $metadata, $adapter) $yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Class-resource Info type: TestClassResource/TestClassResource + metadata: + ${metadata}: + requireAdapter: $adapter properties: Name: 'TestClassResource1' HashTableProp: @@ -281,6 +290,12 @@ Describe 'PowerShell adapter resource tests' { $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text } } + if ($metadata -eq 'Micrososft.DSC') { + "$TestDrive/tracing.txt" | Should -FileContentMatch "Using adapter 'Microsoft.Adapter/PowerShell'" + } + else { + "$TestDrive/tracing.txt" | Should -Not -FileContentMatch "Using adapter 'Microsoft.Adapter/PowerShell'" + } } It 'Config works with credential object' { diff --git a/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json index 35d92cc07..45fdc2b0d 100644 --- a/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json +++ b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json @@ -23,7 +23,7 @@ "Single" ] }, - "config": "full" + "config": "single" }, "get": { "executable": "powershell", @@ -41,7 +41,7 @@ } ], "input": "stdin" - }, + }, "set": { "executable": "powershell", "args": [ diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index 3aa7e25ed..a07dbe2d6 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -6,7 +6,7 @@ param( [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 = '@{}', + [string]$jsonInput = '{}', [Parameter()] [string]$ResourceType ) @@ -78,7 +78,7 @@ if ('Validate' -ne $Operation) { } if ($jsonInput) { - if ($jsonInput -ne '@{}') { + if ($jsonInput -ne '{}') { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath @@ -159,16 +159,23 @@ switch ($Operation) { } } { @('Get','Set','Test','Export') -contains $_ } { + $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 + } + if ($ResourceType) { - $dscResourceCache = Invoke-DscCacheRefresh -ResourceType $ResourceType.Split('/')[0] + 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 } + $desiredState.Type = $ResourceType $inDesiredState = $true - $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput -single }, $jsonInput ) - $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $desiredState -dscResourceCache $dscResourceCache }, $Operation, $desiredState, $dscResourceCache) + $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 exit 1 @@ -178,21 +185,15 @@ switch ($Operation) { } if ($Operation -eq 'Test') { - $result = @{ result = $actualState; _inDesiredState = $inDesiredState } | ConvertTo-Json -Depth 10 -Compress + $result = @{ desiredState = $desiredState.Properties; actualState = $actualState.Properties; _inDesiredState = $inDesiredState } | ConvertTo-Json -Depth 10 -Compress } else { - $result = @{ result = $actualState } | ConvertTo-Json -Depth 10 -Compress + $result = $actualState.Properties | ConvertTo-Json -Depth 10 -Compress } Write-DscTrace -Operation Debug -Message "jsonOutput=$result" return $result } - $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 - } - # only need to cache the resources that are used $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } if ($null -eq $dscResourceModules) { diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index c54d423e4..1fafc3fe5 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -87,5 +87,25 @@ Describe 'Tests for adapter support' { } } } + + + It 'Specifying invalid adapter via metadata fails' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test + type: Microsoft.DSC.Debug/Echo + properties: + output: '1' + metadata: + Microsoft.DSC: + requireAdapter: InvalidAdapter/Invalid +"@ + $out = dsc config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log | Out-String) + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match "Adapter resource 'InvalidAdapter/Invalid' not found" -Because $errorContent + $out | Should -BeNullOrEmpty -Because $errorContent + } } } diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index 367d01456..ce373e60a 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -62,30 +62,33 @@ pub enum RestartRequired { #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct MicrosoftDscMetadata { - /// Version of DSC - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - /// The operation being performed + /// The duration of the configuration operation #[serde(skip_serializing_if = "Option::is_none")] - pub operation: Option, - /// The type of execution - #[serde(rename = "executionType", skip_serializing_if = "Option::is_none")] - pub execution_type: Option, - /// The start time of the configuration operation - #[serde(rename = "startDatetime", skip_serializing_if = "Option::is_none")] - pub start_datetime: Option, + pub duration: Option, /// The end time of the configuration operation #[serde(rename = "endDatetime", skip_serializing_if = "Option::is_none")] pub end_datetime: Option, - /// The duration of the configuration operation + /// The type of execution + #[serde(rename = "executionType", skip_serializing_if = "Option::is_none")] + pub execution_type: Option, + /// The operation being performed #[serde(skip_serializing_if = "Option::is_none")] - pub duration: Option, - /// The security context of the configuration operation, can be specified to be required - #[serde(rename = "securityContext", skip_serializing_if = "Option::is_none")] - pub security_context: Option, + pub operation: Option, + /// Specify specific adapter type used for implicit operations + #[serde(rename = "requireAdapter", skip_serializing_if = "Option::is_none")] + pub require_adapter: Option, /// Indicates what needs to be restarted after the configuration operation #[serde(rename = "restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required: Option>, + /// The security context of the configuration operation, can be specified to be required + #[serde(rename = "securityContext", skip_serializing_if = "Option::is_none")] + pub security_context: Option, + /// The start time of the configuration operation + #[serde(rename = "startDatetime", skip_serializing_if = "Option::is_none")] + pub start_datetime: Option, + /// Version of DSC + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, } impl MicrosoftDscMetadata { diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 04e96a144..9cb70b0a5 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -367,7 +367,15 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; - let filter = add_metadata(dsc_resource, properties, resource.metadata.clone())?; + let mut dsc_resource = dsc_resource.clone(); + if let Some(resource_metadata) = &resource.metadata { + if let Some(microsoft_metadata) = &resource_metadata.microsoft { + if let Some(require_adapter) = µsoft_metadata.require_adapter { + dsc_resource.require_adapter = Some(require_adapter.clone()); + } + } + } + let filter = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { Ok(result) => result, @@ -957,14 +965,15 @@ impl Configurator { Metadata { microsoft: Some( MicrosoftDscMetadata { - version: Some(version), - operation: Some(operation), - execution_type: Some(self.context.execution_type.clone()), - start_datetime: Some(self.context.start_datetime.to_rfc3339()), - end_datetime: Some(end_datetime.to_rfc3339()), + require_adapter: None, duration: Some(end_datetime.signed_duration_since(self.context.start_datetime).to_string()), - security_context: Some(self.context.security_context.clone()), + end_datetime: Some(end_datetime.to_rfc3339()), + execution_type: Some(self.context.execution_type.clone()), + operation: Some(operation), restart_required: self.context.restart_required.clone(), + security_context: Some(self.context.security_context.clone()), + start_datetime: Some(self.context.start_datetime.to_rfc3339()), + version: Some(version), } ), other: Map::new(), From 5db7a7c0f9229f9c56b007abf04f2b9edbe2843b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 18 Dec 2025 17:04:15 -0800 Subject: [PATCH 06/10] fix adapter on processing single json --- .../Tests/powershellgroup.config.tests.ps1 | 23 ++++----- .../Tests/win_powershellgroup.tests.ps1 | 48 +++++++++++++++++++ .../psDscAdapter/powershell.resource.ps1 | 18 ++++--- .../powershell/psDscAdapter/psDscAdapter.psm1 | 32 ++++++------- .../psDscAdapter/win_psDscAdapter.psm1 | 20 ++++---- lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/configure/mod.rs | 38 +++++++++++++-- lib/dsc-lib/src/dscresources/dscresource.rs | 1 + 8 files changed, 131 insertions(+), 50 deletions(-) diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index b3b489e52..e77ebf1f5 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -248,12 +248,12 @@ Describe 'PowerShell adapter resource tests' { } It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( - @{ Operation = 'get'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } - @{ Operation = 'set'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } - @{ Operation = 'test'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } - @{ Operation = 'get'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } - @{ Operation = 'set'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } - @{ Operation = 'test'; metadata = 'Micrososft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } @{ Operation = 'get'; metadata = 'Ignored' } @{ Operation = 'set'; metadata = 'Ignored' } @{ Operation = 'test'; metadata = 'Ignored' } @@ -273,14 +273,13 @@ Describe 'PowerShell adapter resource tests' { HashTableProp: Name: 'DSCv3' "@ - $out = dsc -l trace config $operation -i $yaml 2> $TestDrive/tracing.txt $text = $out | Out-String $out = $out | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) switch ($Operation) { 'get' { - $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because $text + $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because ("$text`n" + (Get-Content -Raw -Path $TestDrive/tracing.txt)) } 'set' { $out.results[0].result.beforeState.Name | Should -BeExactly 'TestClassResource1' -Because $text @@ -290,11 +289,9 @@ Describe 'PowerShell adapter resource tests' { $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text } } - if ($metadata -eq 'Micrososft.DSC') { - "$TestDrive/tracing.txt" | Should -FileContentMatch "Using adapter 'Microsoft.Adapter/PowerShell'" - } - else { - "$TestDrive/tracing.txt" | Should -Not -FileContentMatch "Using adapter 'Microsoft.Adapter/PowerShell'" + if ($metadata -eq 'Microsoft.DSC') { + "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + } } diff --git a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 index f5c505478..bb38787b8 100644 --- a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 +++ b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 @@ -230,5 +230,53 @@ resources: $out.resources.count | Should -Be 5 $out.resources[0].properties.Ensure | Should -Be 'Present' # Check for enum property } + + It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'get'; metadata = 'Ignored' } + @{ Operation = 'set'; metadata = 'Ignored' } + @{ Operation = 'test'; metadata = 'Ignored' } + ) { + param($Operation, $metadata, $adapter) + + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Class-resource Info + type: TestClassResource/TestClassResource + metadata: + ${metadata}: + requireAdapter: $adapter + properties: + Name: 'TestClassResource1' + HashTableProp: + Name: 'DSCv3' +"@ + $out = dsc -l trace config $operation -i $yaml 2> $TestDrive/tracing.txt + $text = $out | Out-String + $out = $out | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + switch ($Operation) { + 'get' { + $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because ("$text`n" + (Get-Content -Raw -Path $TestDrive/tracing.txt)) + } + 'set' { + $out.results[0].result.beforeState.Name | Should -BeExactly 'TestClassResource1' -Because $text + $out.results[0].result.afterState.Name | Should -BeExactly 'TestClassResource1' -Because $text + } + 'test' { + $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text + } + } + if ($metadata -eq 'Microsoft.DSC') { + "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + + } + } } diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index a07dbe2d6..50bf164e6 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -159,12 +159,6 @@ switch ($Operation) { } } { @('Get','Set','Test','Export') -contains $_ } { - $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 - } - if ($ResourceType) { Write-DscTrace -Operation Debug -Message "Using resource type override: $ResourceType" $dscResourceCache = Invoke-DscCacheRefresh -Module $ResourceType.Split('/')[0] @@ -173,6 +167,12 @@ switch ($Operation) { exit 1 } + $desiredState = $psDscAdapter.invoke( { param($jsonInput, $type) Get-DscResourceObject -jsonInput $jsonInput -type $type }, $jsonInput, $ResourceType ) + if ($null -eq $desiredState) { + Write-DscTrace -Operation Error -message '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) @@ -194,6 +194,12 @@ switch ($Operation) { return $result } + $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 + } + # only need to cache the resources that are used $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } if ($null -eq $dscResourceModules) { diff --git a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 index a82420ac7..fc1b84f0e 100644 --- a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 @@ -153,7 +153,7 @@ function FindAndParseResourceDefinitions { function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName) { $methods = $ResourceType.GetMethods() | Where-Object { $_.Name -eq 'Export' } $method = $null - + if ($HasFilterProperties) { "Properties provided for filtered export" | Write-DscTrace -Operation Trace $method = foreach ($mt in $methods) { @@ -162,7 +162,7 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName break } } - + if ($null -eq $method) { "Export method with parameters not implemented by resource '$ResourceTypeName'. Filtered export is not supported." | Write-DscTrace -Operation Error exit 1 @@ -176,13 +176,13 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName break } } - + if ($null -eq $method) { "Export method not implemented by resource '$ResourceTypeName'" | Write-DscTrace -Operation Error exit 1 } } - + return $method } @@ -395,16 +395,16 @@ function Get-DscResourceObject { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $jsonInput, - [Switch] - $single + [Parameter(Mandatory = $false)] + $type ) # normalize the INPUT object to an array of dscResourceObject objects $inputObj = $jsonInput | ConvertFrom-Json - if ($single) { + if ($type) { $desiredState = [dscResourceObject]@{ - name = $inputObj.name - type = $inputObj.type - properties = $inputObj.properties + name = '' + type = $type + properties = $inputObj } } else { @@ -486,7 +486,7 @@ function Invoke-DscOperation { } else { if ($validateProperty -and $validateProperty.PropertyType -in @('SecureString', 'System.Security.SecureString') -and -not [string]::IsNullOrEmpty($_.Value)) { - $dscResourceInstance.$($_.Name) = ConvertTo-SecureString -AsPlainText $_.Value -Force + $dscResourceInstance.$($_.Name) = ConvertTo-SecureString -AsPlainText $_.Value -Force } else { $dscResourceInstance.$($_.Name) = $_.Value } @@ -498,7 +498,7 @@ function Invoke-DscOperation { 'Get' { $Result = @{} $raw_obj = $dscResourceInstance.Get() - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result[$_] = $raw_obj.$_.ToString() @@ -518,7 +518,7 @@ function Invoke-DscOperation { } 'Export' { $t = $dscResourceInstance.GetType() - $hasFilter = $null -ne $DesiredState.properties -and + $hasFilter = $null -ne $DesiredState.properties -and ($DesiredState.properties.PSObject.Properties | Measure-Object).Count -gt 0 $method = GetExportMethod -ResourceType $t -HasFilterProperties $hasFilter -ResourceTypeName $DesiredState.Type @@ -532,12 +532,12 @@ function Invoke-DscOperation { foreach ($raw_obj in $raw_obj_array) { $Result_obj = @{} - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result_obj[$_] = $raw_obj.$_.ToString() } - else { - $Result_obj[$_] = $raw_obj.$_ + else { + $Result_obj[$_] = $raw_obj.$_ } } $resultArray += $Result_obj diff --git a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 index 23aa5eef5..38665e720 100644 --- a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 @@ -281,16 +281,16 @@ function Get-DscResourceObject { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $jsonInput, - [Switch] - $single + [Parameter(Mandatory = $false)] + $type ) # normalize the INPUT object to an array of dscResourceObject objects $inputObj = $jsonInput | ConvertFrom-Json - if ($single) { + if ($type) { $desiredState = [dscResourceObject]@{ - name = $inputObj.name - type = $inputObj.type - properties = $inputObj.properties + name = '' + type = $type + properties = $inputObj } } else { @@ -446,7 +446,7 @@ function Invoke-DscOperation { 'Get' { $Result = @{} $raw_obj = $dscResourceInstance.Get() - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result[$_] = $raw_obj.$_.ToString() } else { @@ -468,11 +468,11 @@ function Invoke-DscOperation { $raw_obj_array = $method.Invoke($null, $null) foreach ($raw_obj in $raw_obj_array) { $Result_obj = @{} - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result_obj[$_] = $raw_obj.$_.ToString() - } else { - $Result_obj[$_] = $raw_obj.$_ + } else { + $Result_obj[$_] = $raw_obj.$_ } } $resultArray += $Result_obj diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index dce048834..6037287d5 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -84,6 +84,7 @@ skippingOutput = "Skipping output for '%{name}' due to condition evaluating to f secureOutputSkipped = "Secure output '%{name}' is skipped" outputTypeNotMatch = "Output '%{name}' type does not match expected type '%{expected_type}'" copyNotSupported = "Copy for output '%{name}' is currently not supported" +requireAdapter = "Resource '%{resource}' requires adapter '%{adapter}'" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 9cb70b0a5..87ec169d6 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -206,7 +206,7 @@ fn add_metadata(dsc_resource: &DscResource, mut properties: Option result, Err(e) => { progress.set_failure(get_failure_from_error(&e)); diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index afb2612a8..c992fd97b 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -144,6 +144,7 @@ impl DscResource { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + debug!("Using single input kind for adapter '{}'", adapter.type_name); adapter.target_resource = Some(resource_name.to_string()); return adapter.get(filter); } From ff76ad014ce9ba7c6cce28f66baf27f7edba9a88 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 18 Dec 2025 17:52:37 -0800 Subject: [PATCH 07/10] Fix test operation --- ...indowsPowerShell_adapter.dsc.resource.json | 7 +-- .../psDscAdapter/powershell.resource.ps1 | 9 ++-- lib/dsc-lib/src/configure/mod.rs | 49 +++++++------------ 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json index 45fdc2b0d..f04334c2c 100644 --- a/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json +++ b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json @@ -41,7 +41,7 @@ } ], "input": "stdin" - }, + }, "set": { "executable": "powershell", "args": [ @@ -58,7 +58,7 @@ } ], "input": "stdin", - "preTest": true + "implementsPretest": true }, "test": { "executable": "powershell", @@ -106,7 +106,8 @@ "Bypass", "-Command", "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" - ] + ], + "input": "stdin" }, "exitCodes": { "0": "Success", diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index 50bf164e6..30ab60673 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -184,12 +184,13 @@ switch ($Operation) { $inDesiredState = $false } + Write-DscTrace -Operation Debug -Message "actualState=$($actualState | ConvertTo-Json -Depth 10)" if ($Operation -eq 'Test') { - $result = @{ desiredState = $desiredState.Properties; actualState = $actualState.Properties; _inDesiredState = $inDesiredState } | ConvertTo-Json -Depth 10 -Compress - } - else { - $result = $actualState.Properties | ConvertTo-Json -Depth 10 -Compress + $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) + $actualState.Properties | Add-Member -MemberType NoteProperty -Name inDesiredState -Value $inDesiredState -Force } + + $result = $actualState.Properties | ConvertTo-Json -Depth 10 -Compress Write-DscTrace -Operation Debug -Message "jsonOutput=$result" return $result } diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 87ec169d6..8aab1161d 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -282,6 +282,18 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu Ok(()) } +fn update_require_adapter_from_metadata(resource: &mut DscResource, resource_metadata: &Option) -> Result<(), DscError> { + if let Some(resource_metadata) = resource_metadata { + if let Some(microsoft_metadata) = &resource_metadata.microsoft { + if let Some(require_adapter) = µsoft_metadata.require_adapter { + info!("{}", t!("configure.mod.requireAdapter", resource = &resource.type_name, adapter = require_adapter)); + resource.require_adapter = Some(require_adapter.clone()); + } + } + } + Ok(()) +} + impl Configurator { /// Create a new `Configurator` instance. /// @@ -368,14 +380,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; let mut dsc_resource = dsc_resource.clone(); - if let Some(resource_metadata) = &resource.metadata { - if let Some(microsoft_metadata) = &resource_metadata.microsoft { - if let Some(require_adapter) = µsoft_metadata.require_adapter { - info!("{}", t!("configure.mod.requireAdapter", resource = &resource.resource_type, adapter = require_adapter)); - dsc_resource.require_adapter = Some(require_adapter.clone()); - } - } - } + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; let filter = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { @@ -461,14 +466,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; let mut dsc_resource = dsc_resource.clone(); - if let Some(resource_metadata) = &resource.metadata { - if let Some(microsoft_metadata) = &resource_metadata.microsoft { - if let Some(require_adapter) = µsoft_metadata.require_adapter { - info!("{}", t!("configure.mod.requireAdapter", resource = &resource.resource_type, adapter = require_adapter)); - dsc_resource.require_adapter = Some(require_adapter.clone()); - } - } - } + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; debug!("resource_type {}", &resource.resource_type); // see if the properties contains `_exist` and is false @@ -638,14 +636,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; let mut dsc_resource = dsc_resource.clone(); - if let Some(resource_metadata) = &resource.metadata { - if let Some(microsoft_metadata) = &resource_metadata.microsoft { - if let Some(require_adapter) = µsoft_metadata.require_adapter { - info!("{}", t!("configure.mod.requireAdapter", resource = &resource.resource_type, adapter = require_adapter)); - dsc_resource.require_adapter = Some(require_adapter.clone()); - } - } - } + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; debug!("resource_type {}", &resource.resource_type); let expected = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.expectedState", state = expected)); @@ -730,14 +721,8 @@ impl Configurator { }; let properties = self.get_properties(resource, &dsc_resource.kind)?; let mut dsc_resource = dsc_resource.clone(); - if let Some(resource_metadata) = &resource.metadata { - if let Some(microsoft_metadata) = &resource_metadata.microsoft { - if let Some(require_adapter) = µsoft_metadata.require_adapter { - info!("{}", t!("configure.mod.requireAdapter", resource = &resource.resource_type, adapter = require_adapter)); - dsc_resource.require_adapter = Some(require_adapter.clone()); - } - } - } + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; + debug!("resource_type {}", &resource.resource_type); let input = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.exportInput", input = input)); let export_result = match add_resource_export_results_to_configuration(&dsc_resource, &mut conf, input.as_str()) { From 138221bdfd02561547ebc697e43a3d7853763781 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 18 Dec 2025 20:13:02 -0800 Subject: [PATCH 08/10] fix test operation --- adapters/powershell/Tests/powershellgroup.config.tests.ps1 | 3 ++- adapters/powershell/psDscAdapter/powershell.resource.ps1 | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index e77ebf1f5..2ceb42de1 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -272,6 +272,7 @@ Describe 'PowerShell adapter resource tests' { Name: 'TestClassResource1' HashTableProp: Name: 'DSCv3' + Prop1: foo "@ $out = dsc -l trace config $operation -i $yaml 2> $TestDrive/tracing.txt $text = $out | Out-String @@ -286,7 +287,7 @@ Describe 'PowerShell adapter resource tests' { $out.results[0].result.afterState.Name | Should -BeExactly 'TestClassResource1' -Because $text } 'test' { - $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text + $out.results[0].result.inDesiredState | Should -BeFalse -Because $text } } if ($metadata -eq 'Microsoft.DSC') { diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index 30ab60673..621cad434 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -187,7 +187,6 @@ switch ($Operation) { Write-DscTrace -Operation Debug -Message "actualState=$($actualState | ConvertTo-Json -Depth 10)" if ($Operation -eq 'Test') { $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) - $actualState.Properties | Add-Member -MemberType NoteProperty -Name inDesiredState -Value $inDesiredState -Force } $result = $actualState.Properties | ConvertTo-Json -Depth 10 -Compress From c3a698ddde5d1ef393e2092d3e14c7f69801fab8 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 18 Dec 2025 20:38:34 -0800 Subject: [PATCH 09/10] fix test --- .../powershell/Tests/powershellgroup.resource.tests.ps1 | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 index 9efd22a72..c5170ad7f 100644 --- a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 @@ -75,13 +75,7 @@ Describe 'PowerShell adapter resource tests' { $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.InDesiredState | Should -Be $True - $res.actualState.InDesiredState.GetType().Name | Should -Be "Boolean" - - # verify that only properties with DscProperty attribute are returned - $propertiesNames = $res.actualState.InDesiredState | Get-Member -MemberType NoteProperty | % Name - $propertiesNames | Should -Not -Contain 'NonDscProperty' - $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' + $res.InDesiredState | Should -Be $True -Because $r } It 'Set works on class-based resource' { From 3464a21aa367377120e66b0c8fcb3ff1df11f3c5 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 18 Dec 2025 20:57:53 -0800 Subject: [PATCH 10/10] fix tests --- .../powershell/psDscAdapter/powershell.resource.ps1 | 1 - dsc/tests/dsc_discovery.tests.ps1 | 4 ++-- dsc/tests/dsc_group.tests.ps1 | 10 +++++----- dsc/tests/dsc_metadata.tests.ps1 | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index 621cad434..333f65579 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -184,7 +184,6 @@ switch ($Operation) { $inDesiredState = $false } - Write-DscTrace -Operation Debug -Message "actualState=$($actualState | ConvertTo-Json -Depth 10)" if ($Operation -eq 'Test') { $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) } diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 6f168e692..792a48a5d 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -148,13 +148,13 @@ Describe 'tests for resource discovery' { Test-Path $script:lookupTableFilePath -PathType Leaf | Should -BeFalse # initial invocation should populate and save adapter lookup table - $null = dsc -l trace resource list -a Microsoft.DSC/PowerShell 2> $TestDrive/tracing.txt + $null = dsc -l trace resource list -a 'Microsoft.*/PowerShell' 2> $TestDrive/tracing.txt "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Read 0 items into lookup table" "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Saving lookup table" -Because (Get-Content -Raw "$TestDrive/tracing.txt") # second invocation (without an update) should use but not save adapter lookup table "{'Name':'TestClassResource1'}" | dsc -l trace resource get -r 'TestClassResource/TestClassResource' -f - 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly "Saving lookup table" + "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly "Saving lookup table" -Because (Get-Content -Raw "$TestDrive/tracing.txt") # third invocation (with an update) should save updated adapter lookup table $null = dsc -l trace resource list -a Test/TestGroup 2> $TestDrive/tracing.txt diff --git a/dsc/tests/dsc_group.tests.ps1 b/dsc/tests/dsc_group.tests.ps1 index 1030b7651..e4643914c 100644 --- a/dsc/tests/dsc_group.tests.ps1 +++ b/dsc/tests/dsc_group.tests.ps1 @@ -8,13 +8,13 @@ Describe 'Group resource tests' { $out | Should -BeLike @' metadata: Microsoft.DSC: - version: 3* - operation: Get - executionType: Actual - startDatetime: * - endDatetime: * duration: PT*S + endDatetime: * + executionType: Actual + operation: Get securityContext: * + startDatetime: * + version: 3* results: - metadata: Microsoft.DSC: diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 4ecd5b072..5a6c81988 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -13,9 +13,9 @@ Describe 'metadata tests' { properties: output: hello world '@ - $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $out = dsc -l info config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*" + (Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*INFO Will not add '_metadata' to properties because resource schema does not support it*" -Because (Get-Content $TestDrive/error.log -Raw) $out.results.result.actualState.output | Should -BeExactly 'hello world' }