Skip to content

draft of command based resources #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 377 additions & 0 deletions specs/Command-DSCResources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
# Command based resources for DSC v3

Configuration is based on a declarative idempotent model that allows users to get, set, and test settings values.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to also expose API and not just cmdline tool

Copy link
Collaborator

@gaelcolas gaelcolas Jan 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. You start by describing "Configuration" that is the PoC you've built, shouldn't talk about the problem first?
  2. Why an API is needed? If the tool/utility accepts JSON input, is an API needed and why? I guess showing the purpose first would help understand the why.
    Creating an API for it means you already think about some use cases or integration you haven't described.

Configuration relies on resources to perform the actual domain specific work.
To enable customers and partners to more easily participate in authoring resources,
we need a simplified model that is not tied to any specific programming language.

However, we still need to support the existing resources that are written in PowerShell including
class based and script functions based resources.
Comment on lines +8 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
However, we still need to support the existing resources that are written in PowerShell including
class based and script functions based resources.

I'd say you don't need this for the purpose of the tool.
You want this to be supported, so that motivation should be recorded in the context (Why such tool would be useful? What's the business case?)


## Commands as resources

Resources are not required to be standalone executables,
but they must be able to execute as a command-line with optional arguments.
This enables resources to be written in python, PowerShell script, or could be a standalone executable written in
Go, Rust, etc...

Communication to and from the resource will be via STDIN or arguments for input and STDOUT for output along
with using the exit code to indicate success or failure and STDERR for informative messages.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All output should be serialised structured data (JSON), with schema.


> [Note] We should reserve a range of exit codes for standard errors and leave the rest for custom resource use

### Resource discovery

Any "command" (which can be a script requiring a host runtime) that particpates would have a command manifest file with the name
"<command>.dscresource.json" that would be found within the `PATH` environment variable.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the plug-in model I described above, the discovery should be handled by that plug-in.
Having config resource manifest in the PATH would not make sense for DSC resources.
If the plug-ins implement a discovery method/action, then config can ask the plugins for their available resources.
The DSC Resource plug-in would do a Get-DscResource (no need for JSON manifest anymore, the plug-in would return the JSON manifest in some ways, from the DSC resources it found).


PowerShell based resources can have an optional `.dscresource.json` file to indicate if the resource should be hosted by powershell.exe or pwsh.exe (see below).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PowerShell and pwsh should tell which DSC Resource is available, and the resource modules should say which one works and which one doesn't with a given version of PowerShell.
The plug-in specifies which version of WPS or pwsh it supports or not (i.e. pwsh_psdscv3 plug-in works with PS 7.3.1, so Get-DscResource in PS 7.3.1 won't show DSC Resources that are only supported in WPS such as SharePointDsc, because its Module manifest says so).
If a DSC Resource module "says" it supports both, then where to run it should be defined when declaring the configuration, similar to the "which user to run this config as" (PSDscRunAsCredential)...

This file would be discovered via the existing `PSModulePath` environment variable along with the resource module.

For PSDesiredStateConfiguration module, in addition to searching for PowerShell class resources,
it would also search through the `PATH` environment variable for files with the name "<command>.dscresource.json".
The contents of this file would have a `dscresource` section indicating that commands participates in configuration (defined in next section).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why you'd need another way to discover DSC Resources than the existing way to discover DSC Resources...
At least with the plug-in model I don't see why you'd want to discover DSC Resources in another way from a file.
Maybe you could configure the plug-in from config to change its PSModulePath, but I don't see we need another discovery mechanism.


Example of getting the metadata for a resource:

```output
PS> Get-DscResource MyResource
PS> config list MyResource

ImplementationDetail : CommandBased
ResourceType : MyResource
Name : MyResource
FriendlyName :
Module :
ModuleName :
Version : 0.0.1
Path : /usr/bin/MyResource.command.json
ParentPath : /usr/bin
ImplementedAs : Command
CompanyName : Microsoft
Settings : # this is renamed from `Properties`, but in PS we can alias it
SHA256Hash :
SignerCertThumbprint :
exitCodes : {}
requires : {}
schema : https://schemas.microsoft.com/configuration/myResource/20220621/schema.json
```

These members are carried over form existing `Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo` type with addition of
`SHA256Hash` and `SignerCertThumprint` optional members and `exitCodes`, `requires`, and `schema` are from the manifest file.

### Command manifest for configuration

The manifest file is a JSON file following specific schema (actual JSON schema file to be defined later).
The file name must end with `.dscresource.json` while the first part
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The file name must end with `.dscresource.json` while the first part

I don't think the config tool should need to read the json from file (unless it's for caching), so the name shouldn't matter.
Also, the sentence is not complete.

An example "MyResource.dscresource.json":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An example "MyResource.dscresource.json":
An example of the Resource definition for `MyResource` returned from the _pwsh PSDSC_ plug-in":


```json
{
"manifestVersion": "1.0",
"dscresource": {
"name": "Microsoft.DSC.MyResource",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for DSC Resources, can't we default the namespace to ModuleName + DSC Resource Name?
I believe namespaces don't work for modules written in PowerShell ;)

"version": "0.0.1-preview",
"get": {
"executable": "mycommand",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get what "mycommand" is?
It sounds like xFile the DSC Resource would be the command for a DSC Resource?
I guess it's because I'm trying to infer too much from this early example of JSON?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mycommand is the name of the executable. It's just an example here. Maybe adding .exe makes it more clear even though on Linux it wouldn't have an extension?

"args": [ "getconfig" ],
"inputViaStdin": true
},
"set": {
"executable": "mycommand",
"args": [ "setconfig" ],
"inputAsArgs": true,
"returnState": true
},
"test": {
"caseSensitive": true
},
"requires": [
{
"resource": "name of dependency",
"resourceType": "executable|OS",
"version": "1.2.3"
}
],
"exitCodes": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow way to execute command to get these, probably as JSON output making it easier to keep it all in the code rather than keeping code and manifest in sync

"1": "Access Denied",
"2": "Invalid setting value",
"3": "Unknown setting"
},
"schema": "https://schemas.microsoft.com/configuration/myResource/20220621/schema.json"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enable getting schema dynamically via the executable

}
}
```

The resource `name` includes the namespace which is required to be unique.
The `version` allows for semver.

If `inputViaStdin` is `true`, then the input JSON will be sent to the command via STDIN.
This is preferred for resources that support nested JSON objects.

If `inputAsArgs` is `true`, then the input JSON will be deserialized into a list of arguments.
The top level members of the JSON become the argument names and the values become the argument values.
Any nested object values or arrays will be sent as JSON argument values.
When this is used, the `config` command will need to be careful escaping the arguments correctly
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why having a plug-in model is important IMO.
Given different runtime host, you'd escape the arguments differently (i.e. in BASH it would escape differently than other hosts). The config should not care (beyond option inputAsArgs in the schema), but the plug-in would do that process.

to not expose a code injection vulnerability.
This would make it easy to implement simple resources as BASH scripts which can parse arguments
more easily than JSON.

The `returnState` for `set` is optional but indicates that the resource will return the current state
of the settings.
Otherwise, the `config` command will execute a `get` upon successful `set` to get the current state.

Here, `mycommand` is an executable that is found within `PATH`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I was a bit confused above.
I guess if it's an executable, then its not a DSC resource, it's a config resource.

Note that the executable name does not need to match the command name as part of the manifest file name.
This means that a single executable can implement multiple resources where each resource
would have its own manifest file.
The `args` member allows passing an arbitrary number of arguments to the executable for get, set, or test operations.
This allows the executable to be "python3", for example, and the args point to a specific python script with
other arguments as needed by the script if it implements get, set, or test within the same script.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works in simple cases, but with resources sometimes you have to know that the desired value is $null (and not "not-set" which means don't touch), which might be hard to emulate if you're passing parameters to command like that.
So it's ok, but we need to bear in mind the limitation in many use cases (but maybe passing null is fine and I don't see what you have in mind or how to do it).

Alternatively, the args could point to different scripts that implement get, set, or test.

In the example above, doing:

```powershell
Invoke-DscResource -Method Get -Name MyResource
```

would execute: "mycommand getconfig"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I'm completely lost...
Were you trying to create a higher level configuration tool that could support DSC Resources, or did you want to have DSC Resources extended to support resources defined in other ways like executable...
To me, that means you need to have PowerShell & PSDSC loaded to call... an executable...?


`set` and `test` are optional.
If `test` is not implemented, PSDesiredStateConfiguration will perform it's own comparison with desired state
with current state by performing a `get` operation.
In the example above, because there is no `executable` specified, then it does not implement `test` and relies on
higher level tooling to provide that operation.
Comment on lines +142 to +145
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If `test` is not implemented, PSDesiredStateConfiguration will perform it's own comparison with desired state
with current state by performing a `get` operation.
In the example above, because there is no `executable` specified, then it does not implement `test` and relies on
higher level tooling to provide that operation.
If `test` is not implemented, PSDesiredStateConfiguration will perform its own comparison of the desired state
with the current state by performing a `get` operation, and comparing with the desired state.
In the example above, because there is no `executable` specified, then the `config` resource does not implement `test` and relies on `config` to do the validation.

The `caseSensitive` member defaults to `false` if not specified and is used for higher level tooling to determine
when comparing the input JSON with retrieved current JSON if values should be case sensitive or not.

In the case of of a resource that has properties whose status cannot be determined by simple equivalence, for example a resource that supports a version range,
then a simple comparison of desired configuration and current configuration JSON won't work.
For now, resources with properties that require any type of more complex check will need to implement `test` to perform it themselves.

> **Note**: JSON treats member names as case-sensitive. Expectation is that JSON schema is published to help
> with authoring configuration files which will take care of case-sensitive member names so only the member values
> case-sensitivity is determined by thie `caseSensitive` member.

Comment on lines +149 to +156
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the case of of a resource that has properties whose status cannot be determined by simple equivalence, for example a resource that supports a version range,
then a simple comparison of desired configuration and current configuration JSON won't work.
For now, resources with properties that require any type of more complex check will need to implement `test` to perform it themselves.
> **Note**: JSON treats member names as case-sensitive. Expectation is that JSON schema is published to help
> with authoring configuration files which will take care of case-sensitive member names so only the member values
> case-sensitivity is determined by thie `caseSensitive` member.
In the case of a resource that has properties whose status cannot be determined by simple equivalence, for example a resource that supports a version range,
then a simple comparison of desired configuration and current configuration JSON won't work.
For now, resources with properties that require any type of more complex check will need to implement `test` to perform it themselves.
> **Note**: JSON treats member names as case-sensitive. Expectation is that JSON schema is published to help
> with authoring configuration files which will take care of case-sensitive member names so only the member values
> case-sensitivity is determined by the `caseSensitive` member.

The `requires` section is optional and declares resources the configuration resource depends upon.
For example, version 1.0 of the sshdconfig resource may require a minimal version of sshd executable to work correctly.
Tooling like PSDesiredStateConfiguration may perform dependency versioning checks and fail fast,
but initially it may only serve as documentation.
Version can be a range using [nuget version range syntax](https://docs.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges).

The `exitCodes` section is optional and documents the exit codes that the resource can return.
An exit code of 0 is always success. Any non-zero exit code is considered a failure.
This section enables more detailed error reporting by tooling.

> [Note] Need to think of how localization will work for `exitCode` messages.
Comment on lines +163 to +167
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think resources need or should use exit codes.
Resources called by the plug-in will returned structured data (i.e. an error), then only the config tool should return an exit code when such data is received.


The `schema` member should be a URL to a JSON schema that describes the configuration for the resource
so it can be used for intellisense while authoring configuration.

> [Note] Should we support embedded schema in the manifest file?

In the future, if we want to support long running host processes instead of spawning a new
process each time, we can use something like:
Comment on lines +174 to +175
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The beauty of the plug-in model.
Once registered, the plug-in decides what to do, and can be ordered by the config tool.
For pwsh you might want to re-use the same host, and other spin a new instance every time.


```json
{
"manifestVersion": "1.0",
"configuration": {
"get": {
"host": "powershell-host",
"args": [ "" ],
"acceptStdin": true
}
}
}
```

Here, the "host" member points to an executable that implements some well defined GRPC endpoints to be
defined in the future.
This is currently out of scope, but allows for this type of support in the future.

## PowerShell module based resources

Existing PowerShell based resources are packaged as modules whether they are script functions or classes.
Some require Windows PowerShell while others may require PowerShell 7.
Both will be supported, but a new resource manifest must be created to work in DSC v3.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Both will be supported, but a new resource manifest must be created to work in DSC v3.

With the plug-in model, the plugin does the discovery, converts the results as resource manifests, and return them to the config utility.


### .ps1 as a resource manifest

A PowerShell based resource can be use a `.ps1` script and not part of a module.
A `.ps1` script works similar to a command based resource except that converstion to/from JSON will be handled by the `config` command
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, this is not a DSC Resource, it's a script with functions (but no contract).
This would then call for a different plug-in, the ps1_resource plug-in.
Such plug-in would do the discovery, and would be in charge of finding the *.ps1.config.json (I'm making this up).
It would also be in charge during invocation to dispatch to pwsh or WPS.

and the script will just need to work with PSObjects.
The `config` command will need to know whether to use Windows PowerShell or PowerShell 7 to execute the script and is specified explicitly
in the manifest file:

```json
{
"manifestVersion": "1.0",
"dscresource": {
"name": "Microsoft.DSC.ScriptResource",
"version": "0.0.1-preview",
"get": {
"executable": "pwsh",
"type": "powershellscript",
"file": "ScriptResource.ps1",
"args": [ "-method", "get" ],
"inputArg": "-inputObject"
}
}
}
```

In this fragment, the `executable` points to the PowerShell executable to use.
The `type` member informs the `config` command to special case the execution of the script as it would be base64 encoded.
The JSON input will be converted to a HashTable and passed to the script as a parameter.
The output object from the resource will be converted by the `config` command to JSON.
As `get`, `set`, and `test` could be different script files with different requires, each section may repeat similar property values.

### Module based resources

Existing PowerShell class and script function resources packaged as a module will need the addition of a manifest file.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may change this to NOT require a new JSON config file as most legacy modules authors will likely not make a change. Instead, we can have a meta property as part of the input JSON like _powershell that specifies powershell.exe or pwsh.exe to execute.


```json
{
"manifestVersion": "1.0",
"dscresource": {
"name": "Microsoft.DSC.MyResource",
"version": "0.0.1-preview",
"get": {
"executable": "pwsh",
"type": "powershellmodule",
"module": "Microsoft.DSC.DSCResource",
"name": "MyResource"
}
}
}
```

> [Note] Perhaps we can require this JSON be in the same folder as the module and then the module name doesn't need to be specified.

## Resource input

The JSON input to the resource will be just the `settings` for that resource.
See https://microsoft.ghe.com/AzureCore-Compute/PowerShellTeam-Docs/pull/36/files for a higher level configuration example and the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have access to that link, and I don't really understand :(

contents of the `settings` for a resource is expected to be passed as JSON.

For example, a `get` operation against a `SecretManagement` resource would be:

```json
{
"name": "MySecret",
"vault": "AzureKeyVault"
}
```

## Resource output

### Success output

The exit code must be 0 for success.
Legacy PowerShell resources don't set the exit code so will default to 0.
Optional JSON output via STDOUT provides additional information and may be used by the orchestrator.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Optional JSON output via STDOUT provides additional information and may be used by the orchestrator.
Optional JSON output via STDOUT provides additional information and may be used by the `config` utility.

Changed for consistency.
Orchestrator is another loaded term.

For example, the result of a `set` with a resource to install software may return the path to the installed software
as well as the install date and version which may not be specifically requested by the input payload.
In this case, the request may be a range of versions and the response will be the specific version installed.

### Failure output

The exit code may be non-zero to indicate failure.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exit code should be important for the config utility, not what it runs.
What it runs should return JSON.

However, some languages may not make it easy to set the exit code so it's not required.
JSON output via STDERR is interpreted as a failure only if the top level member is `error`:

```json
{
"error": {
"code": 2,
"message": "Access Denied"
}
}
```

Since STDERR is used for other types of output (verbose/debug messages), the top level member indicates failure.

> [TODO] Should define a common schema for error messages so an orchestrator can more easily parse
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have a _reason property that contains ErrorId and Message per property if there is a failure, like Access Denied, or Not Found

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the Reason is not an error?
What if there's more than 1 reason per property? (User is not in group xyz and is also disabled)?

> and report the errors to the user.
Comment on lines +296 to +297
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> [TODO] Should define a common schema for error messages so an orchestrator can more easily parse
> and report the errors to the user.
> [TODO] Should define a common schema for error messages so that the `config` utility, or higher level tools, can more easily parse
> and report the errors to the user.


### Get operation

If no input JSON is provided as a filter, then the resource will return all managed settings as a single
JSON object to STDOUT.
In cases where it does not make sense to return the entirety of the domain as JSON (for example,
registry or file system), then the resource should return an error with a non-zero exit code indicating
such an operation is not supported.

If input JSON is provided, then the resource will return all managed settings that match the filter as a single
JSON object to STDOUT.
If multiple instances need to be returned, it would be represented as an array within the single JSON response object.

> [Note] Streaming of JSON is only needed if there's a scenario that would make use of streaming, otherwise
> it's simpler to always return a single JSON object as a container for multiple objects.

### Set operation

If the resource declares `returnState` as `true` in the manifest, then the resource shall return JSON
representing the current state of the settings after successful set operation.
Otherwise, no output is expected for success and the `config` command will invoke `get` to get the current state.
Failure follows "Failure output" section above.

### Test operation

If the input JSON matches the current state, then a 0 exit code is returned and no output is sent to STDOUT.
Otherwise, a non-zero exit code is returned and JSON representing settings where the values differ is sent to STDOUT
where the values are the current values.

### Extended information output

Resources that return extended information must conform to this format sent to STDERR.

Error messages have this syntax:

```json
{
"error": {
"code": "1",
"message": "Access Denied"
}
}
```

Verbose messages have this syntax:

```json
{
"verbose": {
"message": "Setting 'MySetting' changed from 'OldValue' to 'NewValue'"
}
}
```

Warning messages have this syntax:

```json
{
"warning": {
"message": "Setting 'MySetting' is not valid"
}
}
```

Note that any JSON object to STDERR needs to be on a single line even thought these
examples show them on multiple lines.
It is up to tooling to determine how to display this information and how to handle any
JSON objects that are returned via STDERR that don't match the above syntax.
Any non-standard JSON objects (or invalid JSON) will result in the `config` command emitting a warning.

## Common configuration members

For consistency, the following members are common to all configuration resource settings:

`_ensure`: Used to specify if that setting should be `present` or `absent`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some rare occasions this does not make sense for some resources...
For instance, some resources are not installable/removable, they will always be present, but maybe with different values...

`_purge`: Indicate if unmanaged settings should be removed (`true`) or left alone (`false`). Default is `false`.
`_validation`: Indicate if the resource should fail if there are unknown settings (`strict`) or ignore them (`loose`). Default is `strict`.

Common configuration members start with a leading underscore to avoid conflicts with existing resource settings
and indicate consistent definitions for these settings.