diff --git a/.vscode/settings.json b/.vscode/settings.json index 54f38dc4..799f6e4e 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 00000000..0b34c327 --- /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 a15e6b4c..c7e2df0e 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 acaafad5..8ace4130 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -3,7 +3,7 @@ [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 = '@{}' @@ -27,19 +27,19 @@ 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" +$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 { - # 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" - } + 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 @@ -53,8 +53,7 @@ if ('Validate' -ne $Operation) { # load private functions of psDscAdapter stub module 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 } @@ -67,8 +66,7 @@ if ($jsonInput) { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath - if ($new_psmodulepath) - { + if ($new_psmodulepath) { $env:PSModulePath = $ExecutionContext.InvokeCommand.ExpandString($new_psmodulepath) } } @@ -80,7 +78,7 @@ switch ($Operation) { # cache was refreshed on script load foreach ($dscResource in $dscResourceCache) { - + # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo $DscResourceInfo = $dscResource.DscResourceInfo @@ -90,8 +88,7 @@ switch ($Operation) { $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 if ($module.PrivateData.PSData.DscCapabilities) { $capabilities = $module.PrivateData.PSData.DscCapabilities - } - else { + } else { $capabilities = @('Get', 'Set', 'Test') } } @@ -99,20 +96,17 @@ switch ($Operation) { # this text comes directly from the resource manifest for v3 native resources 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) { $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' - } - else { + } else { $requireAdapter = 'Microsoft.DSC/PowerShell' } @@ -132,7 +126,7 @@ 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) { $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress @@ -165,21 +159,54 @@ switch ($Operation) { } $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 } + 'Schema' { + $cache = Get-Content $cacheFilePath | ConvertFrom-Json + + # 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) { + $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 = ($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 not implemented - + # OUTPUT @{ valid = $true } | ConvertTo-Json } Default { - Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Schema, Validate' } } @@ -197,3 +224,12 @@ 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