Skip to content

Commit acb01cd

Browse files
authored
Merge pull request #441 from tgauth/add-resource-what-if
Add resource support for what if
2 parents b26a223 + 857c03d commit acb01cd

File tree

10 files changed

+140
-12
lines changed

10 files changed

+140
-12
lines changed

dsc/src/subcommand.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,11 +511,12 @@ fn list_resources(dsc: &mut DscManager, resource_name: &Option<String>, adapter_
511511
write_table = true;
512512
}
513513
for resource in dsc.list_available_resources(&resource_name.clone().unwrap_or("*".to_string()), &adapter_name.clone().unwrap_or_default()) {
514-
let mut capabilities = "-------".to_string();
514+
let mut capabilities = "--------".to_string();
515515
let capability_types = [
516516
(Capability::Get, "g"),
517517
(Capability::Set, "s"),
518518
(Capability::SetHandlesExist, "x"),
519+
(Capability::WhatIf, "w"),
519520
(Capability::Test, "t"),
520521
(Capability::Delete, "d"),
521522
(Capability::Export, "e"),

dsc/tests/dsc_whatif.tests.ps1

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,40 @@ Describe 'whatif tests' {
6767
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
6868
$LASTEXITCODE | Should -Be 2
6969
}
70+
71+
It 'actual execution of WhatIf resource' {
72+
$config_yaml = @"
73+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
74+
resources:
75+
- name: WhatIf
76+
type: Test/WhatIf
77+
properties:
78+
executionType: Actual
79+
"@
80+
$result = $config_yaml | dsc config set | ConvertFrom-Json
81+
$result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'Actual'
82+
$result.results.result.afterState.executionType | Should -BeExactly 'Actual'
83+
$result.results.result.changedProperties | Should -Be $null
84+
$result.hadErrors | Should -BeFalse
85+
$result.results.Count | Should -Be 1
86+
$LASTEXITCODE | Should -Be 0
87+
}
88+
89+
It 'what-if execution of WhatIf resource' {
90+
$config_yaml = @"
91+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
92+
resources:
93+
- name: WhatIf
94+
type: Test/WhatIf
95+
properties:
96+
executionType: Actual
97+
"@
98+
$result = $config_yaml | dsc config set -w | ConvertFrom-Json
99+
$result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'WhatIf'
100+
$result.results.result.afterState.executionType | Should -BeExactly 'WhatIf'
101+
$result.results.result.changedProperties | Should -BeExactly 'executionType'
102+
$result.hadErrors | Should -BeFalse
103+
$result.results.Count | Should -Be 1
104+
$LASTEXITCODE | Should -Be 0
105+
}
70106
}

dsc_lib/src/discovery/command_discovery.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,9 @@ fn load_manifest(path: &Path) -> Result<DscResource, DscError> {
450450
capabilities.push(Capability::SetHandlesExist);
451451
}
452452
}
453+
if manifest.what_if.is_some() {
454+
capabilities.push(Capability::WhatIf);
455+
}
453456
if manifest.test.is_some() {
454457
capabilities.push(Capability::Test);
455458
}

dsc_lib/src/dscresources/command_resource.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,24 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
9595
#[allow(clippy::too_many_lines)]
9696
pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError> {
9797
// TODO: support import resources
98-
99-
let Some(set) = &resource.set else {
98+
let operation_type: String;
99+
let mut is_synthetic_what_if = false;
100+
let set_method = match execution_type {
101+
ExecutionKind::Actual => {
102+
operation_type = "set".to_string();
103+
&resource.set
104+
},
105+
ExecutionKind::WhatIf => {
106+
operation_type = "whatif".to_string();
107+
if resource.what_if.is_none() {
108+
is_synthetic_what_if = true;
109+
&resource.set
110+
} else {
111+
&resource.what_if
112+
}
113+
}
114+
};
115+
let Some(set) = set_method else {
100116
return Err(DscError::NotImplemented("set".to_string()));
101117
};
102118
verify_json(resource, cwd, desired)?;
@@ -105,7 +121,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
105121
if !skip_test && set.pre_test != Some(true) {
106122
info!("No pretest, invoking test {}", &resource.resource_type);
107123
let test_result = invoke_test(resource, cwd, desired)?;
108-
if execution_type == &ExecutionKind::WhatIf {
124+
if is_synthetic_what_if {
109125
return Ok(test_result.into());
110126
}
111127
let (in_desired_state, actual_state) = match test_result {
@@ -121,7 +137,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
121137
}
122138
};
123139

124-
if in_desired_state {
140+
if in_desired_state && execution_type == &ExecutionKind::Actual {
125141
return Ok(SetResult::Resource(ResourceSetResponse{
126142
before_state: serde_json::from_str(desired)?,
127143
after_state: actual_state,
@@ -130,9 +146,8 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
130146
}
131147
}
132148

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

138153
let Some(get) = &resource.get else {
@@ -141,7 +156,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
141156
let args = process_args(&get.args, desired);
142157
let command_input = get_command_input(&get.input, desired)?;
143158

144-
info!("Getting current state for set by invoking get {} using {}", &resource.resource_type, &get.executable);
159+
info!("Getting current state for {} by invoking get '{}' using '{}'", operation_type, &resource.resource_type, &get.executable);
145160
let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
146161

147162
if resource.kind == Some(Kind::Resource) {
@@ -171,21 +186,21 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
171186
},
172187
}
173188

174-
info!("Invoking set '{}' using '{}'", &resource.resource_type, &set.executable);
189+
info!("Invoking {} '{}' using '{}'", operation_type, &resource.resource_type, &set.executable);
175190
let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env)?;
176191

177192
match set.returns {
178193
Some(ReturnKind::State) => {
179194

180195
if resource.kind == Some(Kind::Resource) {
181-
debug!("Verifying output of set '{}' using '{}'", &resource.resource_type, &set.executable);
196+
debug!("Verifying output of {} '{}' using '{}'", operation_type, &resource.resource_type, &set.executable);
182197
verify_json(resource, cwd, &stdout)?;
183198
}
184199

185200
let actual_value: Value = match serde_json::from_str(&stdout){
186201
Result::Ok(r) => {r},
187202
Result::Err(err) => {
188-
return Err(DscError::Operation(format!("Failed to parse json from set {}|{}|{} -> {err}", &set.executable, stdout, stderr)))
203+
return Err(DscError::Operation(format!("Failed to parse json from {} '{}'|'{}'|'{}' -> {err}", operation_type, &set.executable, stdout, stderr)))
189204
}
190205
};
191206

dsc_lib/src/dscresources/dscresource.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub enum Capability {
5050
Set,
5151
/// The resource supports the `_exist` property directly.
5252
SetHandlesExist,
53+
/// The resource supports simulating configuration directly.
54+
WhatIf,
5355
/// The resource supports validating configuration.
5456
Test,
5557
/// The resource supports deleting configuration.

dsc_lib/src/dscresources/resource_manifest.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ pub struct ResourceManifest {
4040
/// Details how to call the Set method of the resource.
4141
#[serde(skip_serializing_if = "Option::is_none")]
4242
pub set: Option<SetMethod>,
43+
/// Details how to call the `WhatIf` method of the resource.
44+
#[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")]
45+
pub what_if: Option<SetMethod>,
4346
/// Details how to call the Test method of the resource.
4447
#[serde(skip_serializing_if = "Option::is_none")]
4548
pub test: Option<TestMethod>,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
3+
"type": "Test/WhatIf",
4+
"version": "0.1.0",
5+
"get": {
6+
"executable": "dsctest",
7+
"args": [
8+
"whatif"
9+
]
10+
},
11+
"set": {
12+
"executable": "dsctest",
13+
"args": [
14+
"whatif"
15+
],
16+
"return": "state"
17+
},
18+
"whatIf": {
19+
"executable": "dsctest",
20+
"args": [
21+
"whatif",
22+
"-w"
23+
],
24+
"return": "state"
25+
},
26+
"schema": {
27+
"command": {
28+
"executable": "dsctest",
29+
"args": [
30+
"schema",
31+
"-s",
32+
"what-if"
33+
]
34+
}
35+
}
36+
}

tools/dsctest/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub enum Schemas {
1010
Exist,
1111
Sleep,
1212
Trace,
13+
WhatIf,
1314
}
1415

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

5556
#[clap(name = "trace", about = "The trace level")]
5657
Trace,
58+
59+
#[clap(name = "whatif", about = "Check if it is a whatif operation")]
60+
WhatIf {
61+
#[clap(name = "whatif", short, long, help = "Run as a whatif executionType instead of actual executionType")]
62+
what_if: bool,
63+
}
5764
}

tools/dsctest/src/main.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod echo;
77
mod exist;
88
mod sleep;
99
mod trace;
10+
mod whatif;
1011

1112
use args::{Args, Schemas, SubCommand};
1213
use clap::Parser;
@@ -16,6 +17,7 @@ use crate::echo::Echo;
1617
use crate::exist::{Exist, State};
1718
use crate::sleep::Sleep;
1819
use crate::trace::Trace;
20+
use crate::whatif::WhatIf;
1921
use std::{thread, time::Duration};
2022

2123
fn main() {
@@ -75,6 +77,9 @@ fn main() {
7577
Schemas::Trace => {
7678
schema_for!(Trace)
7779
},
80+
Schemas::WhatIf => {
81+
schema_for!(WhatIf)
82+
},
7883
};
7984
serde_json::to_string(&schema).unwrap()
8085
},
@@ -100,6 +105,14 @@ fn main() {
100105
};
101106
serde_json::to_string(&trace).unwrap()
102107
},
108+
SubCommand::WhatIf { what_if } => {
109+
let result: WhatIf = if what_if {
110+
WhatIf { execution_type: "WhatIf".to_string() }
111+
} else {
112+
WhatIf { execution_type: "Actual".to_string() }
113+
};
114+
serde_json::to_string(&result).unwrap()
115+
},
103116
};
104117

105118
println!("{json}");

tools/dsctest/src/whatif.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
8+
#[serde(deny_unknown_fields)]
9+
pub struct WhatIf {
10+
#[serde(rename = "executionType")]
11+
pub execution_type: String,
12+
}

0 commit comments

Comments
 (0)