diff --git a/rfc/draft/rfc0001.md b/rfc/draft/rfc0001.md new file mode 100644 index 000000000..0a30a74f0 --- /dev/null +++ b/rfc/draft/rfc0001.md @@ -0,0 +1,1284 @@ +--- +RFC: RFC0001 # WG will set the number after submission +Author: jahelmic # <@GitHubUserName> +Sponsor: michaeltlombardi # <@GitHubUserName> +Status: Draft # +SupercededBy: null # +Version: 1.0 # . +Area: DSC # +CommentsDue: null # +--- + +# Class-based PSDSC resource contract for DSC v3 + +This RFC defines a contract for PowerShell **class-based** PSDSC resources so that a single +implementation can: + +- Continue to work with **PSDSC v1/v2**, and +- Participate fully in **DSC v3 semantics** via the PSDSC adapter, + +without requiring a hard dependency on a Microsoft-shipped "DSC types" or RDK module. + +The RFC focuses on: + +- The **method signatures** and shapes DSC v3 cares about for PowerShell class-based resources. +- The **expected return structures** (results, messages, and streams). +- How this contract **aligns with JSON schema and manifest generation**, so that tooling (RDK, + Sampler, analyzers) can build on it. + +## Motivation + +> As a DSC resource author with existing PSDSC class-based resources, I want to augment those +> resources to participate in DSC v3 semantics, so that I can support both PSDSC v1/v2 and DSC v3 +> without maintaining separate codebases. + +Additional motivations: + +- Many resources already implemented as **class-based PSDSC resources** have active users and are + expensive to rewrite. +- DSC v3 introduces richer semantics and tooling: + - Structured results from `Test`, `Set`, and `Get` (not just a "Reason" string). + - Better "diff" semantics (for latest-version / package-like scenarios). + - JSON-schema-based validation and manifest-driven discovery. +- Resource authors should be able to: + - Incrementally add **V3-only capabilities** (e.g., richer test results, schema) to existing + class-based resources. + - Avoid taking a **mandatory dependency** on a Microsoft-shipped types or RDK module. +- The DSC community (including RDK / Sampler users) needs a **clear, documented contract** so that: + - Static analysis is feasible, + - Schema/manifest generation is consistent, + - ScriptAnalyzer rules can be updated coherently for V1/V2/V3 resources. + +### DSC operation semantics + +In PSDSC, the only operations defined were: + +- `Get` to return an instance of the resource representing its actual current state. For + class-based resources, this mapped to the method signature: + + ```powershell + [] Get() {} + ``` + + The semantics of this operation are identical for both DSC and PSDSC. + +- `Test` to return a boolean value representing whether the current state of a resource instance is + correct for a given desired state. For class-based resources, this mapped to the method + signature: + + ```powershell + [bool] Test() {} + ``` + + In DSC, the `test` operation indicates not only _whether_ the resource instance is in the desired + state but which properties are non-compliant. The result an end user sees for a specific instance + looks like the following YAML snippet for a non-compliant instance: + + ```yaml + desiredState: + path: MyPackage + _exist: true + version: 1.2.3 + actualState: + name: MyPackage + _exist: true + version: 1.1.0 + inDesiredState: false + differingProperties: + - version + ``` + + Moreover, what DSC expects a resource to return is never just a boolean result: + + 1. A DSC resource that doesn't define the test operation itself can rely on DSC's synthetic + testing feature, which invokes the `get` operation for a resource and then compares the + desired state against the actual state to determine whether the instance is compliant and + which properties (if any) aren't in the desired state. + 1. A DSC resource that implements the `test` operation explicitly must return an instance of the + resource representing the actual state of the instance with the canonical `_inDesiredState` + read-only property populated with a boolean indicating whether the instance is compliant. + + The resource must return that data as a JSON Line. The resource may also emit a second JSON + Line containing a JSON array where every item in the array is a string matching the name of a + property that isn't in the desired state. + + If the resource doesn't return the second JSON Line as an array of differing properties, DSC + synthesizes the array itself by comparing the returned actual state and given desired state. + + The current implementation for the PSDSC adapter is to invoke the `Test` method for the resource + to determine whether the resource is in the desired state and compose that information with the + actual state of the resource by invoking the `Get` operation. + +- `Set` to idempotently enforce the resource instance to match the desired state. For class-based + resources, this mapped to the method signature: + + ```powershell + [void] Set() {} + ``` + + In DSC, the `set` operation returns more useful data to the user, showing them which properties + were modified and how they were changed. The result an end user sees for a specific instance + looks like the following YAML snippet for an instance that required changes: + + ```yaml + beforeState: + path: MyPackage + _exist: true + version: 1.1.0 + afterState: + name: MyPackage + _exist: true + version: 1.2.3 + changedProperties: + - version + ``` + + If the resource is implemented not to return any data from the `set` operation, DSC invokes the + `get` operation after the `set` operation concludes and uses that data to populate the + `afterState` map and the `changedProperties` array. + + If the resource is implemented to return data from the `set` operation, it _must_ return an + instance of the resource representing the actual state of the instance after the `set` operation. + + The resource must emit that data as a JSON Line. The resource may also emit a second JSON Line + containing a JSON array where every item in the array is a string representing the name of a + property that the resource modified to match the desired state. + + If the resource doesn't return the second JSON Line as an array of changed properties, DSC + synthesizes the array itself by comparing the returned actual state and given desired state. + +In addition to the differing semantics for the `Test` and `Set` operation return data, DSC supports +new operations that have no equivalence in PSDSC: + +- The `delete` operation removes an instance of the resource from a system. In the current + implementation, the `delete` operation returns no data. +- The `export` operation enumerates every instance of the resource on a system. Optionally, the + resource may support filtered `export` operations, where the user supplies a filtering instance + of the resource and only matching instances are returned. Resources may _require_ a filtering + instance in cases where enumerating every instance on a system is incoherent or may cause + critical performance issues, such as enumerating every file and folder on the system recursively. + +The contract for class-based PSDSC resources in DSC must account for these differences in semantics +to enable PSDSC resource authors to fully participate in the richer semantics of DSC. + +### Reasons pattern in PSDSC + +Due to the limited return data and operation semantics in PSDSC, many resources adopted a pattern +where they defined a read-only property named `Reasons` for the class-based DSC Resource. In +particular, this pattern was mandatory for resources to fully participate in the semantics of Azure +Machine Configuration. + +This required the resource author to: + +1. Define a class representing a _reason_ for why the resource is non-compliant. The class must + define the `Code` and `Phrase` properties with type `[string]`. The class must not define any + other properties. + + Because of type conflicts, resource authors were expected to redefine this class in their own + PSDSC module with a sufficiently distinct name to avoid conflicts across modules. +1. Add the `Reasons` property to their PSDSC resource as a non-configurable property that accepts + an array of instances of the reason class. + +An example of this pattern is shown in the following (incomplete) snippet: + +```powershell +class MyModuleReasons { + [DscProperty()] [string] $Code + [DscProperty()] [string] $Phrase +} + +[DscResource()] +class Package { + [DscProperty(NotConfigurable)] [MyModuleReasons[]] $Reasons +} +``` + +Implementing the reasons pattern required a resource author to populate compliance information in +the `Get` operation, since neither `Test` nor `Set` returned an instance of the resource. This +effectively shifts the logic for testing a resource instance from the `Test` method into the `Get` +method. + +This pattern arose due to limitations in the data that could be returned from a PSDSC resource. In +DSC, by contrast: + +- The `test` and `set` operations provide richer semantics for the operation results. The test + result data indicates whether the instance is in the desired state _and_ provides a list of + non-compliant properties along with both the desired state and actual state data. The set result + shows which properties the resource modified as well as the before and after state. + +- DSC supports resources emitting arbitrary messages during an operation which can serve to enhance + the level of detail a caller receives for that operation. + + For example, a DSC resource could emit messages for every non-compliant property that indicates + how and why that property is non-compliant. This information is bubbled up to the user but + doesn't affect the actual result object for the instance. + +- DSC supports a pattern for resources to emit additional metadata. The reasons pattern could be + incorporated into this idiom if emitting additional messages is insufficient. + +## Proposed experience + +This section describes how the contract feels for a resource author and for DSC v3 consumers. + +### Explicitly out of scope concerns + +Before describing the contract for defining a PowerShell class with methods for DSC, note that the +following concerns are out-of-scope for this RFC, though the authors admit that these concerns are +_related_, they should be discussed in separate RFCs: + +- Defining a DSC resource as a PowerShell class without the `[DscResource()]` attribute is + explicitly out of scope for this PR. There are concerns around discovering and surfacing such + resources from a PowerShell module that require their own RFC. + + The contracts for operation methods defined in this RFC are also a prerequisite for any such + RFC. + +- DSC introduces the concept of _canonical resource properties_. Every canonical resource property + has a standard JSON Schema and has a property name that begins with an underscore (`_`), like the + `_exist` canonical property. The purpose of canonical properties is to provide a set of properties + with specific semantics that the DSC engine and higher order tools may rely on. + + There are some concerns around canonical properties that need to be addressed for DSC resources + implemented as PowerShell classes, including but not limited to: + + - The non-idiomatic status of naming a PowerShell class property with a leading underscore. + - The automatic detection of whether a resource defines canonical properties without inspecting + the resource's JSON Schema. + - Detecting and enforcing correctness for the definition of a canonical property on a PowerShell + class. + + All of these concerns are out of scope for this RFC. + +- DSC expects resources to emit messages to the `stderr` stream as JSON Lines containing an object + with a single key-value pair. The key defines the level of the message, such as `trace` or `debug` + while the value defines the actual message. + + How these messages are emitted from a class and made available to DSC from the PowerShell adapter + is a separate concern from the method signatures for resource operations and enabling resources + implemented as PowerShell classes to participate in the operational semantics of DSC. This concern + is being addressed separately. + +- Static analysis of the PowerShell class to generate a JSON Schema for the DSC resource is out of + scope for this RFC and being addressed separately. + +- Generation of reference documentation from the AST of the PowerShell class is out of scope for + this RFC. Any such proposal: + + 1. Is dependent on the RFC for defining the data model for reference documentation of a resource, + which isn't filed yet. + 1. Would probably depend heavily on enhanced parsing and semantics for comment-based help, which + will itself merit an RFC. + +- Enhanced developer experience features, like a base class, reusable attributes, and generic return + types are all out of scope for this RFC. Any design for those enhancements to the developer + experience necessarily depend on this RFC. + +### Authoring a class-based resource that works for PSDSC v1/v2 and DSC v3 + +> [!NOTE] +> This RFC uses a hypothetical resource named `SoftwarePackage` in example code snippets. The +> resource has three properties: +> +> - `Name` - the name of the software package. +> - `Version` - the semantic version of the software package. +> - `Exist` - whether the software package should be installed. +> +> While it is more typical for PSDSC resources to use the `Ensure` property as a convention, the +> design for migrating PSDSC resources from defining `Ensure` to using the `_exist` canonical +> property is out of scope. For the purposes of this RFC, the `SoftwarePackage` PSDSC resource +> was already using the `Exist` boolean property so we can ellide that conversation, deferring it +> to a future RFC about canonical properties for class-based resources. +> +> In the example snippets for this RFC, the implementation for the methods is either elided or +> has the method simply wrapping a function call. This is both for brevity and because the RFC +> authors expect this to be the primary way that PowerShell developers implement class-based +> resources based on experience in the community. This is **not** a requirement for the method +> signatures. Static analysis of a PowerShell class can inspect the signatures without having any +> opinion about the implementation details _within_ those methods. + +A resource author today might have: + +```powershell +[DscResource()] +class SoftwarePackage { + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Version + + [DscProperty()] + [bool] $Exist = $true + + [SoftwarePackage] Get() { + # Elided implementation + } + [void] Set() { + # Elided implementation + } + [bool] Test() { + # Elided implementation + } +} +``` + +With this RFC, the author can introduce: + +```powershell +[DscResource()] +class SoftwarePackage { + # DSC methods + static [string] InstanceJsonSchema() { + return @{ + type = 'object' + required = @('name') + properties = @{ + name = @{ type = 'string' } + version = @{ type = 'string' } + _exist = @{ '$ref' = 'https://aka.ms/dsc/schemas/v3/resource/properties/exist.json' } + } + } + } + + static [System.Tuple[bool, SoftwarePackage, String[]]] Test( + [SoftwarePackage]$instance + ) { + return Test-SoftwarePackageResource -Instance $instance + } + + static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance + ) { + Set-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage] Get( + [SoftwarePackage]$instance + ) { + return Get-SoftwarePackageResource -Instance $instance + } + + static [void] Delete( + [SoftwarePackage]$instance + ) { + Remove-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + return Export-SoftwarePackageResource + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + if ($null -eq $filteringInstance) { + throw 'Invalid operation' + } + + return Export-SoftwarePackageResource -FilteringInstance $filteringInstance + } + + # PSDSC methods + [SoftwarePackage] Get() { + return Get-SoftwarePackageResource -Instance $this + } + + [bool] Test() { + return Test-SoftwarePackageResource -Instance $this | + Select-Object -ExpandProperty Item1 + } + + [void] Set() { + Set-SoftwarePackageResource -Instance $this + } +} +``` + +Key changes: + +- All DSC v3-relevant methods are **static** and _optional_. If the class doesn't implement a + static method for an operation, DSC can use the PSDSC instance method for that operation. +- A single class supports **both PSDSC and DSC v3**. +- Authors may optionally return richer structured data for DSC v3. +- No mandatory dependency on Microsoft-owned types. + +### Using the resource in DSC v3 + +Example configuration: + +```yaml +resources: +- type: Contoso.DSC/SoftwarePackage + name: InstallGit + properties: + Name: git +``` + +DSC v3 (via the PSDSC adapter): + +- Validates JSON against the schema returned by the `InstanceJsonSchema()` method. +- Calls `Test`, `Get`, `Set` with class-based resource instances. +- Accepts both simple and structured returns. +- Emits structured messages and differences. + +Resource consumers see **consistent behavior**, regardless of whether the implementation is native +DSC or an adapted PSDSC class-based resource. + +## Specification + +### Resource class shape + +A PowerShell class-based resource compatible with DSC _must_ define a PowerShell class that is also +a valid PSDSC resource. This requires the class to: + +- Have the `[DscResource()]` attribute +- Define at least one property with the `[DscProperty(Key)]` attribute. +- Define the PSDSC instance methods `Get`, `Set`, and `Test` with the corect signatures. + +To enhance the class for participating in the semantics of DSC, the author may also implement +static methods on the class that adhere to the signatures and contracts defined in this RFC. + +Class skeleton (methods only): + +```powershell +[DscResource()] +class { + # PSDSC (instance) methods + [] Get() {} + [bool] Test() {} + [void] Set() {} + # DSC (static) methods + static [] Get([]$instance) {} + static [] Set([]$instance) {} + static [] Test([]$instance) {} + static [[]] Export() + static [[]] Export([]$filteringInstance) {} + static [void] Delete(...) + static [] InstanceSchema(...) +} +``` + +> ![NOTE] +> With the exception of ``, all types with the wrapping angle brackets, like +> `[]` are placeholders that are defined later in this RFC. +> +> `` always refers to the name of the implementing PowerShell class, like +> `ExampleResource` in the following snippet: +> +> ```powershell +> class ExampleResource { +> static [ExampleResource] Get([ExampleResource]$instance) {} +> } +> ``` + +> [!IMPORTANT] +> _Every_ static method signature is optional. When the class doesn't define a given static method, +> the PowerShell adapter for DSC handles the resource like a classic PSDSC resource. + +### DSC operation method selection + +If a class has the `[DscResource()]` attribute, DSC and the adapter know that the resource class +implements the traditional PSDSC resource methods `Get()`, `Set()`, and `Test()`. + +When selecting the method to use for an operation, the adapter: + +1. Checks for the existance of a DSC static method for that operation. +1. If the resource class implements a static method for the operation, DSC invokes that method. +1. If the class doesn't implement a static method for the operation and the operation is part of + the PSDSC resource API, DSC uses the appropriate PSDSC instance method. +1. If the class doesn't implement a static method or instance method for the operation, the + resource can't be used for that operation and DSC raises an error. + +> [!NOTE] In this model, we _can_ support classes defined for DSC that don't have the +> `[DscResource()]` attribute and thus may not have the PSDSC instance methods. Supporting these +> classes is out of scope for this RFC and should be addressed in a future RFC. + +### Method signatures + +This RFC proposes the following new method signatures. Each section defines method signatures for a +different DSC resource operation. In this proposal, we use the `Tuple` type for structured return +data. This enables static analysis and implementation without any dependencies for defined types. + +Future RFCs may: + +- Introduce strongly-typed result classes (e.g., `DscTestResult`) that map to the same shape. +- Define a shared types module that authors can optionally reference. + +#### Get operation method + +Signature: + +```pwsh +static [] Get([]$instance) +``` + +The `get` operation must always return the actual state of the instance with all discoverable +properties populated. There are no meaningful differences in the semantics for the `get` operation +between DSC and PSDSC. + +#### Set operation method + +##### DSC Context for set operations + +There are meaningful differences between the semantics of the `set` operation between DSC and +PSDSC: + +1. For PSDSC, resources aren't expected to return any data. In contrast, the result for a `set` + operation against a specific resource instance in DSC takes the following shape: + + ```yaml + beforeState + name: foo + version: 1.2.3 + _exist: true + afterState + name: foo + version: 1.3.0 + _exist: true + changedProperties: + - version + ``` + +1. In DSC, a non-adapted resource manifest defines the following fields that influence how and + when DSC invokes the operation for the resource and what DSC expects the resource to return: + + - The `set.return` field defines the output DSC should expect for the resource. When the + manifest omits this field, DSC synthesizes the result object by invoking the `get` operation + for the resource after the `set` operation finishes. + + - The `set.implementsPreTest` field indicates whether DSC should invoke the `test` operation to + determine whether to invoke the `set` operation. + + When the manifest omits this field or defines it as `false`, DSC invokes `test` for the + resource. DSC only invokes the `set` operation for the resource when the test result indicates + that the resource isn't in the desired state. + + When the manifest defines this field as `true`, DSC invokes the `set` operation for the + resource without invoking `test` first. Functionally, this manifest field explicitly indicates + that the resource's `Set` operation is idempotent. + + - The `set.handlesExist` field indicates whether DSC can invoke the `set` operation to delete + an instance of the resource. This only applies to resources that have the `_exist` canonical + property and define the `delete` operation. + + If the manifest doesn't define this field or defines it as `false` and _does_ define the + `delete` method, DSC invokes the `delete` method whenever a user invokes the `set` operation + for the resource with the `_exist` property defined as `false` in the desired state. + + If the manifest defines this field as `true`, DSC invokes the `set` operation for the resource + when the caller invokes the set operation, like with the `dsc resource set` or + `dsc config set` commands. It does so even when the `_exist` property is defined as `false` in + the desired state. + + This setting does _not_ affect whether DSC invokes the `set` operation when the caller + explicitly invokes the delete operation, like with the `dsc resource delete` command. + +1. DSC supports invoking the `set` operation for a resource in `whatIf` mode. By default, DSC is + able to synthesize how a resource will change the system by invoking the `test` operation for + the resource and converting the result to show how the `set` operation will modify the system. + + However, a synthetic what-if is not always sufficient for showing how a resource will modify the + system when the `set` operation is invoked. + + Consider an example where a package resource supports defining a version range, not just + specific versions. If the version range specifies that the resource must be between versions + `1.2.0` and `1.5.0`, the test operation will report that the resource is in the desired state + when the current version is `1.2.3`. Depending on the implementation of the resource, invoking + the `set` operation may upgrade the package to version `1.3.0`. To better show the difference, + compare the following result snippets between the synthesized changes and the actual changes: + + ```yaml + # synthetic what-if result + beforeState: + name: foo + version: 1.2.3 + _exist: true + afterState: + name: foo + version: 1.2.3 + _exist: true + changedProperties: [] + --- + # Implemented what-if result + beforeState: + name: foo + version: 1.2.3 + _exist: true + afterState: + name: foo + version: 1.3.0 + _exist: true + changedProperties: + - version + ``` + + To support resources that need to implement their own logic to correctly indicate how the + resource will modify system state, resource authors can define the `whatIf` field in the + resource manifest. Then, if a user invokes the `set` operation for the resource in `whatIf` + mode, DSC invokes the resource using that information instead of the actual `set` definition. + In all respects except for the `command` and `args` sub-fields, DSC requires the `set` and + `whatIf` sections of the manifest to be identical. + +For the purposes of this RFC: + +1. The return data and inferring the effective value of the `set.return` manifest field can be + accomplished with a normal class method signature. Supporting the definition of resources to + match those manifest values with different method signatures is in scope. +1. Support for explicitly implemented `whatIf` mode can also be inferred from a normal class method + signature and is in scope. +1. Instead of further complicating the class with additional methods, this RFC sets the contract + for the `set.implementsPretest` field to be `false`. DSC (and the adapter) will always invoke + the `test` method to determine whether to actually invoke the `set` method. For more information + about this decision, see + [Alternate proposals and considerations](#alternate-proposals-and-considerations). +1. Instead of further complicating the class with additional methods, this RFC sets the contract + for the `set.handlesExist` field to be `true`. This is because the PSDSC `Set()` instance method + contract already requires PSDSC resources to handle creating, updating, and deleting instances + that can be created and deleted. For more information about this decision, see + [Alternate proposals and considerations](#alternate-proposals-and-considerations). + +##### Proposed signatures for set + +- No return data (DSC invokes `get` after `set` to generate after state and changed properties): + + ```pwsh + static [void] Set([]$instance) + ``` + + This maps to a non-adapted DSC resource that omits the `set.returns` field in its manifest. + +- Return state only (DSC generates the changed properties arrray): + + ```pwsh + static [] Set([]$instance) + ``` + + This maps to a non-adapted DSC resource that defines the `set.returns` field in its manifest as + `state`. + +- Return state and changed properties (DSC uses the result without processing): + + ```pwsh + static [System.Tuple[, String[]]] Set([]$instance) + ``` + + This maps to a non-adapted DSC resource that defines the `set.returns` field in its manifest as + `stateAndDiff`. + +- To indicate that the resource supports `whatIf` mode operations as well as `actual`, the class + should define a method signature that expects a boolean parameter after the instance parameter: + + ```pwsh + # No return data + static [void] Set([]$instance, [bool]$whatIf) + # state return kind + static [] Set([]$instance, [bool]$whatIf) + # stateAndDiff return kind + static [System.Tuple[, String[]]] Set([]$instance, [bool]$whatIf) + ``` + +The `set` operation may use one of three return types: + +- The `[void]` return type maps to the same behavior and handling as the PSDSC `Set()` instance + method and a command resource that omits the `set.return` field in its manifest. + + In this case, the DSC engine invokes the `get` operation for the resource after `set` completes + to construct the `afterState` and `changedProperties` fields of the set result. + +- The `[]` return type maps to the DSC `state` value for the `set.return` manifest + field of a command resource. + + In this case, the DSC engine populates the `afterState` field of the set result with this return + data. It compares the `beforeState` and `afterState` fields property-by-property, performing a + strict equivalence check (case sensitively). Any properties with non-equal values in the + `beforeState` and `afterState` fields are inserted into the array for the `changedProperties` + field of the result. + +- The `[System.Tuple[, String[]]]` return type maps to the DSC `stateAndDiff` value + for the `set.return` manifest field of a command resource. + + In this case, the DSC engine populates the `afterState` and `changedProperties` fields of the set + result with the return data. DSC doesn't munge the output. + +> [!NOTE] +> In all cases, DSC _always_ validates any returned state data against the defined JSON Schema for +> the resource instance. This is intended to ensure that resources are matching their own JSON +> Schema in their return data and helps resource authors catch mistakes early in their development +> lifecycle, since acceptance tests will immediately catch this kind of violation. + +The `set` operation may support `whatIf` mode invocations. In this mode, the resource doesn't +change the system. Instead, it reports _how_ it would modify the system. The return data for this +operation is the _expected_ final state and changed properties. The return type for the what-if +method _must_ be the same as any defined `set` method signature without the `[bool]$whatIf` +parameter, such as: + +```pwsh +static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance, + [bool]$whatIf +) { + # Implementation +} +static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance +) { + [SoftwarePackage]::Set($instance, $false) +} +``` + +If the two methods have different return values, static analysis should consider the implementation +to be invalid. For example, the following implementation should cause static analysis to identify +the resource implementation as invalid: + +```pwsh +static [SoftwarePackage] Set( + [SoftwarePackage]$instance, + [bool]$whatIf +) { + # Implementation +} + +static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance +) {} +``` + +#### Test operation method + +##### DSC Context for test operations + +There are meaningful differences between the semantics of the `set` operation between DSC and +PSDSC: + +1. For PSDSC, resources are expected to return a simple boolean result indicating whether the + resource is in the desired state. For DSC, the result for a `test` operation against a specific + resource instance takes the following shape: + + ```yaml + desiredState + name: foo + version: '[1.3.0,2.0.0)' # Nuget version requirement + _exist: true + actualState + name: foo + version: 1.2.3 # actual semantic version + _exist: true + inDesiredState: false + differingProperties: + - version + ``` + +1. In DSC, implementing the `test` operation is entirely optional, while it's mandatory for PSDSC. + + When a DSC resource doesn't implement the test operation (as indicated by not defining the + `test` section in the resource manifest), DSC performs a synthetic test for the resource + instance by invoking the `get` operation to populate the `actualState` field for the result. DSC + then compares every property defined in the `desiredState` against that same property in + `actualState`. The comparison is a strict equivalence check (case-sensitive, order insensitive). + Any properties with differing values between `desiredState` and `actualState` are inserted into + the `differingProperties` array field of the result. + + For non-adapted resources that implement the `test` operation, DSC _requires_ the resource to + define the `_inDesiredState` canonical resource property and return a boolean value for that + property in the return data for the `test` operation. DSC hoists that return value into the + `inDesiredState` field of the result object. + + If the non-adapted resource defines the `test.return` field of its manifest as `stateAndDiff`, + DSC expects the resource to also return the array of property names that aren't in the desired + state as a JSON Line after the JSON Line containing the actual state of the resource (which must + include the `_inDesiredState` canonical property). + +For the purposes of this RFC: + +- The `test` operation is mandatory, since any class with the `[DscResource()]` attribute _must_ + implement the `Test()` instance method. Defining one of the proposed static method signatures + simply enables the resource to provide better information to users as enabled by the semantics of + DSC. + +- The contract of the adapter can obviate the need to define an `_inDesiredState` class property + and include it in the resource instance JSON Schema. This RFC proposes signatures that send the + boolean value as part of the return data to the adapter to avoid defining an awkward class + property to simplify the resource's configurable API. This is particularly relevant when + considering the probability that resource authors developing classes for DSC resources will want + their resources to function idiomatically with both DSC and PSDSC. + + The adapter can correctly handle any required munging when returning the JSON for the `test` + operation to DSC. Handling the existence of the `_inDesiredState` canonical property in terms of + JSON Schema should be deferred to a future RFC about handling canonical properties for + class-based resources. + +##### Proposed signatures for test + +- Return state only (DSC generates the differing properties array): + + ```pwsh + static [System.Tuple[bool, ]] Test([] $instance) + ``` + + This maps to a non-adapted DSC resource that omits the `test.return` field in its manifest or + defines it as `state`. + +- Return state and differing properties (DSC uses the result without processing): + + ```pwsh + static [System.Tuple[bool, , String[]]] Test([] $instance) + ``` + + This maps to a non-adapted DSC resource that defines the `test.return` field in its manifest as + `stateAndDiff`. + +The `test` operation may use one of two return types: + +- The `[System.Tuple[bool, ]]` return type maps to the DSC `state` value of the + `test.return` field in the manifest of a non-adapted resource. Instead of requiring the class to + define the `InDesiredState` read-only property, DSC expects the resource to return the boolean + value _and_ the actual state of the resource. The adapter munges the result for DSC. + + When a resource uses the `state` return kind, DSC populates the `actualState` field of the result + object with the returned data. DSC then compares every property defined in the `desiredState` + against that same property in `actualState`. The comparison is a strict equivalence check + (case-sensitive, order insensitive). Any properties with differing values between `desiredState` + and `actualState` are inserted into the `differingProperties` array field of the result. + +- The `[System.Tuple[bool, , String[]]]` return type maps to the DSC `stateAndDiff` + return kind for a command resource. + + When a resource uses the `stateAndDiff` return kind, DSC populates the `actualState` field of the + result with the returned data. DSC populates the `differingProperties` field of the result with + the returned array. + +#### Export operation method + +##### DSC Context for export operations + +PSDSC has no API or semantics for discovering and returning every instance of a resource on a +system. DSC introduces the `export` operation to enable users and higher order tools to query for +every instance of a resource with a single command. DSC also supports filtered export operations, +where the user passes a filtering instance to limit the return data for the operation. + +Regardless of whether the results are filtered, DSC expects the resource to emit a JSON Line +containing the actual state of the discovered instances. + +For the purposes of this RFC: + +- Currently, DSC doesn't distinguish between a JSON Schema that validates the properties of an + actual resource instance from the JSON Schema validating a filter for export. This is being + addressed separately and is out of scope for this RFC. +- Class-based resources should be able to support filtered export and/or unfiltered export. For + some resources, _only_ filtered exports make sense, like a resource that manages files, to avoid + accidentally enumerating every file on the system. For other resources, filtered exports may not + make sense, such as a resource for managing the system timezone which will only ever return a + single instance. + + Choosing whether to support filtered and/or unfiltered exports should be up to the resource + author and discoverable from the method signatures on the PowerShell class. + +##### Proposed signatures for export + +- Non-filtering export (resource returns every discovered instance): + + ```pwsh + static [[]] Export() + ``` + +- Filtered export (resource uses the input instance to limit the return data): + + ```pwsh + static [[]] Export([]$filteringInstance) + ``` + +The return type for the `export` operation is always an array of instances of the resource class. + +The export functionality depends on which method signatures are implemented: + +- If the class implements both signatures, it supports filtered and unfiltered exports. +- If the class implements only the parameterless signature, it doesn't support filtered exports. +- If the class implements only the signature with a filtering instance, it doesn't support + unfiltered exports. +- If the class doesn't implement either signature, it doesn't support the `export` operation. + +#### Delete operation method + +##### DSC context for delete operations + +PSDSC has no explicit API or semantics for deleting an instance of a resource on a system. DSC +introduces the `delete` operation to simplify: + +1. Removing resource instances for users, who can use the `dsc resource delete` command without + having to explicitly specify `_exist` as `false` in the input JSON, which they would have to + do for the `set` operation. + +1. Implementing resources for authors, who can define implement the `set` operation to create and + update instances of the resources and only handle deleting instances in the `delete` operation, + simplifying the code logic for `set`. + +The `delete` operation is only intended for resources that define the `_exist` canonical property. +As noted in [DSC Context for set operations](#dsc-context-for-set-operations), DSC resource authors +have some choice in whether the `set` operation must handle deleting instances. + +For the purposes of this RFC: + +- Implementing the static `Delete()` method is entirely optional. DSC will only ever invoke this + method when a user specifically invokes the `delete` operation, as with the `dsc resource delete` + command. Even if the resource defines the `_exist` canonical property and the `Delete()` method, + DSC will still invoke the `Set()` method for all `set()` operations. + +##### Proposed signatures for delete + +```pwsh +static [void] Delete([]$instance) +``` + +In the current data model for DSC, the `delete` method returns no data. Only messages and execution +status (success or failure) are reported back to the engine. While there are ongoing discussions +about extending the data model to support optionally returning data from the `delete` operation, +those concerns are out of scope for this RFC. + +If the data model is updated in the future then this RFC should be similarly amended to clarify +how a class-based resource can participate in those semantics by defining additional method +signatures. + +#### InstanceSchema method + +##### DSC context for JSON Schemas + +In PSDSC, the schema for the properties a user could specify to configure a resource was defined +either by a MOF file file or by the properties of the PowerShell class with the `[DscProperty()]` +attribute. + +In DSC, the schema is defined as a JSON Schema either emitted from a command or embedded in the +resource manifest, with embedded schemas being the preferred option. + +There are several other meaningful differences between what could be expressed in the schema for +a PSDSC resource and a DSC resource: + +- PSDSC resource properties could be annotated as being: + + - _Key_ properties, which uniquely identify an instance of the resource together. This prevented + PSDSC configuration authors from defining conflicting instances. + + DSC does not yet have a way to represent key properties in the JSON Schema. This is planned + work. + - _Mandatory_ properties, which the resource always requires the user to explicitly define. + + In JSON Schema and DSC, this is represented with the `required` keyword, which takes an array of + property names that are mandatory. + - _NotConfigurable_ properties, which the resource could return but the user couldn't enforce. An + example of such a property is the `LastWriteTime` for a file. + + In DSC, properties that define the `readOnly` JSON Schema keyword have the same behavior. + +Additionally, DSC supports defining the JSON Schema for a property with the `writeOnly` keyword, +which indicates that a user can send the property to the resource but the resource will never +return it. This is used conventionally in DSC resources for passing secrets to a resource and for +defining directives that change the behavior of a resource but can't be represented in the actual +state of a resource. An example of this sort of property is the `_purge` canonical property. + +For the purposes of this RFC: + +- How the adapter can infer a JSON Schema from the class definition is explicitly out of scope. + Currently, the PowerShell adapter for PSDSC resources skips validation. This is an ongoing design + concern that is being addressed separately. +- Programmatically managing the differences in what can be represented in the PSDSC schema and the + DSC JSON Schema is out of scope. If a class-based resource provides a JSON Schema to DSC, DSC + will use that to validate the data that a user supplies. +- Defining a different JSON Schema to validate filtering instances of the resource for the `export` + operation is out of scope. The potential conflict for a strictly defined JSON Schema that + validates a configurable instance of the resource and a more lax JSON Schema that validates a + filtering instance is a known design problem that is being addressed separately. + + The only concession to this known problem that the RFC makes is to use a more specific name for + the proposed method than simply `Schema`. + +##### Proposed signatures for emitting a JSON Schema + +Signatures: + +```pwsh +static [string] InstanceJsonSchema() +``` + +DSC expects the resource to return a string representation of the resource instance JSON Schema. +The output must validate against the resource instance meta schema at the following JSON pointer +URI: + +```text +https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/manifest.schema.json#/properties/embedded +``` + +In the current implementation of DSC, no pre-validation is performed for the resource properties +of adapted PSDSC resources. If the class implements this method, DSC and the adapter will validate +the user-supplied data with the JSON Schema emitted by the method. + +As noted above, generating a JSON Schema from the properties of the class is out of scope for this +RFC. + +While programmatically reconciling the differences in what can be represented in JSON Schema and +how PSDSC interprets class properties with the `[DscProperty()]` is out of scope for this PR, here +we make a few recommendations to resource authors: + +1. Annotate every property relevant to using the class as a DSC resource with the + `[DscProperty()]` attribute as normal. _Do not_ define properties in the JSON Schema that + aren't annotated with the attribute. + +1. When defining write-only properties, which are only supported by DSC (not PSDSC): + + - Clearly document the behavior of those properties for PSDSC users. Ensure that PSDSC users + understand that they can define a value for those properties but will _never_ get a non-null + value for them from the `Get()` method. + + - If the property type is a value type, like `[bool]` or `[int]`, define the type as nullable + (`[System.Nullable[bool]]` or `[System.Nullable[int]]`) + + - Always explicitly set the value for write-only properties to `$null` on the instance that you + are returning for a DSC or PSDSC operation. + +1. Define the property names in Pascal case as normal, like `[string] $RequiredVersion` and not + `[string] $requiredVersion`. + + The casing that you define in the JSON Schema will be the casing that DSC expects and uses to + validate input, but neither the class itself nor PSDSC care about casing. +1. When defining a canonical property, define the property name in PowerShell _with_ the leading + underscore and correct casing, like `[bool] $_exist = $true` instead of `[bool] $Exist = $true`. + + While a future RFC (or enhancement to this RFC) may provide better programmatic translation of + the names for canonical properties, this will make the class immediately usable with the engine + semantics for canonical properties. + +### Adapter considerations + +This RFC assumes a PSDSC-aware adapter for DSC that: + +- Can load class-based PSDSC resources and recognize the proposed contract. +- May be shipped: + - As part of the PSDSC module, or + - As a separate adapter module with a dependency on PSDSC. + +The adapter will also be responsible for the following behaviors: + +1. Inserting the `_inDesiredState` canonical property into the emitted JSON Schema. This property + isn't represented in the PowerShell class as a property and is _built into_ the contract for + class-based resources. +1. Converting the return data for resource operations into JSON Lines for DSC. +1. Indicating the resource capabilities to DSC based on static analysis. +1. Capturing PowerShell stream messages from a resource and emitting them as the correctly + structured JSON Line to stderr. This is already being addressed separately. + +> OPEN: +> +> - Final shipping model (in-module vs separate module) and versioning strategy. - How to clearly +> communicate to users what changed when PSDSC or the adapter is updated. + +## Detailed examples + +The following snippet defines a resource adhering to this contract. Each of the following examples +shows how the resource itself behaves and how DSC leverages the contract with the resource through +the adapter. + +```powershell +[DscResource()] +class SoftwarePackage { + #region Properties + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Version + + [DscProperty()] + [bool] $_exist = $true + #endregion Properties + #region DSC methods + static [string] InstanceJsonSchema() { + return @{ + type = 'object' + required = @('name') + properties = @{ + name = @{ type = 'string' } + version = @{ type = 'string' } + _exist = @{ '$ref' = 'https://aka.ms/dsc/schemas/v3/resource/properties/exist.json' } + } + } + } + + static [System.Tuple[bool, SoftwarePackage, String[]]] Test( + [SoftwarePackage]$instance + ) { + return Test-SoftwarePackageResource -Instance $instance + } + + static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance + ) { + Set-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage] Get( + [SoftwarePackage]$instance + ) { + return Get-SoftwarePackageResource -Instance $instance + } + + static [void] Delete( + [SoftwarePackage]$instance + ) { + Remove-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + return Export-SoftwarePackageResource + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + if ($null -eq $filteringInstance) { + throw 'Invalid operation' + } + + return Export-SoftwarePackageResource -FilteringInstance $filteringInstance + } + #endregion DSC methods + #region PSDSC methods + [SoftwarePackage] Get() { + return Get-SoftwarePackageResource -Instance $this + } + + [bool] Test() { + return Test-SoftwarePackageResource -Instance $this | + Select-Object -ExpandProperty Item1 + } + + [void] Set() { + Set-SoftwarePackageResource -Instance $this + } + #endregion PSDSC methods +} +``` + +> [!NOTE] +> The snippet doesn't define the functions these methods call. The output in the examples is +> effectively mocking the behavior of those functions to limit the cognitive load for reviewing +> the examples in this RFC. + +### Example: Validating input + +todo + +### Example: Getting current state + +todo + +### Example: Testing desired state + +todo + +### Example: Enforcing desired state + +todo + +### Example: Deleting an instance + +todo + +### Example: Exporting instances + +todo + +## Alternate Proposals and Considerations + +### Functions as the primary contract + +An alternative approach was to make top-level functions the DSC v3 contract surface, using the +class only for schema: + +- Pros: + - Familiar for PowerShell users who prefer functions over classes. +- Cons: + - Requires DSC to reason about a more complex combination of functions and classes. + - Static analysis and manifest generation are simpler with everything on the class. + - Harder to express the contract as a single analyzable unit. + +This RFC proposes **static class methods** as the primary contract, with authors free to delegate +to functions internally. + +### Mandatory shared types module + +Another alternative was to require all resources to depend on a shared types module (for result +types, attributes, etc.): + +- Pros: + - Strong typing and IntelliSense for result objects and attributes. + - Clear place to evolve shared patterns. +- Cons: + - Introduces "dependency hell" for resource authors and consumers. + - Complicates versioning and servicing. + - Not necessary for basic functionality; generic structured returns are sufficient. + +This RFC opts for: + +- Generic structured forms (tuples) to represent resource output. + +Defined types can be addressed in the future with a separate RFC. + +### Resource metadata and manifest equivalence + +Omitted from the current iteration of this proposal, but not _explicitly_ out of scope for this RFC: + +- Enabling greater flexibility in defining the resource, such as choosing the equivalent value for + `set.handlesExist`. For the purposes of this RFC, the authors have chosen to define the contract + in a way that requires as little additional work and consideration for resource authors who are + defining PSDSC resources but want to take advantage of DSC semantics. + + Options like `handlesExist` and `implementsPretest` are valid considerations for DSC that are + entirely ignored by PSDSC. Instead of leading resource authors to develop resources that behave + differently in terms of "what happens to the system when I invoke the `set` operation for this + resource" depending on whether the resource is invoked through DSC (and the adapter) or PSDSC, + this RFC prefers consistency for the end user regardless of invoking tool. + + Further, the idiomatic way to represent these options would be with attributes on the static + `Set()` method. Given that this RFC was explicitly intended to avoid requiring resources to be + aware of any new types, no idiomatic proposal could be made. + + In the future, when DSC supports resources implemented as PowerShell classes that are _not_ + compatible with PSDSC, _those_ classes (and the RFC defining their contract) should support the + enhanced flexibility. + +- Defining static metadata for the resource as represented by the `description`, `author`, `tags`, + and other fields in a DSC resource manifest. + + While we _could_ define these as static properties, the static properties for PowerShell classes + are _not_ immutable. If the static property for a class is modified in a runspace it will return + the new value for that property whenever you access that property. In practice, the authors + think that this is a minimal concern, since DSC creates a new PowerShell process whenever it + invokes the adapter, but for test and other integration purposes it can be a problem. + + On the other hand, defining them as static methods that just return a string or array of strings + also seems a little strange. It is more idiomatic to represent this information with attributes + or by parsing comments. However, both of those approaches require much more work than defining + simple method contracts, and so are considered out of scope for this RFC. + +### RDK on the critical path + +The working group explicitly does **not** want the Resource Development Kit (RDK) on the critical +path: + +- RDK should be able to build on this contract once defined. +- The contract and adapter behavior must stand on their own. +- Community and Sampler-based tooling can implement schema/manifest generation independently. + +## Related work items + +- Issue: "Define method signatures for PSDSC resource classes" (link TBD) + + Describes the need to clarify what methods and signatures DSC v3 should look for on class-based + resources. + +- Future (potential separate RFCs): + - PSDSC v2 adapter for DSC v3 (shipping model and behavior). + - JSON schema/manifest specification for DSC v3 resources. + - ScriptAnalyzer rule set for V1/V2/V3 DSC resources, including class-based patterns.