DSC Settings and Policy should be more strongly typed
Summary of the new feature / enhancement
As a maintainer of DSC, I want to have strongly defined types for the available settings/policy, so that I can reason about the structure of DSC settings without needing to search through the code and find keys/values to explore how they're used.
As a user and integrating developer, I want to be able to reference a JSON Schema for DSC settings/policy, so that I can validate that data and safely use/integrate with it.
Proposed technical implementation details (optional)
Currently, the implementation uses the get_setting function, which returns a result object where the inner data is a struct containing two optional arbitrary JSON values - one for the definition in settings, one for the definition in policy:
https://github.com/PowerShell/DSC/blob/728b5290f25e5bb057f458e5bd6cb3709f71cf62/dsc_lib/src/util.rs#L110
https://github.com/PowerShell/DSC/blob/728b5290f25e5bb057f458e5bd6cb3709f71cf62/dsc_lib/src/util.rs#L15-L18
We use this setting in two places:
-
We retrieve
resourcePathduring command discovery:https://github.com/PowerShell/DSC/blob/728b5290f25e5bb057f458e5bd6cb3709f71cf62/dsc_lib/src/discovery/command_discovery.rs#L88-L110
Which we then deserialize as a JSON value into the
ResourcePathSettingstruct defined in the same file:https://github.com/PowerShell/DSC/blob/728b5290f25e5bb057f458e5bd6cb3709f71cf62/dsc_lib/src/discovery/command_discovery.rs#L49-L59
-
We retrieve the
tracingsetting in theenable_tracing()function in the DSC CLI itself:https://github.com/PowerShell/DSC/blob/728b5290f25e5bb057f458e5bd6cb3709f71cf62/dsc/src/util.rs#L321-L340
Where we again deserialize the JSON value into a struct defined in the same file, in this case
TracingSetting:https://github.com/PowerShell/DSC/blob/728b5290f25e5bb057f458e5bd6cb3709f71cf62/dsc/src/util.rs#L79-L87
We don't perform any caching and we don't have a way to retrieve every setting once. We don't derive serialization or JSON Schema for these settings and they're not easily discoverable without following the flow of code that uses the get_setting() function.
Instead, we should define a new module for dsc_lib where we centrally define settings/policy. Ideally, we should be able to parse the policy and settings once during an invocation and quickly be able to retrieve the relevant settings/policy from the resolved struct (which is separate from the data a user specifies, like how we separate the manifests from resource/extension info).
The current representation of available settings/policy would look something like:
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DefinedMetaConfiguration {
pub tracing: Option<TracingSetting>,
pub resource_path: Option<ResourcePathSetting>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TracingSetting {
/// Trace level to use - see pub enum `TraceLevel` in `dsc_lib\src\dscresources\command_resource.rs`
pub level: TraceLevel,
/// Trace format to use - see pub enum `TraceFormat` in `dsc\src\args.rs`
pub format: TraceFormat,
/// Whether the 'level' can be overrridden by `DSC_TRACE_LEVEL` environment variable
pub allow_env_override: bool
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ResourcePathSetting {
/// whether to allow overriding with the `DSC_RESOURCE_PATH` environment variable
pub allow_env_override: bool,
/// whether to append the PATH environment variable to the list of resource directories
pub append_env_path: bool,
/// array of directories that DSC should search for non-built-in resources
pub directories: Vec<String>
}
While we could probably just cache two instances of DefinedMetaConfiguration in the context - one for policy, one for settings - and resolve things that way, it would probably better to have a longer discussion about designing the meta configuration for maintainability. Currently we only consider meta configuration from policy, settings, and environment variables. We probably also want to consider things like:
- Cases where definable fields in policy and settings are different (does it make sense to have some meta configuration field only at the policy level?)
- Cases where we want to merge meta configuration as opposed to replace it (e.g. if I define a block list at the policy level, I should be able to add to it in settings but not allow an item blocked at the policy level).
- Being able to define meta configuration in a document (override anything but policy).
- Strongly associating environment variables with meta configuration
- Showing the resolved meta configuration from policy/settings/environment variables.
- Performance and caching - can we cache the resolved settings to disk and see whether we actually need to re-process policy or settings during an invocation? Can we pass resolved settings to a nested invocation?
- Should we ever enable some subset of settings to be modified on the per-resource level (e.g. I want to get tracing for this resource, not the whole run)?
- If we have schemas for various settings, we can probably define resources to set the policy/settings for a machine - but should we have any special handling for updating the originally-resolved context afterward, or clearly indicate they only apply on subsequent executions?
- Which settings, if any, should be surfaced in results/diagnostics? Can we usefully indicate meta configuration to callers for debugging/investigation purposes without requiring the use of a second command?
- Should we have a command that emits the resolved meta configuration at the terminal?
Before I implement this, I want to be sure we agree on the format and how policy vs settings is exposed. My initial thinking is:
- policy replicates most settings (some may not be appropriate for policy) where if a setting has policy applied, then it cannot be overridden
- setting defines the default value and can be overridden by user or at runtime
The settings file would exist in the home folder of DSC and presumed to have appropriate file ACLs to protect it system wide. Users can have their own copy of settings in their home under .dsc that can override settings, but not policy. However, any policy not defined at the home of DSC can be defined within the user policy (hopefully this makes sense).
Unfortunately, JSON doesn't have a concept of attributes, so we either turn every setting into an object to identify if it's policy or not, or simply have a policy section in the JSON (and thus the schema) which is probably simpler but means we need to make sure the policy and settings sections are in sync whenever new things get added.
After discussion in the working group, I'm providing this information to help us continue the design discussion with more clarity.
Terminology
It would be helpful to use consistent terms when discussing this feature:
- Field
- Defines an optional configuration for DSC. The field can be any valid JSON type.
- Scope
- Indicates at what level the field is defined. Available scopes are:
- Machine scope
- Defined for every user on the system in
%PROGRAMDATA%/dsc(Windows),/etc/dsc(Linux), or/Library/dsc(macOS) - User scope
- Defined for a specific user on the system in
%APPDATA%/dsc(Windows),$HOME/.config/dsc(Linux, any whenXDG_CONFIG_HOMEis set), or$HOME/Library/Application Support/dsc(macOS). - Workspace scope
- Defined for a specific workspace at the folder root.
- Environment scope
- Defines individual fields as environmental variables, like
DSC_RESOURCE_PATH. Fields set in this scope are always setting fields, not policy fields - CLI scope
- Defines individual fields as CLI options, like
--trace-level. Fields set in this scope are always setting fields, not policy fields
- Policy field
- Defines an optional configuration for DSC. When set as a policy field, the field value takes precedence, ignoring lower-scope definitions for that field
- Setting field
- Defines an optional configuration for DSC. When set as a setting field, the field value takes precedence over higher-scope settings, but not policies.
Field resolution process
Field resolution follows these steps, stopping when the value is resolved:
- The current value for the field is retrieved from the code default value.
- If the field is defined in the machine scope as a policy, the machine policy value is merged over the current value. The resulting value is final, ending resolution for this field.
- If the field is defined in the user scope as a policy, the user policy value is merged over the current value. The resulting value is final, ending resolution for this field.
- If the field is defined in the workspace scope as a policy, the workspace policy value is merged over the current value. The resulting value is final, ending resolution for this field.
- If the field is defined in the machine scope as a setting, the machine setting value is merged over the curent value.
- If the field is defined in the user scope as a setting, the user scope setting value is merged over the current value.
- If the field is defined in the workspace scope as a setting, the workspace scope setting value is merged over the current value.
- If the field is defined as an environment variable, the environment variable value is merged over the current value.
- If the field is defined as a CLI option, the CLI option value is merged over the current value.
Field resolution table
The following table shows how fields are resolved by the preceding process. The highest-scope policy file is authoratative. If the field isn't defined as a policy field, the lowest-scope setting field is authoratative. If the field isn't defined as a policy or setting, the default value from the code is authoratative.
| Final value | Machine policy | User policy | Workspace policy | Machine setting | User setting | Workspace setting | Environment setting | CLI setting |
|---|---|---|---|---|---|---|---|---|
| Machine policy | Defined | Defined | Defined | Defined | Defined | Defined | Defined | Defined |
| User policy | Undefined | Defined | Defined | Defined | Defined | Defined | Defined | Defined |
| Workspace policy | Undefined | Undefined | Defined | Defined | Defined | Defined | Defined | Defined |
| CLI setting | Undefined | Undefined | Undefined | Defined | Defined | Defined | Defined | Defined |
| Environment setting | Undefined | Undefined | Undefined | Defined | Defined | Defined | Defined | Undefined |
| Workspace setting | Undefined | Undefined | Undefined | Defined | Defined | Defined | Undefined | Undefined |
| User setting | Undefined | Undefined | Undefined | Defined | Defined | Undefined | Undefined | Undefined |
| Machine setting | Undefined | Undefined | Undefined | Defined | Undefined | Undefined | Undefined | Undefined |
| Default (code) | Undefined | Undefined | Undefined | Undefined | Undefined | Undefined | Undefined | Undefined |
Examples
-
Field defined in multiple policy files
If a field is defined in multiple policy files, only the highest-scope policy definition is used:
- If the field is defined for the machine policy, the field is ignored in all other sources.
- If the field is defined for the user policy, the field is ignored for workspace policy and all settings sources.
- If the field is defined for the workspace policy, the field is ignored for all settings sources.
-
Field is defined in multiple setting files
If a field is defined in multiple settings files, the lower-scope values merge over the higher-scope values:
- If the field is defined for the machine scope setting, the machine setting value is merged over the current value.
- If the field is defined for the user scope setting, the user setting value is merged over the current value.
- If the field is defined for the workspace scope setting, the workspace setting value is merged over the current value.
-
Field is defined in settings file and environment variable
WHen a field is defined as an environment variable, it always merges over the current value except when the field is defined as a policy.
-
Field is defined in settings file, environment variable, and CLI option
WHen a field is defined as a CLI option, it always merges over the current value except when the field is defined as a policy.
Considerations
- We need to strongly define where we store files for lookup. We can support per-OS locations. We should consider always supporting
XDG_CONFIG_HOMEif it's defined. - We need to consider the behavior for nested fields. If a top-level field is a map, do we deep-merge or shallow-replace? What do we do for undefined subfields in policy when they are defined in settings?
- We need to decide on criteria for whether a field is surfaced as an environment variable and/or CLI option.
- We need to decide whether we will support reading policy and setting files separately or as top-level keys in the same document.
- We should consider distinguishing between settings as policy and settings as preference instead of policy and settings. This would make the semantics clearer to users and easier to describe.
- We need to decide whether the available fields for policy are the same as preference/settings, or whether policy is a superset or subset. I can think of good reasons to only expose some options as policy, but I'm not sure that outweighs the complexity problem.
- We probably need a command to show the resolved fields (and possibly their sources). Something like
dsc settings showmaybe? This is useful both for local investigation/review and for surfacing to higher ordered tools. - We will eventually need to surface defining the files with DSC resources. We should defer that work until we have completed the redesign.
Summary from WG discussion:
- We define policy settings at the machine level only. If a setting is defined at the machine level, it cannot be overridden.
- All other levels defines preferences - user preference file, workspace preference file, environment variable as preference, and CLI option as preference. Higher-level preferences are overridden by lower-level preferences.
- We are deferring defining the merge-strategy options. The existing settings are only defined as scalar values so would be replaced anyway.
- We are deferring defining guiding principles for whether to expose a setting as ENV/CLI or only in files. We don't have enough data to build such guidance.
- We will respect
XDG_CONFIG_HOMEfor the user-level preferences if the environment variable is defined. Otherwise we will default to common conventions by platform. - We agree that we should eventually implement a subcommand for showing the resolved settings and source.
- We agree that the canonical settings for a specific run should be in the metadata for the output of that run.
Policy settings file locations:
- Linux:
/etc/dsc/dsc.settings.json - macOS:
/Library/dsc/dsc.settings.json - Windows:
%PROGRAMDATA%\dsc\dsc.settings.json
User settings file locations:
- XDG:
XDG_CONFIG_HOME/dsc/dsc.settings.jsonif and only ifXDG_CONFIG_HOMEis defined. - Linux:
$HOME/.config/dsc/dsc.settings.jsonifXDG_CONFIG_HOMEisn't defined. - macOS:
$HOME/Library/Application Support/dsc/dsc.settings.jsonifXDG_CONFIG_HOMEisn't defined. - Windows:
%APPDATA%\dsc\dsc.settings.jsonifXDG_CONFIG_HOMEisn't defined.
Workspace settings file location:
$PWD/dsc.settings.json
Environment variable prefix:
DSC_