PowerShell / DSC

This repo is for the DSC v3 project
MIT License
193 stars 24 forks source link

Class-based PowerShell DSC Resources include hidden properties in `dsc config` results #157

Open michaeltlombardi opened 1 year ago

michaeltlombardi commented 1 year ago

Prerequisites

Steps to reproduce

  1. Create the files for a new PowerShell module

    mkdir ./dsc-repro/psdsc.repro
    touch ./dsc-repro/psdsc.repro/psdsc.repro.psd1
    touch ./dsc-repro/psdsc.repro/psdsc.repro.psm1
  2. Define the module manifest:

    @{
       RootModule           = 'psdsc.repro.psm1'
       ModuleVersion        = '0.0.1'
       GUID                 = '361fff1b-423a-423f-9e05-39f5ab94a437'
       Author               = 'Bug Reproducer'
       CompanyName          = 'Unknown'
       Copyright            = '(c) Bug Reproducer. All rights reserved.'
       FunctionsToExport    = '*'
       CmdletsToExport      = '*'
       VariablesToExport    = '*'
       AliasesToExport      = '*'
       DscResourcesToExport = @(
           'ReproResultData'
       )
       PrivateData          = @{ PSData = @{} }
    }
  3. Define the root module script:

    [DscResource()] class ReproResultData {
               [DscProperty(Key)]             [string] $KeyProperty
               [DscProperty()]                [string] $NormalProperty
                                              [string] $NonDscProperty
       hidden                                 [string] $HiddenNonDscProperty
       hidden  [DscProperty()]                [string] $HiddenDscProperty
       static  [DscProperty(NotConfigurable)] [string] $StaticDscProperty = @(
           'This property could be in results data,'
           'but is an anti-pattern unless the property'
           'is read-only.'
       ) -join ' '
       static                                 [string] $StaticNonDscProperty = @(
           "This property shouldn't be in results data,"
           "because it's not a DSC property."
       ) -join ' '
    
       ReproResultData() {
           $this.NormalProperty = 'Should be in results data.'
           $this.NonDscProperty = @(
               "This property shouldn't be in results data,"
               "because it's not a DSC property."
           ) -join ' '
           $this.HiddenNonDscProperty = @(
               "This property shouldn't be in results data,"
               "because it's hidden and not a DSC property."
           ) -join ' '
           $this.HiddenDscProperty = @(
               'This property should be in results data,'
               'but is an anti-pattern.'
           ) -join ' '
       }
    
       [ReproResultData] Get() {
           $Current             = [ReproResultData]::new()
           $Current.KeyProperty = $this.KeyProperty
    
           return $Current
       }
    
       [bool] Test() { return $true }
       [void] Set()  { }
    }
  4. Add the folder containing the repro module to the PSModulePath:

    $env:PSModulePath += [System.IO.Path]::PathSeparator + "$PWD/dsc-repro"
  5. Confirm you can get the ReproResultData dsc resource and see its properties:

    $Resource = Get-DscResource -Module psdsc.repro
    ImplementationDetail : ClassBased
    ResourceType         : ReproResultData
    Name                 : ReproResultData
    FriendlyName         :
    Module               : psdsc.repro
    ModuleName           : psdsc.repro
    Version              : 0.0.1
    Path                 : C:\code\dsc-repro\psdsc.repro\psdsc.repro.psd1
    ParentPath           : C:\code\dsc-repro\psdsc.repro
    ImplementedAs        : PowerShell
    CompanyName          : Unknown
    Properties           : {KeyProperty, DependsOn, HiddenDscProperty, NormalProperty…}
    $Resource | Select-Object -ExpandProperty Properties | Format-Table
    Name                 PropertyType   IsMandatory Values
    ----                 ------------   ----------- ------
    KeyProperty          [string]              True {}
    DependsOn            [string[]]           False {}
    HiddenDscProperty    [string]             False {}
    NormalProperty       [string]             False {}
    PsDscRunAsCredential [PSCredential]       False {}
    dsc --format yaml resource list psdsc.repro/ReproResultData
    # Formatted for easier reading
    type          : psdsc.repro/ReproResultData
    version       : 0.0.1
    path          : C:\code\dsc-repro\psdsc.repro\psdsc.repro.psd1
    description   : null
    directory     : C:\code\dsc-repro\psdsc.repro
    implementedAs : ClassBased
    author        : ''
    properties    : [
                       KeyProperty,
                       DependsOn,
                       HiddenDscProperty,
                       NormalProperty,
                       PsDscRunAsCredential
                   ]
    requires      : DSC/PowerShellGroup
    manifest      : null
  6. Invoke the resource with Invoke-DscResource

    $PSInvokeParams = @{
       Method   = 'Get'
       Module   = 'psdsc.repro'
       Name     = 'ReproResultData'
       Property = @{ KeyProperty = 'Repro Example (PSDSC)' }
    }
    $PSInvokeResult = Invoke-DscResource @PSInvokeParams
    $PSInvokeResult | Format-List
    KeyProperty    : Repro Example (PSDSC)
    NormalProperty : Should be in results data.
    NonDscProperty : This property shouldn't be in results data, because it's not a DSC property.
  7. Invoke the resource with dsc resource:

    $V3InvokeResult = '{ "KeyProperty": "Repro Example (DSCv3)" }' |
       dsc resource get -r psdsc.repro/ReproResultData |
       ConvertFrom-Json |
       Select-Object -ExpandProperty actualState
    $V3InvokeResult | Format-List
    KeyProperty          : Repro Example (DSCv3)
    NormalProperty       : Should be in results data.
    NonDscProperty       : This property shouldn't be in results data, because it's not a DSC property.
    HiddenNonDscProperty : This property shouldn't be in results data, because it's hidden and not a DSC property.
    HiddenDscProperty    : This property should be in results data, but is an anti-pattern.
  8. Compare the actual and preferred output statuses, with metadata:

    filter Test-HasAttribute($Property, $Attribute) {
       [bool]($Property.CustomAttributes.AttributeType -eq $Attribute)
    }
    filter Test-InPropertySet($Property, $InputObject) {
       $Set = if ($InputObject -is [Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo]) {
           $InputObject.Properties
       } else {
           $InputObject.psobject.Properties
       }
       $Property.Name -in $Set.Name
    }
    filter Test-IsHidden ($p) {
       $a = [System.Management.Automation.HiddenAttribute]
       Test-HasAttribute -Property $p -Attribute $a
    }
    filter Test-IsDscProperty ($p) {
       $a = [System.Management.Automation.DscPropertyAttribute]
       Test-HasAttribute -Property $p -Attribute $a
    }
    filter Test-ShouldBeInResult ($p) {
       $IsDscProperty    = Test-IsDscProperty $_
       $IsHiddenProperty = Test-IsHidden      $_
    
       $IsDscProperty -and (-not $IsHiddenProperty)
    }
    $PSInvokeResult.GetType().GetProperties() | Format-Table -Property @(
       'Name'
       @{ Name = 'InResourceInfo'    ; Expression = { Test-InPropertySet $_ $Resource }}
       @{ Name = 'InPSDscResult'     ; Expression = { Test-InPropertySet $_ $PSInvokeResult }}
       @{ Name = 'InV3DscResult'     ; Expression = { Test-InPropertySet $_ $V3InvokeResult }}
       @{ Name = 'ShouldBeInResults' ; Expression = { Test-ShouldBeInResult $_ }}
       @{ Name = 'IsDscProperty'     ; Expression = { Test-IsDscProperty $_ }}
       @{ Name = 'IsHidden'          ; Expression = { Test-IsHidden $_ }}
       @{ Name = 'IsStatic'          ; Expression = { $_.GetGetMethod().IsStatic }}
    )
    Name                 InResourceInfo InPSDscResult InV3DscResult ShouldBeInResults IsDscProperty IsHidden IsStatic
    ----                 -------------- ------------- ------------- ----------------- ------------- -------- --------
    KeyProperty                    True          True          True             False          True    False    False
    NormalProperty                 True          True          True             False          True    False    False
    NonDscProperty                False          True          True             False         False    False    False
    HiddenNonDscProperty          False         False          True             False         False     True    False
    HiddenDscProperty              True         False          True             False          True     True    False
    StaticDscProperty             False         False         False             False          True    False     True
    StaticNonDscProperty          False         False         False             False         False    False     True

Expected behavior

$Invoking = @{
    Method   = 'Get'
    Module   = 'psdsc.repro'
    Name     = 'ReproResultData'
    Property = @{ KeyProperty = 'Repro Example' }
}
Invoke-DscResource @Invoking | Format-List
KeyProperty    : Repro Example
NormalProperty : Should be in results data.
$invoking.Property |
    ConvertTo-Json |
    dsc resource get -r "$($i.Module)/$($i.Name)" |
    ConvertFrom-Json |
    Select-Object -ExpandProperty actualState |
    Format-List
KeyProperty          : Repro Example
NormalProperty       : Should be in results data.

Actual behavior

$Invoking = @{
    Method   = 'Get'
    Module   = 'psdsc.repro'
    Name     = 'ReproResultData'
    Property = @{ KeyProperty = 'Repro Example' }
}
Invoke-DscResource @Invoking | Format-List
KeyProperty    : Repro Example
NormalProperty : Should be in results data.
NonDscProperty : This property shouldn't be in results data, because it's not a DSC property.
$invoking.Property |
    ConvertTo-Json |
    dsc resource get -r "$($i.Module)/$($i.Name)" |
    ConvertFrom-Json |
    Select-Object -ExpandProperty actualState |
    Format-List
KeyProperty          : Repro Example
NormalProperty       : Should be in results data.
NonDscProperty       : This property shouldn't be in results data, because it's not a DSC property.
HiddenNonDscProperty : This property shouldn't be in results data, because it's hidden and not a DSC property.
HiddenDscProperty    : This property should be in results data, but is an anti-pattern.

Error details

No response

Environment data

Name                           Value
----                           -----
PSVersion                      7.3.6
PSEdition                      Core
GitCommitId                    7.3.6
OS                             Microsoft Windows 10.0.22621
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Version

DSC (build from main), PSDSC v2.0.7

Visuals

No response

Notes

While both PSDSC and DSCv3 do show the NonDscProperty in the results, that seems like a bug in PSDesiredStateConfiguration. Similarly, the non-inclusion of StaticDscProperty is (arguably) a bug in PSDesiredStateConfiguration.

However, the return data handling by the DSC/PowerShellGroup resource provider is to just convert the result data to JSON:

https://github.com/PowerShell/DSC/blob/6b286f190c81f2652cfe1fbbbfe10474467eece5/powershellgroup/powershellgroup.resource.ps1#L179

This runs into PowerShell/PowerShell#9847, where the serialization unexpectedly includes hidden properties (compared to ConvertTo-Csv and Select-Object).

Fix Proposal

Filter the returned properties to only include properties with the DscProperty attribute, so that the schema surface for the resources matches with Get-DscResource and the contract that the class definition implements.

SteveL-MSFT commented 5 months ago

I believe https://github.com/PowerShell/DSC/pull/363 will address this