Skip to content

Commit 82b9135

Browse files
authored
Merge pull request #879 from Gijsreyn/fix-class-property-discovery
Fix property discovery on class-based resources WinPS adapter
2 parents a68304a + 967f1d9 commit 82b9135

File tree

2 files changed

+118
-48
lines changed

2 files changed

+118
-48
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ Describe 'PowerShell adapter resource tests' {
5151
$res.actualState.EnumProp | Should -BeExactly 'Expected'
5252
}
5353

54+
It 'Get should return the correct properties on class-based resource' {
55+
$r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource' -f -
56+
$LASTEXITCODE | Should -Be 0
57+
$res = $r | ConvertFrom-Json -AsHashtable
58+
$res.actualState.ContainsKey('Name') | Should -Be $True
59+
$res.actualState.ContainsKey('Prop1') | Should -Be $True
60+
$res.actualState.ContainsKey('HashTableProp') | Should -Be $True
61+
$res.actualState.ContainsKey('EnumProp') | Should -Be $True
62+
$res.actualState.ContainsKey('Credential') | Should -Be $True
63+
$res.actualState.ContainsKey('Ensure') | Should -Be $True
64+
$res.actualState.ContainsKey('BaseProperty') | Should -Be $True
65+
$res.actualState.ContainsKey('HiddenDscProperty') | Should -Be $True
66+
$res.actualState.ContainsKey('NonDscProperty') | Should -Be $False
67+
$res.actualState.ContainsKey('HiddenNonDscProperty') | Should -Be $False
68+
}
69+
5470
It 'Test works on class-based resource' {
5571

5672
$r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'TestClassResource/TestClassResource' -f -

powershell-adapter/psDscAdapter/win_psDscAdapter.psm1

Lines changed: 102 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ function Write-DscTrace {
2323
if ($PSVersionTable.PSVersion.Major -gt 5) {
2424
$m = Get-Module PSDesiredStateConfiguration -ListAvailable | Sort-Object -Descending | Select-Object -First 1
2525
$PSDesiredStateConfiguration = Import-Module $m -Force -PassThru
26-
}
27-
else {
26+
} else {
2827
$env:PSModulePath = "$env:windir\System32\WindowsPowerShell\v1.0\Modules;$env:PSModulePath"
2928
$PSDesiredStateConfiguration = Import-Module -Name 'PSDesiredStateConfiguration' -RequiredVersion '1.1' -Force -PassThru -ErrorAction stop -ErrorVariable $importModuleError
3029
if (-not [string]::IsNullOrEmpty($importModuleError)) {
@@ -63,16 +62,14 @@ function Invoke-DscCacheRefresh {
6362
if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) {
6463
$refreshCache = $true
6564
"Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace
66-
}
67-
else {
65+
} else {
6866
$dscResourceCacheEntries = $cache.ResourceCache
6967

7068
if ($dscResourceCacheEntries.Count -eq 0) {
7169
# if there is nothing in the cache file - refresh cache
7270
$refreshCache = $true
7371
"Filtered DscResourceCache cache is empty" | Write-DscTrace
74-
}
75-
else {
72+
} else {
7673
"Checking cache for stale PSModulePath" | Write-DscTrace
7774

7875
$m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ErrorAction Ignore }
@@ -104,8 +101,7 @@ function Invoke-DscCacheRefresh {
104101
$namedModules.Add($cacheEntry.DscResourceInfo.ModuleName)
105102
break
106103
}
107-
}
108-
else {
104+
} else {
109105
"Detected non-existent cache entry '$($_.Name)'" | Write-DscTrace
110106
$namedModules.Add($cacheEntry.DscResourceInfo.ModuleName)
111107
break
@@ -123,8 +119,7 @@ function Invoke-DscCacheRefresh {
123119
}
124120
}
125121
}
126-
}
127-
else {
122+
} else {
128123
"Cache file not found '$cacheFilePath'" | Write-DscTrace
129124
$refreshCache = $true
130125
}
@@ -159,8 +154,7 @@ function Invoke-DscCacheRefresh {
159154
$filteredResources += $dscResources | Where-Object -Property ModuleName -NE $null | ForEach-Object { [System.String]::Concat($_.ModuleName, '/', $_.Name) }
160155
# Exclude the one module that was passed in as a parameter
161156
$existingDscResourceCacheEntries = @($cache.ResourceCache | Where-Object -Property Type -NotIn $filteredResources)
162-
}
163-
else {
157+
} else {
164158
# if no module is specified, get all resources
165159
$DscResources = Get-DscResource
166160
$Modules = Get-Module -ListAvailable
@@ -194,16 +188,14 @@ function Invoke-DscCacheRefresh {
194188
$dscResource.PSObject.Properties | ForEach-Object -Process {
195189
if ($null -ne $_.Value) {
196190
$DscResourceInfo.$($_.Name) = $_.Value
197-
}
198-
else {
191+
} else {
199192
$DscResourceInfo.$($_.Name) = ''
200193
}
201194
}
202195

203196
if ($dscResource.ModuleName) {
204197
$moduleName = $dscResource.ModuleName
205-
}
206-
elseif ($binaryBuiltInModulePaths -contains $dscResource.ParentPath) {
198+
} elseif ($binaryBuiltInModulePaths -contains $dscResource.ParentPath) {
207199
$moduleName = 'PSDesiredStateConfiguration'
208200
$DscResourceInfo.Module = 'PSDesiredStateConfiguration'
209201
$DscResourceInfo.ModuleName = 'PSDesiredStateConfiguration'
@@ -212,8 +204,7 @@ function Invoke-DscCacheRefresh {
212204
if ($PSVersionTable.PSVersion.Major -le 5 -and $DscResourceInfo.ImplementedAs -eq 'Binary') {
213205
$DscResourceInfo.ImplementationDetail = 'Binary'
214206
}
215-
}
216-
elseif ($binaryBuiltInModulePaths -notcontains $dscResource.ParentPath -and $null -ne $dscResource.ParentPath) {
207+
} elseif ($binaryBuiltInModulePaths -notcontains $dscResource.ParentPath -and $null -ne $dscResource.ParentPath) {
217208
# workaround: populate module name from parent path that is three levels up
218209
$moduleName = Split-Path $dscResource.ParentPath | Split-Path | Split-Path -Leaf
219210
$DscResourceInfo.Module = $moduleName
@@ -230,11 +221,15 @@ function Invoke-DscCacheRefresh {
230221
if ($classBased -and ($classBased.CustomAttributes.AttributeType.Name -eq 'DscResourceAttribute')) {
231222
"Detected class-based resource: $($dscResource.Name) => Type: $($classBased.BaseType.FullName)" | Write-DscTrace
232223
$dscResourceInfo.ImplementationDetail = 'ClassBased'
224+
$properties = GetClassBasedProperties -filePath $dscResource.Path -className $dscResource.Name
225+
if ($null -ne $properties) {
226+
$DscResourceInfo.Properties = $properties
227+
}
233228
}
234229

235230
# fill in resource files (and their last-write-times) that will be used for up-do-date checks
236231
$lastWriteTimes = @{}
237-
Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*.psm1", "*.mof" -ea Ignore | % {
232+
Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*.psm1", "*.mof" -ea Ignore | ForEach-Object {
238233
$lastWriteTimes.Add($_.FullName, $_.LastWriteTime.ToFileTime())
239234
}
240235

@@ -254,7 +249,7 @@ function Invoke-DscCacheRefresh {
254249

255250
[dscResourceCache]$cache = [dscResourceCache]::new()
256251
$cache.ResourceCache = $dscResourceCacheEntries.ToArray()
257-
$m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue }
252+
$m = $env:PSModulePath -split [IO.Path]::PathSeparator | ForEach-Object { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue }
258253
$cache.PSModulePaths = $m.FullName
259254
$cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion
260255

@@ -361,12 +356,10 @@ function Invoke-DscOperation {
361356
exit 1
362357
}
363358
$property.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force))
364-
}
365-
else {
359+
} else {
366360
$property.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash }
367361
}
368-
}
369-
else {
362+
} else {
370363
$property[$_.Name] = $_.Value
371364
}
372365
}
@@ -378,16 +371,14 @@ function Invoke-DscOperation {
378371

379372
if ($invokeResult.GetType().Name -eq 'Hashtable') {
380373
$invokeResult.keys | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ }
381-
}
382-
else {
374+
} else {
383375
# the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties
384376
$invokeResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ }
385377
}
386378

387379
# set the properties of the OUTPUT object from the result of Get-TargetResource
388380
$addToActualState.properties = $ResultProperties
389-
}
390-
catch {
381+
} catch {
391382
$_.Exception | Format-List * -Force | Out-String | Write-DscTrace -Operation Debug
392383
if ($_.Exception.MessageId -eq 'DscResourceNotFound') {
393384
Write-DscTrace -Operation Warn -Message 'For Windows PowerShell, DSC resources must be installed with scope AllUsers'
@@ -418,12 +409,10 @@ function Invoke-DscOperation {
418409
exit 1
419410
}
420411
$dscResourceInstance.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force))
421-
}
422-
else {
412+
} else {
423413
$dscResourceInstance.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash }
424414
}
425-
}
426-
else {
415+
} else {
427416
$dscResourceInstance.$($_.Name) = $_.Value
428417
}
429418
}
@@ -436,8 +425,7 @@ function Invoke-DscOperation {
436425
$ValidProperties | ForEach-Object {
437426
if ($raw_obj.$_ -is [System.Enum]) {
438427
$Result[$_] = $raw_obj.$_.ToString()
439-
}
440-
else {
428+
} else {
441429
$Result[$_] = $raw_obj.$_
442430
}
443431
}
@@ -459,8 +447,7 @@ function Invoke-DscOperation {
459447
$ValidProperties | ForEach-Object {
460448
if ($raw_obj.$_ -is [System.Enum]) {
461449
$Result_obj[$_] = $raw_obj.$_.ToString()
462-
}
463-
else {
450+
} else {
464451
$Result_obj[$_] = $raw_obj.$_
465452
}
466453
}
@@ -469,8 +456,7 @@ function Invoke-DscOperation {
469456
$addToActualState = $resultArray
470457
}
471458
}
472-
}
473-
catch {
459+
} catch {
474460
$_.Exception | Format-List * -Force | Out-String | Write-DscTrace -Operation Debug
475461
if ($_.Exception.MessageId -eq 'DscResourceNotFound') {
476462
Write-DscTrace -Operation Warn -Message 'For Windows PowerShell, DSC resources must be installed with scope AllUsers'
@@ -500,12 +486,10 @@ function Invoke-DscOperation {
500486
exit 1
501487
}
502488
$property.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force))
503-
}
504-
else {
489+
} else {
505490
$property.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash }
506491
}
507-
}
508-
else {
492+
} else {
509493
$property[$_.Name] = $_.Value
510494
}
511495
}
@@ -516,16 +500,14 @@ function Invoke-DscOperation {
516500
$invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property
517501
if ($invokeResult.GetType().Name -eq 'Hashtable') {
518502
$invokeResult.keys | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ }
519-
}
520-
else {
503+
} else {
521504
# the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties
522505
$invokeResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ }
523506
}
524507

525508
# set the properties of the OUTPUT object from the result of Get-TargetResource
526509
$addToActualState.properties = $ResultProperties
527-
}
528-
catch {
510+
} catch {
529511
'Exception: ' + $_.Exception.Message | Write-DscTrace -Operation Error
530512
exit 1
531513
}
@@ -537,8 +519,7 @@ function Invoke-DscOperation {
537519
}
538520

539521
return $addToActualState
540-
}
541-
else {
522+
} else {
542523
$dsJSON = $DesiredState | ConvertTo-Json -Depth 10
543524
'Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.' | Write-DscTrace -Operation Error
544525
exit 1
@@ -584,6 +565,72 @@ function ValidateMethod {
584565
return $method
585566
}
586567

568+
function GetClassBasedProperties {
569+
param (
570+
[Parameter(Mandatory = $true)]
571+
[string] $filePath,
572+
573+
[Parameter(Mandatory = $true)]
574+
[string] $className
575+
)
576+
577+
if (".psd1" -notcontains ([System.IO.Path]::GetExtension($filePath))) {
578+
return @('get', 'set', 'test')
579+
}
580+
581+
$module = Import-Module $filePath -PassThru -Force -ErrorAction Ignore
582+
583+
$properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new()
584+
585+
if (Test-Path $module.Path -ErrorAction Ignore) {
586+
[System.Management.Automation.Language.Token[]] $tokens = $null
587+
[System.Management.Automation.Language.ParseError[]] $errors = $null
588+
$ast = [System.Management.Automation.Language.Parser]::ParseFile($module.Path, [ref]$tokens, [ref]$errors)
589+
foreach ($e in $errors) {
590+
$e | Out-String | Write-DscTrace -Operation Warn
591+
}
592+
593+
$typeDefinitions = $ast.FindAll(
594+
{
595+
$typeAst = $args[0] -as [System.Management.Automation.Language.TypeDefinitionAst]
596+
return $null -ne $typeAst;
597+
},
598+
$false);
599+
600+
$typeDefinition = $typeDefinitions | Where-Object -Property Name -EQ $className
601+
602+
foreach ($member in $typeDefinition.Members) {
603+
$property = $member -as [System.Management.Automation.Language.PropertyMemberAst]
604+
if (($null -eq $property) -or ($property.IsStatic)) {
605+
continue;
606+
}
607+
$skipProperty = $true
608+
$isKeyProperty = $false
609+
foreach ($attr in $property.Attributes) {
610+
if ($attr.TypeName.Name -eq 'DscProperty') {
611+
$skipProperty = $false
612+
foreach ($attrArg in $attr.NamedArguments) {
613+
if ($attrArg.ArgumentName -eq 'Key') {
614+
$isKeyProperty = $true
615+
break
616+
}
617+
}
618+
}
619+
}
620+
if ($skipProperty) {
621+
continue;
622+
}
623+
624+
[DscResourcePropertyInfo]$prop = [DscResourcePropertyInfo]::new()
625+
$prop.Name = $property.Name
626+
$prop.PropertyType = $property.PropertyType.TypeName.Name
627+
$prop.IsMandatory = $isKeyProperty
628+
$properties.Add($prop)
629+
}
630+
return $properties
631+
}
632+
}
633+
587634
# cached resource
588635
class dscResourceCacheEntry {
589636
[string] $Type
@@ -612,6 +659,13 @@ enum dscResourceType {
612659
Composite
613660
}
614661

662+
class DscResourcePropertyInfo {
663+
[string] $Name
664+
[string] $PropertyType
665+
[bool] $IsMandatory
666+
[System.Collections.Generic.List[string]] $Values
667+
}
668+
615669
# dsc resource type (settable clone)
616670
class DscResourceInfo {
617671
[dscResourceType] $ImplementationDetail

0 commit comments

Comments
 (0)