Skip to content

Proposal: Define method signatures for PSDSC resource classes #860

Open
@michaeltlombardi

Description

@michaeltlombardi

Summary of the new feature / enhancement

As an author implementing DSC resources in PowerShell, I want to be able to implement a PowerShell class that can fully participate in the semantics of both DSC and PSDSC.

Currently, implementing a PSDSC resource requires the following method signatures, which map to the operations in PSDSC:

  • [<ResourceClassTypeName>] Get()

    Returns a single instance of the class representing the actual current state.

  • [bool] Test()

    Returns $true if the instance is in the desired state and $false otherwise.

  • [void] Set()

    Enforces the desired state for the instance.

These method signatures are checked by the parser when a PowerShell class has the [DscResource()] attribute. The parser also checks:

  • Whether the class defines at least one string or enum property with the [DscProperty(Key)] attribute.
  • Whether the class has a default constructor (if it defines a non-default constructor, it must also explicitly define a default constructor)

For DSC, we have added support for the export operation with the following method signature:

static [<ResourceClassTypeName>[]] Export()

However, we don't currently support:

  • The delete operation,
  • Invoking the set operation in what-if mode,
  • Returning specific information for how a resource is out of the desired state when the Test() method returns $false,
  • Returning the final state of an instance, optionally with the list of changed properties, for the set operation.

I propose that we consider and define method signatures in support of the semantics of DSC. When we implement the PowerShell resource development kit (RDK), we can provide a bridge between classic implementations for PSDSC and DSC-compliant semantics through a base class.

Proposed technical implementation details (optional)

I propose supporting the following method signatures for PowerShell classes.

Test operation method signatures

  • Result object (requires dependency)

    static [DscResourceTestResult] Test(
      [<ResourceClassTypeName>]$instance
    )

    This signature assumes the availability of the Microsoft.PowerShell.Dsc module to provide the [DscResourceTestResult], which would have a definition like:

    class DscResourceTestResult {
      [DscResourceInstance] $ActualState
      [bool]                $InDesiredState
      [string[]]            $DifferingProperties
    }
    class DscResourceInstance {
      hidden [ValidateIsDscResourceClass()] [object] $value
    }

    Which we could use to provide the equivalent of the state and stateAndDiff outputs without requiring the resource to define an InDesiredState property. The DscResourceInstance class is just a lightweight type to ensure that the value is always an instance of a DSC resource.

  • Tuples (no dependency)

    # State
    static [System.Tuple[<ResourceClassTypeName>, bool]] Test(
      [<ResourceClassTypeName>]$instance
    )
    # State and diff
    static [System.Tuple[<ResourceClassTypeName>, bool, string[]]] Test(
      [<ResourceClassTypeName>]$instance
    )

    This option is more complex for the author, but doesn't require a dependency.

Set operation method signatures

Here we could enable resource authors to support what-if mode with less overhead. If the class doesn't implement the signature with the whatIf parameter, the resource doesn't support invoking the set operation in what-if mode. If the resource does support what-if mode, authors would only need to implement their code in that method. The other method could just invoke the what-if mode method with whatIf as $false.

  • Result object (requires dependency)

    static [DscResourceSetResult] Set(
      [<ResourceClassTypeName>]$instance,
      [bool]$whatIf
    )
    static [DscResourceSetResult] Set(
      [<ResourceClassTypeName>]$instance
    )}

    This signature assumes the availability of the Microsoft.PowerShell.Dsc module to provide the [DscResourceSetResult], which would have a definition like:

    class DscResourceTestResult {
      [DscResourceInstance] $AfterState
      [string[]]            $ChangedProperties
    }
    class DscResourceInstance {
      hidden [ValidateIsDscResourceClass()] [object] $value
    }

    Which we could use to provide the equivalent of the state and stateAndDiff outputs. The DscResourceInstance class is just a lightweight type to ensure that the value is always an instance of a DSC resource.

  • Tuples (no dependency)

    # State
    static [<ResourceClassTypeName>] Set(
      [<ResourceClassTypeName>]$instance
    )
    static [<ResourceClassTypeName>] Set(
      [<ResourceClassTypeName>]$instance,
      [bool]$whatIf
    )
    # State and diff
    static [System.Tuple[<ResourceClassTypeName>, string[]]] Set(
      [<ResourceClassTypeName>]$instance
    )
    static [System.Tuple[<ResourceClassTypeName>, string[]]] Set(
      [<ResourceClassTypeName>]$instance,
      [bool]$whatIf
    )

    This option is more complex for the author, but doesn't require a dependency.

Delete operation method signatures

For resources that support the delete operation, we could expect the following signatures:

static [void] Delete(
    [<ResourceClassTypeName>]$instance
)
static [void] Delete(
  [<ResourceClassTypeName>]$instance,
  [bool]$whatIf
)

As with the set operation methods, this would enable authors to implement what-if support with
little overhead.

Export operation method signatures

For resources that support the export operation, we could expect the following signatures:

static [<ResourceClassTypeName>[]] Export(
  [<ResourceClassTypeName>]$filteringInstance
)

static [<ResourceClassTypeName>[]] Export()

Here the resource author could choose whether to support export with filters, similar to supporting
what-if modes for the set and delete operations. We could infer from static analysis whether the
resource supports filtering.

Get operation method signature

static [<ResourceClassTypeName>] Get(
  [<ResourceClassTypeName>]$instance
)

This static method returns the actual state for the provided instance. This is nearly identical to the PSDSC signature, except that it's static (and thus requires passing the instance instead of invoking the method on an instance.

This is primarily to bring the method signature in line with the other proposed signatures.

Example classes

The following example classes show the methods together.

  • Without dependencies

    [DscResource()]
    class TSToysCliSettings {
      [DscProperty(Key)]
      [ValidateSet("machine", "user")]
      [string]
      $SettingsScope
    
      [DscProperty()]
      [System.Nullable[bool]]
      $UpdateAutomatically
    
      [DscProperty()]
      [ValidateRange(1, 90)]
      [System.Nullable[int]]
      $UpdateFrequency
    
      [DscProperty()]
      [bool]$Exist = $true
    
      #region    DSC static methods
      static [TSToysCliSettings] Get([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [System.Tuple[TSToysCliSettings, bool]] Test([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [TSToysCliSettings] Set([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Set($instance, $false)
      }
      static [TSToysCliSettings] Set([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [void] Delete([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Delete($instance, $false)
      }
      static [void] Delete([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [TSToysCliSettings[]] Export() {
        [TSToysCliSettings]::Export($null)
      }
      static [TSToysCliSettings[]] Export([TSToysCliSettings]$filteringInstance) {
        # elided for brevity
      }
      #endregion DSC static methods
      #region    PSDSC instance methods
      [TSToysCliSettings] Get() {
        return [TSToysCliSettings]::Get($this)
      }
      [bool] Test() {
        return [TSToysCliSettings]::Test($this).Item2
      }
    
      [void] Set() {
        [TSToysCliSettings]::Set($this)
      }
      #endregion PSDSC instance methods
    }
  • With dependency (no base class)

    [DscResource()]
    class TSToysCliSettings {
      [DscProperty(Key)]
      [ValidateSet("machine", "user")]
      [string]
      $SettingsScope
    
      [DscProperty()]
      [System.Nullable[bool]]
      $UpdateAutomatically
    
      [DscProperty()]
      [ValidateRange(1, 90)]
      [System.Nullable[int]]
      $UpdateFrequency
    
      [DscProperty()]
      [bool]$Exist = $true
    
      #region    DSC static methods
      static [TSToysCliSettings] Get([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceTestResult] Test([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceSetResult] Set([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Set($instance, $false)
      }
      static [DscResourceSetResult] Set([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [void] Delete([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Delete($instance, $false)
      }
      static [void] Delete([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [TSToysCliSettings[]] Export() {
        [TSToysCliSettings]::Export($null)
      }
      static [TSToysCliSettings[]] Export([TSToysCliSettings]$filteringInstance) {
        # elided for brevity
      }
      #endregion DSC static methods
      #region    PSDSC instance methods
      [TSToysCliSettings] Get() {
        return [TSToysCliSettings]::Get($this)
      }
      [bool] Test() {
        return [TSToysCliSettings]::Test($this).InDesiredState
      }
    
      [void] Set() {
        [TSToysCliSettings]::Set($this)
      }
      #endregion PSDSC instance methods
    }
  • With dependency and base class

    class TSToysCliSettings : DscResourceBaseClass {
      [DscProperty(Key)]
      [ValidateSet("machine", "user")]
      [string]
      $SettingsScope
    
      [DscProperty()]
      [System.Nullable[bool]]
      $UpdateAutomatically
    
      [DscProperty()]
      [ValidateRange(1, 90)]
      [System.Nullable[int]]
      $UpdateFrequency
    
      [DscProperty()]
      [bool]$Exist = $true
    
      #region    DSC static methods
      static [TSToysCliSettings] Get([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceTestResult] Test([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceSetResult] Set([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [void] Delete([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [TSToysCliSettings[]] Export([TSToysCliSettings]$filteringInstance) {
        # elided for brevity
      }
      #endregion DSC static methods
      
      <#
        The base class would provide friendly handling for:
    
        - Invoking the set and delete operations without implementing the non-what-if overloads
          in the derived class.
        - Invoking the export operation without implementing the non-filtering overload in the
          derived class.
        - Invoking the PSDSC instance methods without needing to implement them in the derived class.
      #>
    }

Additional thoughts

We could certainly support both the friendly result types and the tuples for method output - it
would complicate the implementation for the adapter/module, but it would enable authors to choose
the implementation that best suits their needs.

All of this would be much friendlier with the RDK, where we could iteratively enhance the DevX for
resource authors writing PowerShell.

Separating the DSC methods from the PSDSC instance methods would also ensure we can adjust/enhance the supported definitions as the capabilities and expectations for DSC evolve without affecting the PSDSC functionality.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions