Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 3 additions & 2 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:

if let ConfigSubCommand::Set { what_if , .. } = subcommand {
if *what_if {
configurator.context.execution_type = ExecutionKind::WhatIf;
configurator.context.execution_type = ExecutionKind::WhatIfDSC;
}
};

Expand Down Expand Up @@ -511,11 +511,12 @@ fn list_resources(dsc: &mut DscManager, resource_name: &Option<String>, adapter_
write_table = true;
}
for resource in dsc.list_available_resources(&resource_name.clone().unwrap_or("*".to_string()), &adapter_name.clone().unwrap_or_default()) {
let mut capabilities = "-------".to_string();
let mut capabilities = "--------".to_string();
let capability_types = [
(Capability::Get, "g"),
(Capability::Set, "s"),
(Capability::SetHandlesExist, "x"),
(Capability::SetHandlesWhatIf, "w"),
(Capability::Test, "t"),
(Capability::Delete, "d"),
(Capability::Export, "e"),
Expand Down
36 changes: 36 additions & 0 deletions dsc/tests/dsc_whatif.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,40 @@ Describe 'whatif tests' {
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
$LASTEXITCODE | Should -Be 2
}

It 'actual execution of WhatIf resource' {
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: WhatIf
type: Test/WhatIf
properties:
executionType: Actual
"@
$result = $config_yaml | dsc config set | ConvertFrom-Json
$result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'Actual'
$result.results.result.afterState.executionType | Should -BeExactly 'Actual'
$result.results.result.changedProperties | Should -Be $null
$result.hadErrors | Should -BeFalse
$result.results.Count | Should -Be 1
$LASTEXITCODE | Should -Be 0
}

It 'what-if execution of WhatIf resource' {
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: WhatIf
type: Test/WhatIf
properties:
executionType: Actual
"@
$result = $config_yaml | dsc config set -w | ConvertFrom-Json
$result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'WhatIf'
$result.results.result.afterState.executionType | Should -BeExactly 'WhatIf'
$result.results.result.changedProperties | Should -BeExactly 'executionType'
$result.hadErrors | Should -BeFalse
$result.results.Count | Should -Be 1
$LASTEXITCODE | Should -Be 0
}
}
6 changes: 5 additions & 1 deletion dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ pub enum Operation {
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum ExecutionKind {
Actual,
WhatIf,
// differentiate internally whether what-if should be processed by the resource or dsc engine
#[serde(rename = "WhatIf")]
WhatIfDSC,
#[serde(rename = "WhatIf")]
WhatIfResource,
Comment thread
tgauth marked this conversation as resolved.
Outdated
}

#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
2 changes: 1 addition & 1 deletion dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ impl Configurator {
set_result = dsc_resource.set(&desired, skip_test, &self.context.execution_type)?;
end_datetime = chrono::Local::now();
} else if dsc_resource.capabilities.contains(&Capability::Delete) {
if self.context.execution_type == ExecutionKind::WhatIf {
if self.context.execution_type == ExecutionKind::WhatIfDSC {
// TODO: add delete what-if support
return Err(DscError::NotSupported("What-if execution not supported for delete".to_string()));
}
Expand Down
10 changes: 9 additions & 1 deletion dsc_lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use crate::discovery::discovery_trait::ResourceDiscovery;
use crate::discovery::convert_wildcard_to_regex;
use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs};
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest};
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, ArgKind, Kind, ResourceManifest};
use crate::dscresources::command_resource::invoke_command;
use crate::dscerror::DscError;
use indicatif::ProgressStyle;
Expand Down Expand Up @@ -449,6 +449,14 @@ fn load_manifest(path: &Path) -> Result<DscResource, DscError> {
if set.handles_exist == Some(true) {
capabilities.push(Capability::SetHandlesExist);
}
if let Some(arg_values) = &set.args {
for arg in arg_values {
if let &ArgKind::WhatIf { .. } = arg {
capabilities.push(Capability::SetHandlesWhatIf);
break;
}
}
}
}
if manifest.test.is_some() {
capabilities.push(Capability::Test);
Expand Down
34 changes: 19 additions & 15 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
let Some(get) = &resource.get else {
return Err(DscError::NotImplemented("get".to_string()));
};
let args = process_args(&get.args, filter);
let args = process_args(&get.args, filter, &ExecutionKind::Actual);
if !filter.is_empty() {
verify_json(resource, cwd, filter)?;
command_input = get_command_input(&get.input, filter)?;
Expand Down Expand Up @@ -105,7 +105,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
if !skip_test && set.pre_test != Some(true) {
info!("No pretest, invoking test {}", &resource.resource_type);
let test_result = invoke_test(resource, cwd, desired)?;
if execution_type == &ExecutionKind::WhatIf {
if execution_type == &ExecutionKind::WhatIfDSC {
return Ok(test_result.into());
}
let (in_desired_state, actual_state) = match test_result {
Expand All @@ -121,7 +121,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
}
};

if in_desired_state {
if in_desired_state && execution_type == &ExecutionKind::Actual {
return Ok(SetResult::Resource(ResourceSetResponse{
before_state: serde_json::from_str(desired)?,
after_state: actual_state,
Expand All @@ -130,15 +130,14 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
}
}

if ExecutionKind::WhatIf == *execution_type {
// TODO: continue execution when resources can implement what-if; only return an error here temporarily
return Err(DscError::NotImplemented("what-if not yet supported for resources that implement pre-test".to_string()));
if ExecutionKind::WhatIfDSC == *execution_type {
return Err(DscError::NotImplemented("cannot process what-if execution type, as resource implements pre-test and does not support what-if".to_string()));
}

let Some(get) = &resource.get else {
return Err(DscError::NotImplemented("get".to_string()));
};
let args = process_args(&get.args, desired);
let args = process_args(&get.args, desired, &ExecutionKind::Actual);
let command_input = get_command_input(&get.input, desired)?;

info!("Getting current state for set by invoking get {} using {}", &resource.resource_type, &get.executable);
Expand All @@ -158,7 +157,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te

let mut env: Option<HashMap<String, String>> = None;
let mut input_desired: Option<&str> = None;
let args = process_args(&set.args, desired);
let args = process_args(&set.args, desired, execution_type);
match &set.input {
Some(InputKind::Env) => {
env = Some(json_to_hashmap(desired)?);
Expand Down Expand Up @@ -261,7 +260,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re

verify_json(resource, cwd, expected)?;

let args = process_args(&test.args, expected);
let args = process_args(&test.args, expected, &ExecutionKind::Actual);
let command_input = get_command_input(&test.input, expected)?;

info!("Invoking test '{}' using '{}'", &resource.resource_type, &test.executable);
Expand Down Expand Up @@ -375,7 +374,7 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re

verify_json(resource, cwd, filter)?;

let args = process_args(&delete.args, filter);
let args = process_args(&delete.args, filter, &ExecutionKind::Actual);
let command_input = get_command_input(&delete.input, filter)?;

info!("Invoking delete '{}' using '{}'", &resource.resource_type, &delete.executable);
Expand Down Expand Up @@ -406,7 +405,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) ->
return Err(DscError::NotImplemented("validate".to_string()));
};

let args = process_args(&validate.args, config);
let args = process_args(&validate.args, config, &ExecutionKind::Actual);
let command_input = get_command_input(&validate.input, config)?;

info!("Invoking validate '{}' using '{}'", &resource.resource_type, &validate.executable);
Expand Down Expand Up @@ -481,9 +480,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>
command_input = get_command_input(&export.input, input)?;
}

args = process_args(&export.args, input);
args = process_args(&export.args, input, &ExecutionKind::Actual);
} else {
args = process_args(&export.args, "");
args = process_args(&export.args, "", &ExecutionKind::Actual);
}

let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
Expand Down Expand Up @@ -528,7 +527,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Re
return Err(DscError::Operation(format!("Resolve is not supported by resource {}", &resource.resource_type)));
};

let args = process_args(&resolve.args, input);
let args = process_args(&resolve.args, input, &ExecutionKind::Actual);
let command_input = get_command_input(&resolve.input, input)?;

info!("Invoking resolve '{}' using '{}'", &resource.resource_type, &resolve.executable);
Expand Down Expand Up @@ -620,7 +619,7 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
Ok((exit_code, stdout, cleaned_stderr))
}

fn process_args(args: &Option<Vec<ArgKind>>, value: &str) -> Option<Vec<String>> {
fn process_args(args: &Option<Vec<ArgKind>>, value: &str, execution_type: &ExecutionKind) -> Option<Vec<String>> {
let Some(arg_values) = args else {
debug!("No args to process");
return None;
Expand All @@ -640,6 +639,11 @@ fn process_args(args: &Option<Vec<ArgKind>>, value: &str) -> Option<Vec<String>>
processed_args.push(json_input_arg.clone());
processed_args.push(value.to_string());
},
ArgKind::WhatIf { what_if_input_arg } => {
if execution_type == &ExecutionKind::WhatIfResource {
processed_args.push(what_if_input_arg.clone());
}
}
}
}

Expand Down
10 changes: 9 additions & 1 deletion dsc_lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub enum Capability {
Set,
/// The resource supports the `_exist` property directly.
SetHandlesExist,
/// The resource supports the `what-if` execution type directly.
SetHandlesWhatIf,
Comment thread
tgauth marked this conversation as resolved.
Outdated
/// The resource supports validating configuration.
Test,
/// The resource supports deleting configuration.
Expand Down Expand Up @@ -209,7 +211,13 @@ impl Invoke for DscResource {
return Err(DscError::MissingManifest(self.type_name.clone()));
};
let resource_manifest = import_manifest(manifest.clone())?;
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type)
let execution = if self.capabilities.contains(&Capability::SetHandlesWhatIf) && execution_type == &ExecutionKind::WhatIfDSC {
ExecutionKind::WhatIfResource
}
else {
execution_type.clone()
};
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, &execution)
},
}
}
Expand Down
5 changes: 5 additions & 0 deletions dsc_lib/src/dscresources/resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ pub enum ArgKind {
/// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false.
mandatory: Option<bool>,
},
WhatIf {
/// The argument that serves as the what-if switch.
#[serde(rename = "whatIfSwitchArg")]
what_if_input_arg: String,
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
31 changes: 31 additions & 0 deletions tools/dsctest/dscwhatif.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
"type": "Test/WhatIf",
"version": "0.1.0",
"get": {
"executable": "dsctest",
"args": [
"what-if"
]
},
"set": {
"executable": "dsctest",
"args": [
"what-if",
{
"whatIfSwitchArg": "--what-if"
}
],
"return": "state"
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"schema",
"-s",
"what-if"
]
}
}
}
7 changes: 7 additions & 0 deletions tools/dsctest/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub enum Schemas {
Exist,
Sleep,
Trace,
WhatIf,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -54,4 +55,10 @@ pub enum SubCommand {

#[clap(name = "trace", about = "The trace level")]
Trace,

#[clap(name = "what-if", about = "Check if it is a what-if operation")]
WhatIf {
#[clap(name = "what-if", short, long, help = "Run as a what-if executionType instead of actual executionType")]
what_if: bool,
}
}
13 changes: 13 additions & 0 deletions tools/dsctest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod echo;
mod exist;
mod sleep;
mod trace;
mod whatif;

use args::{Args, Schemas, SubCommand};
use clap::Parser;
Expand All @@ -16,6 +17,7 @@ use crate::echo::Echo;
use crate::exist::{Exist, State};
use crate::sleep::Sleep;
use crate::trace::Trace;
use crate::whatif::WhatIf;
use std::{thread, time::Duration};

fn main() {
Expand Down Expand Up @@ -75,6 +77,9 @@ fn main() {
Schemas::Trace => {
schema_for!(Trace)
},
Schemas::WhatIf => {
schema_for!(WhatIf)
},
};
serde_json::to_string(&schema).unwrap()
},
Expand All @@ -100,6 +105,14 @@ fn main() {
};
serde_json::to_string(&trace).unwrap()
},
SubCommand::WhatIf { what_if } => {
let result: WhatIf = if what_if {
WhatIf { execution_type: "WhatIf".to_string() }
} else {
WhatIf { execution_type: "Actual".to_string() }
};
serde_json::to_string(&result).unwrap()
},
};

println!("{json}");
Expand Down
12 changes: 12 additions & 0 deletions tools/dsctest/src/whatif.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WhatIf {
#[serde(rename = "executionType")]
pub execution_type: String,
}