Skip to content

Commit c84ee9b

Browse files
authoredMay 30, 2024··
Merge pull request #439 from anmenaga/cross_plat
Making PowerShell adapter and class-based resources cross-platform
2 parents f7ef3cb + cb17f13 commit c84ee9b

File tree

5 files changed

+233
-68
lines changed

5 files changed

+233
-68
lines changed
 

‎build.ps1

+7-4
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,13 @@ if (!$Clippy -and !$SkipBuild) {
324324
if ($Test) {
325325
$failed = $false
326326

327-
$FullyQualifiedName = @{ModuleName="PSDesiredStateConfiguration";ModuleVersion="2.0.7"}
328-
if (-not(Get-Module -ListAvailable -FullyQualifiedName $FullyQualifiedName))
329-
{ "Installing module PSDesiredStateConfiguration 2.0.7"
330-
Install-PSResource -Name PSDesiredStateConfiguration -Version 2.0.7 -Repository PSGallery -TrustRepository
327+
if ($IsWindows) {
328+
# PSDesiredStateConfiguration module is needed for Microsoft.Windows/WindowsPowerShell adapter
329+
$FullyQualifiedName = @{ModuleName="PSDesiredStateConfiguration";ModuleVersion="2.0.7"}
330+
if (-not(Get-Module -ListAvailable -FullyQualifiedName $FullyQualifiedName))
331+
{ "Installing module PSDesiredStateConfiguration 2.0.7"
332+
Install-PSResource -Name PSDesiredStateConfiguration -Version 2.0.7 -Repository PSGallery -TrustRepository
333+
}
331334
}
332335

333336
if (-not(Get-Module -ListAvailable -Name Pester))

‎powershell-adapter/Tests/powershellgroup.config.tests.ps1

+9-8
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Describe 'PowerShell adapter resource tests' {
2424
Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath
2525
}
2626

27-
It 'Get works on config with class-based resources' -Skip:(!$IsWindows){
27+
It 'Get works on config with class-based resources' {
2828

2929
$r = Get-Content -Raw $pwshConfigPath | dsc config get
3030
$LASTEXITCODE | Should -Be 0
@@ -33,23 +33,23 @@ Describe 'PowerShell adapter resource tests' {
3333
$res.results[0].result.actualState.result[0].properties.EnumProp | Should -BeExactly 'Expected'
3434
}
3535

36-
It 'Test works on config with class-based resources' -Skip:(!$IsWindows){
36+
It 'Test works on config with class-based resources' {
3737

3838
$r = Get-Content -Raw $pwshConfigPath | dsc config test
3939
$LASTEXITCODE | Should -Be 0
4040
$res = $r | ConvertFrom-Json
4141
$res.results[0].result.actualState.result[0] | Should -Not -BeNull
4242
}
4343

44-
It 'Set works on config with class-based resources' -Skip:(!$IsWindows){
44+
It 'Set works on config with class-based resources' {
4545

4646
$r = Get-Content -Raw $pwshConfigPath | dsc config set
4747
$LASTEXITCODE | Should -Be 0
4848
$res = $r | ConvertFrom-Json
4949
$res.results.result.afterState.result[0].type | Should -Be "TestClassResource/TestClassResource"
5050
}
5151

52-
It 'Export works on config with class-based resources' -Skip:(!$IsWindows){
52+
It 'Export works on config with class-based resources' {
5353

5454
$yaml = @'
5555
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
@@ -71,20 +71,21 @@ Describe 'PowerShell adapter resource tests' {
7171
$res.resources[0].properties.result[0].Prop1 | Should -Be "Property of object1"
7272
}
7373

74-
It 'Custom psmodulepath in config works' -Skip:(!$IsWindows){
74+
It 'Custom psmodulepath in config works' {
7575

7676
$OldPSModulePath = $env:PSModulePath
7777
Copy-Item -Recurse -Force -Path "$PSScriptRoot/TestClassResource" -Destination $TestDrive
7878
Rename-Item -Path "$PSScriptRoot/TestClassResource" -NewName "_TestClassResource"
7979

8080
try {
81+
$psmp = "`$env:PSModulePath"+[System.IO.Path]::PathSeparator+$TestDrive
8182
$yaml = @"
8283
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
8384
resources:
8485
- name: Working with class-based resources
8586
type: Microsoft.DSC/PowerShell
8687
properties:
87-
psmodulepath: `$env:PSModulePath;$TestDrive
88+
psmodulepath: $psmp
8889
resources:
8990
- name: Class-resource Info
9091
type: TestClassResource/TestClassResource
@@ -104,7 +105,7 @@ Describe 'PowerShell adapter resource tests' {
104105
}
105106
}
106107

107-
It 'DSCConfigRoot macro is working when config is from a file' -Skip:(!$IsWindows){
108+
It 'DSCConfigRoot macro is working when config is from a file' {
108109

109110
$yaml = @"
110111
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
@@ -129,7 +130,7 @@ Describe 'PowerShell adapter resource tests' {
129130
$res.results.result.actualState.result.properties.Prop1 | Should -Be $TestDrive
130131
}
131132

132-
It 'DSC_CONFIG_ROOT env var is cwd when config is piped from stdin' -Skip:(!$IsWindows){
133+
It 'DSC_CONFIG_ROOT env var is cwd when config is piped from stdin' {
133134

134135
$yaml = @"
135136
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json

‎powershell-adapter/Tests/powershellgroup.resource.tests.ps1

+8-9
Original file line numberDiff line numberDiff line change
@@ -23,48 +23,47 @@ Describe 'PowerShell adapter resource tests' {
2323
Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath
2424
}
2525

26-
It 'Discovery includes class-based and script-based resources ' -Skip:(!$IsWindows){
26+
It 'Discovery includes class-based resources' {
2727

28-
$r = dsc resource list * -a Microsoft.DSC/PowerShell
28+
$r = dsc resource list '*' -a Microsoft.DSC/PowerShell
2929
$LASTEXITCODE | Should -Be 0
3030
$resources = $r | ConvertFrom-Json
3131
($resources | ? {$_.Type -eq 'TestClassResource/TestClassResource'}).Count | Should -Be 1
32-
($resources | ? {$_.Type -eq 'PSTestModule/TestPSRepository'}).Count | Should -Be 1
3332
}
3433

35-
It 'Get works on class-based resource' -Skip:(!$IsWindows){
34+
It 'Get works on class-based resource' {
3635

3736
$r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource'
3837
$LASTEXITCODE | Should -Be 0
3938
$res = $r | ConvertFrom-Json
4039
$res.actualState.result.properties.Prop1 | Should -BeExactly 'ValueForProp1'
4140
}
4241

43-
It 'Get uses enum names on class-based resource' -Skip:(!$IsWindows){
42+
It 'Get uses enum names on class-based resource' {
4443

4544
$r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource'
4645
$LASTEXITCODE | Should -Be 0
4746
$res = $r | ConvertFrom-Json
4847
$res.actualState.result.properties.EnumProp | Should -BeExactly 'Expected'
4948
}
5049

51-
It 'Test works on class-based resource' -Skip:(!$IsWindows){
50+
It 'Test works on class-based resource' {
5251

5352
$r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'TestClassResource/TestClassResource'
5453
$LASTEXITCODE | Should -Be 0
5554
$res = $r | ConvertFrom-Json
5655
$res.actualState.result.properties.InDesiredState | Should -Be $True
5756
}
5857

59-
It 'Set works on class-based resource' -Skip:(!$IsWindows){
58+
It 'Set works on class-based resource' {
6059

6160
$r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource set -r 'TestClassResource/TestClassResource'
6261
$LASTEXITCODE | Should -Be 0
6362
$res = $r | ConvertFrom-Json
6463
$res.afterState.result | Should -Not -BeNull
6564
}
6665

67-
It 'Export works on PS class-based resource' -Skip:(!$IsWindows){
66+
It 'Export works on PS class-based resource' {
6867

6968
$r = dsc resource export -r TestClassResource/TestClassResource
7069
$LASTEXITCODE | Should -Be 0
@@ -74,7 +73,7 @@ Describe 'PowerShell adapter resource tests' {
7473
$res.resources[0].properties.result[0].Prop1 | Should -Be "Property of object1"
7574
}
7675

77-
It 'Get --all works on PS class-based resource' -Skip:(!$IsWindows){
76+
It 'Get --all works on PS class-based resource' {
7877

7978
$r = dsc resource get --all -r TestClassResource/TestClassResource
8079
$LASTEXITCODE | Should -Be 0

‎powershell-adapter/psDscAdapter/powershell.resource.ps1

+23-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ param(
99
[string]$jsonInput = '@{}'
1010
)
1111

12+
function Write-DscTrace {
13+
param(
14+
[Parameter(Mandatory = $false)]
15+
[ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')]
16+
[string]$Operation = 'Debug',
17+
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
18+
[string]$Message
19+
)
20+
21+
$trace = @{$Operation = $Message } | ConvertTo-Json -Compress
22+
$host.ui.WriteErrorLine($trace)
23+
}
24+
25+
# Adding some debug info to STDERR
26+
'PSVersion=' + $PSVersionTable.PSVersion.ToString() | Write-DscTrace
27+
'PSPath=' + $PSHome | Write-DscTrace
28+
'PSModulePath=' + $env:PSModulePath | Write-DscTrace
29+
1230
if ('Validate' -ne $Operation) {
1331
# write $jsonInput to STDERR for debugging
1432
$trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress
@@ -21,14 +39,15 @@ if ('Validate' -ne $Operation) {
2139
else {
2240
$psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru
2341
}
24-
2542

2643
# initialize OUTPUT as array
2744
$result = [System.Collections.Generic.List[Object]]::new()
2845
}
2946

3047
if ($jsonInput) {
31-
$inputobj_pscustomobj = $jsonInput | ConvertFrom-Json
48+
if ($jsonInput -ne '@{}') {
49+
$inputobj_pscustomobj = $jsonInput | ConvertFrom-Json
50+
}
3251
$new_psmodulepath = $inputobj_pscustomobj.psmodulepath
3352
if ($new_psmodulepath)
3453
{
@@ -48,6 +67,7 @@ switch ($Operation) {
4867
$DscResourceInfo = $dscResource.DscResourceInfo
4968

5069
# Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test
70+
# TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module
5171
if ($DscResourceInfo.ModuleName) {
5272
$module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1
5373
if ($module.PrivateData.PSData.DscCapabilities) {
@@ -82,7 +102,7 @@ switch ($Operation) {
82102
[resourceOutput]@{
83103
type = $dscResource.Type
84104
kind = 'Resource'
85-
version = $DscResourceInfo.version.ToString()
105+
version = [string]$DscResourceInfo.version
86106
capabilities = $capabilities
87107
path = $DscResourceInfo.Path
88108
directory = $DscResourceInfo.ParentPath
@@ -159,14 +179,3 @@ class resourceOutput {
159179
[string] $requireAdapter
160180
[string] $description
161181
}
162-
163-
# Adding some debug info to STDERR
164-
$trace = @{'Debug' = 'PSVersion=' + $PSVersionTable.PSVersion.ToString() } | ConvertTo-Json -Compress
165-
$host.ui.WriteErrorLine($trace)
166-
$trace = @{'Debug' = 'PSPath=' + $PSHome } | ConvertTo-Json -Compress
167-
$host.ui.WriteErrorLine($trace)
168-
$m = Get-Command 'Get-DscResource'
169-
$trace = @{'Debug' = 'Module=' + $m.Source.ToString() } | ConvertTo-Json -Compress
170-
$host.ui.WriteErrorLine($trace)
171-
$trace = @{'Debug' = 'PSModulePath=' + $env:PSModulePath } | ConvertTo-Json -Compress
172-
$host.ui.WriteErrorLine($trace)

‎powershell-adapter/psDscAdapter/psDscAdapter.psm1

+186-33
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,170 @@ function Import-PSDSCModule {
2020
$PSDesiredStateConfiguration = Import-Module $m -Force -PassThru
2121
}
2222

23+
function Get-DSCResourceModules
24+
{
25+
$listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator)
26+
$dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new()
27+
foreach ($folder in $listPSModuleFolders)
28+
{
29+
if (!(Test-Path $folder))
30+
{
31+
continue
32+
}
33+
34+
foreach($moduleFolder in Get-ChildItem $folder -Directory)
35+
{
36+
$addModule = $false
37+
foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2)
38+
{
39+
$containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*'
40+
if($null -ne $containsDSCResource)
41+
{
42+
$dscModulePsd1List.Add($psd1) | Out-Null
43+
break
44+
}
45+
}
46+
}
47+
}
48+
49+
return $dscModulePsd1List
50+
}
51+
52+
function FindAndParseResourceDefinitions
53+
{
54+
[CmdletBinding(HelpUri = '')]
55+
param(
56+
[Parameter(Mandatory = $true)]
57+
[string]$filePath
58+
)
59+
60+
if (-not (Test-Path $filePath))
61+
{
62+
return
63+
}
64+
65+
if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1"))
66+
{
67+
return
68+
}
69+
70+
"Loading resources from '$filePath'" | Write-DscTrace -Operation Trace
71+
#TODO: Handle class inheritance
72+
#TODO: Ensure embedded instances in properties are working correctly
73+
[System.Management.Automation.Language.Token[]] $tokens = $null
74+
[System.Management.Automation.Language.ParseError[]] $errors = $null
75+
$ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors)
76+
foreach($e in $errors)
77+
{
78+
$e | Out-String | Write-DscTrace -Operation Error
79+
}
80+
81+
$resourceDefinitions = $ast.FindAll(
82+
{
83+
$typeAst = $args[0] -as [System.Management.Automation.Language.TypeDefinitionAst]
84+
if ($typeAst)
85+
{
86+
foreach($a in $typeAst.Attributes)
87+
{
88+
if ($a.TypeName.Name -eq 'DscResource')
89+
{
90+
return $true;
91+
}
92+
}
93+
}
94+
95+
return $false;
96+
},
97+
$false);
98+
99+
$resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new()
100+
101+
foreach($typeDefinitionAst in $resourceDefinitions)
102+
{
103+
$DscResourceInfo = [DscResourceInfo]::new()
104+
$DscResourceInfo.Name = $typeDefinitionAst.Name
105+
$DscResourceInfo.ResourceType = $typeDefinitionAst.Name
106+
$DscResourceInfo.FriendlyName = $typeDefinitionAst.Name
107+
$DscResourceInfo.ImplementationDetail = 'ClassBased'
108+
$DscResourceInfo.Module = $filePath
109+
$DscResourceInfo.Path = $filePath
110+
#TODO: ModuleName, Version and ParentPath should be taken from psd1 contents
111+
$DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath)
112+
$DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath)
113+
114+
$DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new()
115+
foreach ($member in $typeDefinitionAst.Members)
116+
{
117+
$property = $member -as [System.Management.Automation.Language.PropertyMemberAst]
118+
if (($property -eq $null) -or ($property.IsStatic))
119+
{
120+
continue;
121+
}
122+
$skipProperty = $true
123+
$isKeyProperty = $false
124+
foreach($attr in $property.Attributes)
125+
{
126+
if ($attr.TypeName.Name -eq 'DscProperty')
127+
{
128+
$skipProperty = $false
129+
foreach($attrArg in $attr.NamedArguments)
130+
{
131+
if ($attrArg.ArgumentName -eq 'Key')
132+
{
133+
$isKeyProperty = $true
134+
}
135+
}
136+
}
137+
}
138+
if ($skipProperty)
139+
{
140+
continue;
141+
}
142+
143+
[DscResourcePropertyInfo]$prop = [DscResourcePropertyInfo]::new()
144+
$prop.Name = $property.Name
145+
$prop.PropertyType = $property.PropertyType.TypeName.Name
146+
$prop.IsMandatory = $isKeyProperty
147+
$DscResourceInfo.Properties.Add($prop)
148+
}
149+
150+
$resourceList.Add($DscResourceInfo)
151+
}
152+
153+
return $resourceList
154+
}
155+
156+
function LoadPowerShellClassResourcesFromModule
157+
{
158+
[CmdletBinding(HelpUri = '')]
159+
param(
160+
[Parameter(Mandatory = $true)]
161+
[PSModuleInfo]$moduleInfo
162+
)
163+
164+
if ($moduleInfo.RootModule)
165+
{
166+
$scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule
167+
}
168+
else
169+
{
170+
$scriptPath = $moduleInfo.Path;
171+
}
172+
173+
$Resources = FindAndParseResourceDefinitions $scriptPath
174+
175+
if ($moduleInfo.NestedModules)
176+
{
177+
foreach ($nestedModule in $moduleInfo.NestedModules)
178+
{
179+
$resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule
180+
$Resources.AddRange($resourcesOfNestedModules)
181+
}
182+
}
183+
184+
return $Resources
185+
}
186+
23187
<# public function Invoke-DscCacheRefresh
24188
.SYNOPSIS
25189
This function caches the results of the Get-DscResource call to optimize performance.
@@ -46,12 +210,8 @@ function Invoke-DscCacheRefresh {
46210
# PS 6+ on Windows
47211
Join-Path $env:LocalAppData "dsc\PSAdapterCache.json"
48212
} else {
49-
# either WinPS or PS 6+ on Linux/Mac
50-
if ($PSVersionTable.PSVersion.Major -le 5) {
51-
Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json"
52-
} else {
53-
Join-Path $env:HOME ".dsc" "PSAdapterCache.json"
54-
}
213+
# PS 6+ on Linux/Mac
214+
Join-Path $env:HOME ".dsc" "PSAdapterCache.json"
55215
}
56216

57217
if (Test-Path $cacheFilePath) {
@@ -113,33 +273,18 @@ function Invoke-DscCacheRefresh {
113273
# create a list object to store cache of Get-DscResource
114274
[dscResourceCacheEntry[]]$dscResourceCacheEntries = [System.Collections.Generic.List[Object]]::new()
115275

116-
Import-PSDSCModule
117-
$DscResources = Get-DscResource
118-
119-
foreach ($dscResource in $DscResources) {
120-
# resources that shipped in Windows should only be used with Windows PowerShell
121-
if ($dscResource.ParentPath -like "$env:windir\System32\*" -and $PSVersionTable.PSVersion.Major -gt 5) {
122-
continue
123-
}
124-
125-
if ( $dscResource.ImplementationDetail ) {
126-
# only support known dscResourceType
127-
if ([dscResourceType].GetEnumNames() -notcontains $dscResource.ImplementationDetail) {
128-
'WARNING: implementation detail not found: ' + $dscResource.ImplementationDetail | Write-DscTrace
129-
continue
130-
}
131-
}
132-
133-
$DscResourceInfo = [DscResourceInfo]::new()
134-
$dscResource.PSObject.Properties | ForEach-Object -Process {
135-
if ($null -ne $_.Value) {
136-
$DscResourceInfo.$($_.Name) = $_.Value
137-
}
138-
else {
139-
$DscResourceInfo.$($_.Name) = ''
140-
}
276+
$DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new()
277+
$dscResourceModulePsd1s = Get-DSCResourceModules
278+
if($null -ne $dscResourceModulePsd1s) {
279+
$modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s)
280+
foreach ($mod in $modules)
281+
{
282+
[System.Collections.Generic.List[DscResourceInfo]]$r = LoadPowerShellClassResourcesFromModule -moduleInfo $mod
283+
$DscResources.AddRange($r)
141284
}
285+
}
142286

287+
foreach ($dscResource in $DscResources) {
143288
$moduleName = $dscResource.ModuleName
144289

145290
# fill in resource files (and their last-write-times) that will be used for up-do-date checks
@@ -150,7 +295,7 @@ function Invoke-DscCacheRefresh {
150295

151296
$dscResourceCacheEntries += [dscResourceCacheEntry]@{
152297
Type = "$moduleName/$($dscResource.Name)"
153-
DscResourceInfo = $DscResourceInfo
298+
DscResourceInfo = $dscResource
154299
LastWriteTimes = $lastWriteTimes
155300
}
156301
}
@@ -342,6 +487,14 @@ enum dscResourceType {
342487
Composite
343488
}
344489

490+
class DscResourcePropertyInfo
491+
{
492+
[string] $Name
493+
[string] $PropertyType
494+
[bool] $IsMandatory
495+
[System.Collections.Generic.List[string]] $Values
496+
}
497+
345498
# dsc resource type (settable clone)
346499
class DscResourceInfo {
347500
[dscResourceType] $ImplementationDetail
@@ -355,5 +508,5 @@ class DscResourceInfo {
355508
[string] $ParentPath
356509
[string] $ImplementedAs
357510
[string] $CompanyName
358-
[psobject[]] $Properties
511+
[System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties
359512
}

0 commit comments

Comments
 (0)
Please sign in to comment.