From dfe6d9b7bf3a46b500eb117385c31d2081a1e142 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Mon, 30 Sep 2024 11:59:26 +0200 Subject: [PATCH 1/5] WIP schema option in PsDscAdapter --- .../psDscAdapter/powershell.resource.ps1 | 162 +++++++++++++----- 1 file changed, 121 insertions(+), 41 deletions(-) diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index acaafad5e..383418652 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -3,13 +3,14 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate.')] - [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'ClearCache')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'Schema', 'ClearCache')] [string]$Operation, [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] [string]$jsonInput = '@{}' ) -function Write-DscTrace { +function Write-DscTrace +{ param( [Parameter(Mandatory = $false)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] @@ -27,34 +28,44 @@ function Write-DscTrace { 'PSPath=' + $PSHome | Write-DscTrace 'PSModulePath=' + $env:PSModulePath | Write-DscTrace -if ($Operation -eq 'ClearCache') { - $cacheFilePath = if ($IsWindows) { - # PS 6+ on Windows - Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } else { - # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) { - Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - } else { - Join-Path $env:HOME ".dsc" "PSAdapterCache.json" - } +$cacheFilePath = if ($IsWindows) +{ + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" +} +else +{ + # either WinPS or PS 6+ on Linux/Mac + if ($PSVersionTable.PSVersion.Major -le 5) + { + Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" + } + else + { + Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } +} +if ($Operation -eq 'ClearCache') +{ 'Deleting cache file ' + $cacheFilePath | Write-DscTrace Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath exit 0 } -if ('Validate' -ne $Operation) { +if ('Validate' -ne $Operation) +{ # write $jsonInput to STDERR for debugging $trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) # load private functions of psDscAdapter stub module - if ($PSVersionTable.PSVersion.Major -le 5) { + if ($PSVersionTable.PSVersion.Major -le 5) + { $psDscAdapter = Import-Module "$PSScriptRoot/win_psDscAdapter.psd1" -Force -PassThru } - else { + else + { $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru } @@ -62,8 +73,10 @@ if ('Validate' -ne $Operation) { $result = [System.Collections.Generic.List[Object]]::new() } -if ($jsonInput) { - if ($jsonInput -ne '@{}') { +if ($jsonInput) +{ + if ($jsonInput -ne '@{}') + { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath @@ -74,45 +87,56 @@ if ($jsonInput) { } # process the operation requested to the script -switch ($Operation) { - 'List' { +switch ($Operation) +{ + 'List' + { $dscResourceCache = Invoke-DscCacheRefresh # cache was refreshed on script load - foreach ($dscResource in $dscResourceCache) { - + foreach ($dscResource in $dscResourceCache) + { + # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo $DscResourceInfo = $dscResource.DscResourceInfo # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test # TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module - if ($DscResourceInfo.ModuleName) { + if ($DscResourceInfo.ModuleName) + { $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 - if ($module.PrivateData.PSData.DscCapabilities) { + if ($module.PrivateData.PSData.DscCapabilities) + { $capabilities = $module.PrivateData.PSData.DscCapabilities } - else { + else + { $capabilities = @('Get', 'Set', 'Test') } } # this text comes directly from the resource manifest for v3 native resources - if ($DscResourceInfo.Description) { + if ($DscResourceInfo.Description) + { $description = $DscResourceInfo.Description } - elseif ($module.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 { + else + { $description = '' } # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) { + if ($PSVersionTable.PSVersion.Major -le 5) + { $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' } - else { + else + { $requireAdapter = 'Microsoft.DSC/PowerShell' } @@ -132,9 +156,11 @@ switch ($Operation) { } | ConvertTo-Json -Compress } } - { @('Get','Set','Test','Export') -contains $_ } { + { @('Get', 'Set', 'Test', 'Export') -contains $_ } + { $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) - if ($null -eq $desiredState) { + if ($null -eq $desiredState) + { $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 @@ -142,49 +168,93 @@ switch ($Operation) { # only need to cache the resources that are used $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } - if ($null -eq $dscResourceModules) { + if ($null -eq $dscResourceModules) + { $trace = @{'Debug' = 'ERROR: Could not get list of DSC resource types from provided JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules - if ($dscResourceCache.count -lt $dscResourceModules.count) { + if ($dscResourceCache.count -lt $dscResourceModules.count) + { $trace = @{'Debug' = 'ERROR: DSC resource module not found.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } - foreach ($ds in $desiredState) { + foreach ($ds in $desiredState) + { # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $ds, $dscResourceCache) - if ($null -eq $actualState) { + if ($null -eq $actualState) + { $trace = @{'Debug' = 'ERROR: Incomplete GET for resource ' + $ds.Name } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } $result += $actualState } - + # OUTPUT json to stderr for debug, and to stdout $result = @{ result = $result } | ConvertTo-Json -Depth 10 -Compress $trace = @{'Debug' = 'jsonOutput=' + $result } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) return $result } - 'Validate' { + 'Schema' + { + $cache = Get-Content $cacheFilePath | ConvertFrom-Json + + # TODO: Validate how input is passed + $resourceInfoproperties = ($cache.ResourceCache | Where-Object { $_.Type -eq $_.Type }).DscResourceInfo.Properties + + $props = @{} + $resourceInfoproperties | Foreach-Object { + if ($_.IsMandatory -eq $true) + { + $props[$_.Name] = [hashtable]@{ + type = $_.PropertyType + description = "" + } + } + else + { + $props[$_.Name] = [hashtable]@{ + type = @($_.PropertyType, $null) + description = "" + } + } + } + + $out = [resourceProperties]@{ + schema = 'http://json-schema.org/draft-04/schema#' + title = $jsonInput.Type + type = 'object' + required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name + properties = $props + additionalProperties = $false + # definitions = $null # TODO: Should we add definitions + } + + $out | ConvertTo-Json -Depth 10 -Compress + } + 'Validate' + { # VALIDATE not implemented - + # OUTPUT @{ valid = $true } | ConvertTo-Json } - Default { + Default + { Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' } } # output format for resource list -class resourceOutput { +class resourceOutput +{ [string] $type [string] $kind [string] $version @@ -197,3 +267,13 @@ class resourceOutput { [string] $requireAdapter [string] $description } + +class resourceProperties +{ + [string] $schema + [string] $title + [string] $type + [string[]] $required + [hashtable] $properties + [bool] $additionalProperties +} \ No newline at end of file From 4b1ef8eb609589e251eb9f2a17ca2e8d78ec7fdf Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Mon, 30 Sep 2024 12:00:43 +0200 Subject: [PATCH 2/5] Remove module --- powershell-helpers/README.md | 13 - powershell-helpers/dscCfgMigMod.psd1 | 47 --- powershell-helpers/dscCfgMigMod.psm1 | 384 ------------------ .../tests/dscCfgMigMod.tests.ps1 | 24 -- 4 files changed, 468 deletions(-) delete mode 100644 powershell-helpers/README.md delete mode 100644 powershell-helpers/dscCfgMigMod.psd1 delete mode 100644 powershell-helpers/dscCfgMigMod.psm1 delete mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md deleted file mode 100644 index d48f4e871..000000000 --- a/powershell-helpers/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: - -- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). - -## Getting started - -To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: - -1. Open a PowerShell terminal session -2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` -3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 deleted file mode 100644 index 83d1ac091..000000000 --- a/powershell-helpers/dscCfgMigMod.psd1 +++ /dev/null @@ -1,47 +0,0 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'dscCfgMigMod.psm1' - - # Version number of this module. - moduleVersion = '0.0.1' - - # ID used to uniquely identify this module - GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' - - # Author of this module - Author = 'Microsoft Corporation' - - # Company or vendor of this module - CompanyName = 'Microsoft Corporation' - - # Copyright statement for this module - Copyright = '(c) Microsoft Corporation. All rights reserved.' - - # Description of the functionality provided by this module - Description = 'PowerShell Desired State Configuration Migration Module helper' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('powershell-yaml') - - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - 'ConvertTo-DscJson' - 'ConvertTo-DscYaml' - ) - - # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() - - # Variables to export from this module - VariablesToExport = @() - - # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() - - PrivateData = @{ - PSData = @{ - ProjectUri = 'https://github.com/PowerShell/dsc' - } - } -} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 deleted file mode 100644 index c61ff4bff..000000000 --- a/powershell-helpers/dscCfgMigMod.psm1 +++ /dev/null @@ -1,384 +0,0 @@ -#region Main functions -function ConvertTo-DscJson -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 JSON format. - - .DESCRIPTION - The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscJson -Path $Path - - Returns: - { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", - "resources": { - "name": "TestResource", - "type": "Microsoft.DSC/PowerShell", - "properties": { - "resources": [ - { - "name": "Configure test resource", - "type": "TestResource/TestResource", - "properties": { - "Name": "MyTestResource", - "Ensure": "Absent" - } - } - ] - } - } - } - - .NOTES - Tags: DSC, Migration, JSON - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} - -function ConvertTo-DscYaml -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 YAML format. - - .DESCRIPTION - The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscYaml -Path $Path - - Returns: - $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - name: TestResource - type: Microsoft.DSC/PowerShell - properties: - resources: - - name: Configure test resource - type: TestResource/TestResource - properties: - Name: MyTestResource - Ensure: Absent - - .NOTES - Tags: DSC, Migration, YAML - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path -Format YAML - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} -#endRegion Main functions - -#region Helper functions -function FindAndExtractConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - if (-not (TestPathExtension $Path)) - { - return @{} - } - - # Parse the abstract syntax tree to get all hash table values representing the configuration resources - [System.Management.Automation.Language.Token[]] $tokens = $null - [System.Management.Automation.Language.ParseError[]] $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) - $configurations = $ast.FindAll({$args[0].GetType().Name -like 'HashtableAst'}, $true) - - # Create configuration document resource class (can be re-used) - $configurationDocument = [DscConfigurationResource]::new() - - # Build simple regex - $regex = [regex]::new('Configuration\s+(\w+)') - $configValue = $regex.Matches($ast.Extent.Text).Value - - if (-not $configValue) - { - return - } - - $documentConfigurationName = $configValue.TrimStart('Configuration').Trim(" ") - - # Start to build the outer basic format - $configurationDocument.name = $documentConfigurationName - $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type - - # Bag to hold resources - $resourceProps = [System.Collections.Generic.List[object]]::new() - - foreach ($configuration in $configurations) - { - # Get parent configuration details - $resourceName = ($configuration.Parent.CommandElements.Value | Select-Object -Last 1 ) - $resourceConfigurationName = ($configuration.Parent.CommandElements.Value | Select-Object -First 1) - - # Get module details - $module = Get-DscResource -Name $resourceConfigurationName -ErrorAction SilentlyContinue - - # Build the module - $resource = [DscConfigurationResource]::new() - $resource.properties = $configuration.SafeGetValue() - $resource.name = $resourceName - $resource.type = ("{0}/{1}" -f $module.ModuleName, $resourceConfigurationName) - # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource - # $document.ModuleName = $module.ModuleName - - Write-Verbose ("Adding document with data") - Write-Verbose ($resource | ConvertTo-Json | Out-String) - $resourceProps.Add($resource) - } - - # Add all the resources - $configurationDocument.properties = @{ - resources = $resourceProps - } - - return $configurationDocument -} - -function BuildConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path, - - [ValidateSet('JSON', 'YAML')] - [System.String] - $Format = 'JSON' - ) - - $configurationDocument = [ordered]@{ - "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder - resources = FindAndExtractConfigurationDocument -Path $Path - } - - switch ($Format) - { - "JSON" { - $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) - } - "YAML" { - if (TestYamlModule) - { - $inputObject = ($configurationDocument | ConvertTo-Yaml) - } - else - { - $inputObject = @{} - } - } - default { - $inputObject = $configurationDocument - } - } - - return $inputObject -} - -function TestPathExtension -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - $res = $true - - if (-not (Test-Path $Path)) - { - $res = $false - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".ps1")) - { - $res = $false - } - - return $res -} - -function TestYamlModule -{ - if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) - { - return $false - } - - return $true -} - -function GetPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\7\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function GetWindowsPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", - "$env:ProgramFiles\WindowsPowerShell\Modules", - "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function ResolvePowerShellPath -{ - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - if (-not (Test-Path $Path)) - { - return - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) - { - return - } - - if (GetPowerShellPath -Path $Path) - { - return "Microsoft.DSC/PowerShell" - } - - if (GetWindowsPowerShellPath -Path $Path) - { - return "Microsoft.Windows/WindowsPowerShell" - } - - return $null # TODO: Or default Microsoft.DSC/PowerShell -} - -#endRegion Helper functions - -#region Classes -class DscConfigurationResource -{ - [string] $name - [string] $type - [hashtable] $properties -} -#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 deleted file mode 100644 index b966993ad..000000000 --- a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -Describe "DSC Configuration Migration Module tests" { - BeforeAll { - $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path - $modLoad = Import-Module $modPath -Force -PassThru - } - - Context "ConvertTo-DscYaml" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) - $res.resources | Should -BeNullOrEmpty - } - } - - Context "ConvertTo-DscJson" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) - $res.resources | Should -BeNullOrEmpty - } - } - - AfterAll { - Remove-Module -Name $modLoad.Name -Force - } -} From 529b9614771fd9ca392332b4ca1beea72e400997 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 2 Oct 2024 04:45:56 +0200 Subject: [PATCH 3/5] Added test --- .vscode/settings.json | 1 + .../testclassresource.dsc.resource.json | 22 +++ .../Tests/powershellgroup.resource.tests.ps1 | 17 +++ .../psDscAdapter/powershell.resource.ps1 | 132 ++++++------------ 4 files changed, 84 insertions(+), 88 deletions(-) create mode 100644 powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 54f38dc44..799f6e4e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "./y2j/Cargo.toml" ], "rust-analyzer.showUnlinkedFileNotification": true, + "powershell.codeFormatting.preset": "OTBS", "json.schemas": [ { "fileMatch": ["**/*.dsc.resource.json"], diff --git a/powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json b/powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json new file mode 100644 index 000000000..0b34c3270 --- /dev/null +++ b/powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "TestClassResource/TestClassResource", + "description": "Manage test class", + "tags": [ + "Windows" + ], + "version": "0.1.0", + "schema": { + "command": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Schema" + ], + "input": "stdin" + } + } +} diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index a15e6b4c0..c7e2df0e5 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -325,4 +325,21 @@ Describe 'PowerShell adapter resource tests' { "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' } } + + It "Verify that Schema operation works on PS class-based resource" { + BeforeDiscovery { + $resourceManifest = Resolve-Path -Path (Join-Path $PSScriptRoot 'TestClassResource' 'testclassresource.dsc.resource.json') + $dest = Split-Path -Path ((Get-Command dsc).Source) -Parent + $script:file = Copy-Item -Path $resourceManifest -Destination $dest -Force -PassThru + } + + $r = dsc resource schema --resource TestClassResource/TestClassResource + $properties = $r | ConvertFrom-Json + $properties.required | Should -Not -BeNullOrEmpty + $properties.properties.PSObject.properties.Name.Contains('BaseProperty') | Should -BeTrue + } } + +AfterAll { + Remove-Item -Path $file.FullName -Force -ErrorAction SilentlyContinue +} \ No newline at end of file diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index 383418652..8ace41308 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -9,8 +9,7 @@ param( [string]$jsonInput = '@{}' ) -function Write-DscTrace -{ +function Write-DscTrace { param( [Parameter(Mandatory = $false)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] @@ -28,44 +27,33 @@ function Write-DscTrace 'PSPath=' + $PSHome | Write-DscTrace 'PSModulePath=' + $env:PSModulePath | Write-DscTrace -$cacheFilePath = if ($IsWindows) -{ +$cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" -} -else -{ +} else { # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) - { + if ($PSVersionTable.PSVersion.Major -le 5) { Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - } - else - { + } else { Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } } -if ($Operation -eq 'ClearCache') -{ +if ($Operation -eq 'ClearCache') { 'Deleting cache file ' + $cacheFilePath | Write-DscTrace Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath exit 0 } -if ('Validate' -ne $Operation) -{ +if ('Validate' -ne $Operation) { # write $jsonInput to STDERR for debugging $trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) # load private functions of psDscAdapter stub module - if ($PSVersionTable.PSVersion.Major -le 5) - { + if ($PSVersionTable.PSVersion.Major -le 5) { $psDscAdapter = Import-Module "$PSScriptRoot/win_psDscAdapter.psd1" -Force -PassThru - } - else - { + } else { $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru } @@ -73,70 +61,52 @@ if ('Validate' -ne $Operation) $result = [System.Collections.Generic.List[Object]]::new() } -if ($jsonInput) -{ - if ($jsonInput -ne '@{}') - { +if ($jsonInput) { + if ($jsonInput -ne '@{}') { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath - if ($new_psmodulepath) - { + if ($new_psmodulepath) { $env:PSModulePath = $ExecutionContext.InvokeCommand.ExpandString($new_psmodulepath) } } # process the operation requested to the script -switch ($Operation) -{ - 'List' - { +switch ($Operation) { + 'List' { $dscResourceCache = Invoke-DscCacheRefresh # cache was refreshed on script load - foreach ($dscResource in $dscResourceCache) - { + foreach ($dscResource in $dscResourceCache) { # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo $DscResourceInfo = $dscResource.DscResourceInfo # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test # TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module - if ($DscResourceInfo.ModuleName) - { + if ($DscResourceInfo.ModuleName) { $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 - if ($module.PrivateData.PSData.DscCapabilities) - { + if ($module.PrivateData.PSData.DscCapabilities) { $capabilities = $module.PrivateData.PSData.DscCapabilities - } - else - { + } else { $capabilities = @('Get', 'Set', 'Test') } } # this text comes directly from the resource manifest for v3 native resources - if ($DscResourceInfo.Description) - { + if ($DscResourceInfo.Description) { $description = $DscResourceInfo.Description - } - elseif ($module.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 - { + } else { $description = '' } # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) - { + if ($PSVersionTable.PSVersion.Major -le 5) { $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' - } - else - { + } else { $requireAdapter = 'Microsoft.DSC/PowerShell' } @@ -156,11 +126,9 @@ switch ($Operation) } | ConvertTo-Json -Compress } } - { @('Get', 'Set', 'Test', 'Export') -contains $_ } - { + { @('Get', 'Set', 'Test', 'Export') -contains $_ } { $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) - if ($null -eq $desiredState) - { + if ($null -eq $desiredState) { $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 @@ -168,27 +136,23 @@ switch ($Operation) # only need to cache the resources that are used $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } - if ($null -eq $dscResourceModules) - { + if ($null -eq $dscResourceModules) { $trace = @{'Debug' = 'ERROR: Could not get list of DSC resource types from provided JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules - if ($dscResourceCache.count -lt $dscResourceModules.count) - { + if ($dscResourceCache.count -lt $dscResourceModules.count) { $trace = @{'Debug' = 'ERROR: DSC resource module not found.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } - foreach ($ds in $desiredState) - { + foreach ($ds in $desiredState) { # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $ds, $dscResourceCache) - if ($null -eq $actualState) - { + if ($null -eq $actualState) { $trace = @{'Debug' = 'ERROR: Incomplete GET for resource ' + $ds.Name } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 @@ -202,24 +166,20 @@ switch ($Operation) $host.ui.WriteErrorLine($trace) return $result } - 'Schema' - { + 'Schema' { $cache = Get-Content $cacheFilePath | ConvertFrom-Json - # TODO: Validate how input is passed - $resourceInfoproperties = ($cache.ResourceCache | Where-Object { $_.Type -eq $_.Type }).DscResourceInfo.Properties + # TODO: Validate how input is passed and remove hindden properties + $resourceInfoproperties = ($cache.ResourceCache | Where-Object { $_.Type -eq 'TestClassResource/TestClassResource' }).DscResourceInfo.Properties $props = @{} $resourceInfoproperties | Foreach-Object { - if ($_.IsMandatory -eq $true) - { + if ($_.IsMandatory -eq $true) { $props[$_.Name] = [hashtable]@{ type = $_.PropertyType description = "" } - } - else - { + } else { $props[$_.Name] = [hashtable]@{ type = @($_.PropertyType, $null) description = "" @@ -228,33 +188,30 @@ switch ($Operation) } $out = [resourceProperties]@{ - schema = 'http://json-schema.org/draft-04/schema#' - title = $jsonInput.Type - type = 'object' - required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name - properties = $props + schema = 'http://json-schema.org/draft-04/schema#' + title = ($cache.ResourceCache | Where-Object { $_.Type -eq 'TestClassResource/TestClassResource' }).Type + type = 'object' + required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name + properties = $props additionalProperties = $false # definitions = $null # TODO: Should we add definitions } $out | ConvertTo-Json -Depth 10 -Compress } - 'Validate' - { + 'Validate' { # VALIDATE not implemented # OUTPUT @{ valid = $true } | ConvertTo-Json } - Default - { - Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + Default { + Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Schema, Validate' } } # output format for resource list -class resourceOutput -{ +class resourceOutput { [string] $type [string] $kind [string] $version @@ -268,8 +225,7 @@ class resourceOutput [string] $description } -class resourceProperties -{ +class resourceProperties { [string] $schema [string] $title [string] $type From 3126e83e0c336bac282885578e78501f9e11dbdf Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 7 Jun 2025 03:59:08 +0200 Subject: [PATCH 4/5] Add schema in message --- powershell-adapter/psDscAdapter/powershell.resource.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index 72e4128fb..c7c019a89 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -2,7 +2,7 @@ # 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.')] + [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate, Schema, ClearCache.')] [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'Schema', 'ClearCache')] [string]$Operation, [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] From f3565484faf6b1c1c0bdde7fc44e03226ecc0f92 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 7 Jun 2025 04:58:14 +0200 Subject: [PATCH 5/5] Fix test --- .../Tests/powershellgroup.resource.tests.ps1 | 19 +++++++++++-------- .../psDscAdapter/powershell.resource.ps1 | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index 80ff75cec..1f40c4fa2 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -339,25 +339,28 @@ Describe 'PowerShell adapter resource tests' { } } - It "Verify that Schema operation works on PS class-based resource" { + It 'Can process a key-value pair object' { + $r = '{"HashTableProp":{"Name":"DSCv3"},"Name":"TestClassResource1"}' | dsc resource get -r 'TestClassResource/TestClassResource' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.HashTableProp.Name | Should -Be 'DSCv3' + } + + It "Verify that schema operation works on PS class-based resource" { BeforeDiscovery { $resourceManifest = Resolve-Path -Path (Join-Path $PSScriptRoot 'TestClassResource' 'testclassresource.dsc.resource.json') $dest = Split-Path -Path ((Get-Command dsc).Source) -Parent $script:file = Copy-Item -Path $resourceManifest -Destination $dest -Force -PassThru } + # Rebuild the cache file first + dsc resource list --adapter Microsoft.DSC/PowerShell | Out-Null + $r = dsc resource schema --resource TestClassResource/TestClassResource $properties = $r | ConvertFrom-Json $properties.required | Should -Not -BeNullOrEmpty $properties.properties.PSObject.properties.Name.Contains('BaseProperty') | Should -BeTrue } - - It 'Can process a key-value pair object' { - $r = '{"HashTableProp":{"Name":"DSCv3"},"Name":"TestClassResource1"}' | dsc resource get -r 'TestClassResource/TestClassResource' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.HashTableProp.Name | Should -Be 'DSCv3' - } } AfterAll { diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index c7c019a89..c8bead34d 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -204,7 +204,7 @@ switch ($Operation) { } $out = [resourceProperties]@{ - schema = 'http://json-schema.org/draft-04/schema#' + schema = 'http://json-schema.org/draft-12/schema#' title = ($cache.ResourceCache | Where-Object { $_.Type -eq 'TestClassResource/TestClassResource' }).Type type = 'object' required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name