Skip to content

Commit ab558d4

Browse files
authored
Merge pull request #400 from tgauth/add-whatif
Add whatif
2 parents 2fc0525 + 144ac00 commit ab558d4

File tree

12 files changed

+169
-59
lines changed

12 files changed

+169
-59
lines changed

dsc/src/args.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ pub enum ConfigSubCommand {
9191
path: Option<String>,
9292
#[clap(short = 'f', long, help = "The output format to use")]
9393
format: Option<OutputFormat>,
94+
#[clap(short = 'w', long, help = "Run as a what-if operation instead of executing the configuration or resource")]
95+
what_if: bool,
9496
},
9597
#[clap(name = "test", about = "Test the current configuration")]
9698
Test {

dsc/src/resource_command.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use crate::args::OutputFormat;
55
use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, add_type_name_to_json, write_output};
6-
use dsc_lib::configure::config_doc::Configuration;
6+
use dsc_lib::configure::config_doc::{Configuration, ExecutionKind};
77
use dsc_lib::configure::add_resource_export_results_to_configuration;
88
use dsc_lib::dscresources::invoke_result::{GetResult, ResourceGetResponse};
99
use dsc_lib::dscerror::DscError;
@@ -117,7 +117,7 @@ pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: &Op
117117
};
118118
}
119119

120-
match resource.set(input.as_str(), true) {
120+
match resource.set(input.as_str(), true, &ExecutionKind::Actual) {
121121
Ok(result) => {
122122
// convert to json
123123
let json = match serde_json::to_string(&result) {

dsc/src/subcommand.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::resource_command::{get_resource, self};
77
use crate::Stream;
88
use crate::tablewriter::Table;
99
use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json};
10-
use dsc_lib::configure::{Configurator, config_result::ResourceGetResult};
10+
use dsc_lib::configure::{Configurator, config_doc::ExecutionKind, config_result::ResourceGetResult};
1111
use dsc_lib::dscerror::DscError;
1212
use dsc_lib::dscresources::invoke_result::{
1313
GroupResourceSetResponse, GroupResourceTestResponse, ResolveResult, TestResult
@@ -248,6 +248,12 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
248248
}
249249
};
250250

251+
if let ConfigSubCommand::Set { what_if , .. } = subcommand {
252+
if *what_if {
253+
configurator.context.execution_type = ExecutionKind::WhatIf;
254+
}
255+
};
256+
251257
let parameters: Option<serde_json::Value> = match if new_parameters.is_some() {
252258
&new_parameters
253259
} else {

dsc/tests/dsc_whatif.tests.ps1

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Describe 'whatif tests' {
2+
AfterEach {
3+
if ($IsWindows) {
4+
Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore
5+
}
6+
}
7+
8+
It 'config set whatif when actual state matches desired state' {
9+
$config_yaml = @"
10+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
11+
resources:
12+
- name: Hello
13+
type: Test/Echo
14+
properties:
15+
output: hello
16+
"@
17+
$what_if_result = $config_yaml | dsc config set -w | ConvertFrom-Json
18+
$set_result = $config_yaml | dsc config set | ConvertFrom-Json
19+
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'WhatIf'
20+
$what_if_result.results.result.beforeState.output | Should -Be $set_result.results.result.beforeState.output
21+
$what_if_result.results.result.afterState.output | Should -Be $set_result.results.result.afterState.output
22+
$what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties
23+
$what_if_result.hadErrors | Should -BeFalse
24+
$what_if_result.results.Count | Should -Be 1
25+
$LASTEXITCODE | Should -Be 0
26+
}
27+
28+
It 'config set whatif when actual state does not match desired state' -Skip:(!$IsWindows) {
29+
# TODO: change/create cross-plat resource that implements set without just matching desired state
30+
$config_yaml = @"
31+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
32+
resources:
33+
- name: Registry
34+
type: Microsoft.Windows/Registry
35+
properties:
36+
keyPath: 'HKCU\1\2'
37+
"@
38+
$what_if_result = $config_yaml | dsc config set -w | ConvertFrom-Json
39+
$set_result = $config_yaml | dsc config set | ConvertFrom-Json
40+
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'WhatIf'
41+
$what_if_result.results.result.beforeState._exist | Should -Be $set_result.results.result.beforeState._exist
42+
$what_if_result.results.result.beforeState.keyPath | Should -Be $set_result.results.result.beforeState.keyPath
43+
$what_if_result.results.result.afterState.KeyPath | Should -Be $set_result.results.result.afterState.keyPath
44+
$what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties
45+
$what_if_result.hadErrors | Should -BeFalse
46+
$what_if_result.results.Count | Should -Be 1
47+
$LASTEXITCODE | Should -Be 0
48+
49+
}
50+
51+
It 'config set whatif for delete is not supported' {
52+
$config_yaml = @"
53+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
54+
resources:
55+
- name: Delete
56+
type: Test/Delete
57+
properties:
58+
_exist: false
59+
"@
60+
$result = $config_yaml | dsc config set -w 2>&1
61+
$result | Should -Match 'ERROR.*?Not supported.*?what-if'
62+
$LASTEXITCODE | Should -Be 2
63+
}
64+
65+
It 'config set whatif for group resource' {
66+
$result = dsc config set -p $PSScriptRoot/../examples/groups.dsc.yaml -w 2>&1
67+
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
68+
$LASTEXITCODE | Should -Be 2
69+
}
70+
}

dsc_lib/src/configure/config_result.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ pub struct ResourceSetResult {
9999
pub result: SetResult,
100100
}
101101

102+
impl From<ResourceTestResult> for ResourceSetResult {
103+
fn from(test_result: ResourceTestResult) -> Self {
104+
Self {
105+
metadata: None,
106+
name: test_result.name,
107+
resource_type: test_result.resource_type,
108+
result: test_result.result.into(),
109+
}
110+
}
111+
}
112+
102113
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
103114
#[serde(deny_unknown_fields)]
104115
pub struct GroupResourceSetResult {

dsc_lib/src/configure/context.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ pub struct Context {
1515
pub parameters: HashMap<String, (Value, DataType)>,
1616
pub security_context: SecurityContextKind,
1717
_variables: HashMap<String, Value>,
18-
1918
pub start_datetime: DateTime<Local>,
2019
}
2120

dsc_lib/src/configure/mod.rs

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::configure::config_doc::Metadata;
4+
use crate::configure::config_doc::{ExecutionKind, Metadata};
55
use crate::configure::parameters::Input;
66
use crate::dscerror::DscError;
77
use crate::dscresources::dscresource::get_diff;
@@ -59,7 +59,7 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, adap
5959

6060
for (i, instance) in export_result.actual_state.iter().enumerate() {
6161
let mut r = config_doc::Resource::new();
62-
r.resource_type = resource.type_name.clone();
62+
r.resource_type.clone_from(&resource.type_name);
6363
r.name = format!("{}-{i}", r.resource_type);
6464
let props: Map<String, Value> = serde_json::from_value(instance.clone())?;
6565
r.properties = escape_property_values(&props)?;
@@ -309,73 +309,64 @@ impl Configurator {
309309
let desired = add_metadata(&dsc_resource.kind, properties)?;
310310
trace!("desired: {desired}");
311311

312+
let start_datetime;
313+
let end_datetime;
314+
let set_result;
312315
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
313316
debug!("Resource handles _exist or _exist is true");
314-
let start_datetime = chrono::Local::now();
315-
let set_result = dsc_resource.set(&desired, skip_test)?;
316-
let end_datetime = chrono::Local::now();
317-
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
318-
let resource_result = config_result::ResourceSetResult {
319-
metadata: Some(
320-
Metadata {
321-
microsoft: Some(
322-
MicrosoftDscMetadata {
323-
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
324-
..Default::default()
325-
}
326-
)
327-
}
328-
),
329-
name: resource.name.clone(),
330-
resource_type: resource.resource_type.clone(),
331-
result: set_result,
332-
};
333-
result.results.push(resource_result);
317+
start_datetime = chrono::Local::now();
318+
set_result = dsc_resource.set(&desired, skip_test, &self.context.execution_type)?;
319+
end_datetime = chrono::Local::now();
334320
} else if dsc_resource.capabilities.contains(&Capability::Delete) {
321+
if self.context.execution_type == ExecutionKind::WhatIf {
322+
// TODO: add delete what-if support
323+
return Err(DscError::NotSupported("What-if execution not supported for delete".to_string()));
324+
}
335325
debug!("Resource implements delete and _exist is false");
336326
let before_result = dsc_resource.get(&desired)?;
337-
let start_datetime = chrono::Local::now();
327+
start_datetime = chrono::Local::now();
338328
dsc_resource.delete(&desired)?;
339-
let end_datetime = chrono::Local::now();
340329
let after_result = dsc_resource.get(&desired)?;
341330
// convert get result to set result
342-
let set_result = match before_result {
331+
set_result = match before_result {
343332
GetResult::Resource(before_response) => {
344333
let GetResult::Resource(after_result) = after_result else {
345334
return Err(DscError::NotSupported("Group resources not supported for delete".to_string()))
346335
};
347336
let before_value = serde_json::to_value(&before_response.actual_state)?;
348337
let after_value = serde_json::to_value(&after_result.actual_state)?;
349-
ResourceSetResponse {
338+
SetResult::Resource(ResourceSetResponse {
350339
before_state: before_response.actual_state,
351340
after_state: after_result.actual_state,
352341
changed_properties: Some(get_diff(&before_value, &after_value)),
353-
}
342+
})
354343
},
355344
GetResult::Group(_) => {
356345
return Err(DscError::NotSupported("Group resources not supported for delete".to_string()));
357346
},
358347
};
359-
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
360-
let resource_result = config_result::ResourceSetResult {
361-
metadata: Some(
362-
Metadata {
363-
microsoft: Some(
364-
MicrosoftDscMetadata {
365-
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
366-
..Default::default()
367-
}
368-
)
369-
}
370-
),
371-
name: resource.name.clone(),
372-
resource_type: resource.resource_type.clone(),
373-
result: SetResult::Resource(set_result),
374-
};
375-
result.results.push(resource_result);
348+
end_datetime = chrono::Local::now();
376349
} else {
377350
return Err(DscError::NotImplemented(format!("Resource '{}' does not support `delete` and does not handle `_exist` as false", resource.resource_type)));
378351
}
352+
353+
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
354+
let resource_result = config_result::ResourceSetResult {
355+
metadata: Some(
356+
Metadata {
357+
microsoft: Some(
358+
MicrosoftDscMetadata {
359+
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
360+
..Default::default()
361+
}
362+
)
363+
}
364+
),
365+
name: resource.name.clone(),
366+
resource_type: resource.resource_type.clone(),
367+
result: set_result,
368+
};
369+
result.results.push(resource_result);
379370
}
380371

381372
result.metadata = Some(

dsc_lib/src/dscresources/command_resource.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
use jsonschema::JSONSchema;
55
use serde_json::Value;
66
use std::{collections::HashMap, env, io::{Read, Write}, process::{Command, Stdio}};
7-
use crate::{configure::{config_result::ResourceGetResult, parameters, Configurator}, util::parse_input_to_json};
8-
use crate::{dscerror::DscError, dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}};
9-
use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
7+
use crate::{configure::{config_doc::ExecutionKind, {config_result::ResourceGetResult, parameters, Configurator}}, util::parse_input_to_json};
8+
use crate::dscerror::DscError;
9+
use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
1010
use tracing::{error, warn, info, debug, trace};
1111

1212
pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;
@@ -93,7 +93,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
9393
///
9494
/// Error returned if the resource does not successfully set the desired state
9595
#[allow(clippy::too_many_lines)]
96-
pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool) -> Result<SetResult, DscError> {
96+
pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError> {
9797
// TODO: support import resources
9898

9999
let Some(set) = &resource.set else {
@@ -104,7 +104,11 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
104104
// if resource doesn't implement a pre-test, we execute test first to see if a set is needed
105105
if !skip_test && set.pre_test != Some(true) {
106106
info!("No pretest, invoking test {}", &resource.resource_type);
107-
let (in_desired_state, actual_state) = match invoke_test(resource, cwd, desired)? {
107+
let test_result = invoke_test(resource, cwd, desired)?;
108+
if execution_type == &ExecutionKind::WhatIf {
109+
return Ok(test_result.into());
110+
}
111+
let (in_desired_state, actual_state) = match test_result {
108112
TestResult::Group(group_response) => {
109113
let mut result_array: Vec<Value> = Vec::new();
110114
for result in group_response.results {
@@ -126,6 +130,11 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
126130
}
127131
}
128132

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()));
136+
}
137+
129138
let Some(get) = &resource.get else {
130139
return Err(DscError::NotImplemented("get".to_string()));
131140
};

dsc_lib/src/dscresources/dscresource.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::dscresources::resource_manifest::Kind;
4+
use crate::{configure::config_doc::ExecutionKind, dscresources::resource_manifest::Kind};
55
use dscerror::DscError;
66
use schemars::JsonSchema;
77
use serde::{Deserialize, Serialize};
@@ -118,7 +118,7 @@ pub trait Invoke {
118118
/// # Errors
119119
///
120120
/// This function will return an error if the underlying resource fails.
121-
fn set(&self, desired: &str, skip_test: bool) -> Result<SetResult, DscError>;
121+
fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError>;
122122

123123
/// Invoke the test operation on the resource.
124124
///
@@ -199,7 +199,7 @@ impl Invoke for DscResource {
199199
}
200200
}
201201

202-
fn set(&self, desired: &str, skip_test: bool) -> Result<SetResult, DscError> {
202+
fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError> {
203203
match &self.implemented_as {
204204
ImplementedAs::Custom(_custom) => {
205205
Err(DscError::NotImplemented("set custom resources".to_string()))
@@ -209,7 +209,7 @@ impl Invoke for DscResource {
209209
return Err(DscError::MissingManifest(self.type_name.clone()));
210210
};
211211
let resource_manifest = import_manifest(manifest.clone())?;
212-
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test)
212+
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type)
213213
},
214214
}
215215
}

dsc_lib/src/dscresources/invoke_result.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,27 @@ pub enum SetResult {
6969
Group(GroupResourceSetResponse),
7070
}
7171

72+
impl From<TestResult> for SetResult {
73+
fn from(value: TestResult) -> Self {
74+
match value {
75+
TestResult::Group(group) => {
76+
let mut results = Vec::<ResourceSetResult>::new();
77+
for result in group.results {
78+
results.push(result.into());
79+
}
80+
SetResult::Group(GroupResourceSetResponse { results })
81+
},
82+
TestResult::Resource(resource) => {
83+
SetResult::Resource(ResourceSetResponse {
84+
before_state: resource.actual_state,
85+
after_state: resource.desired_state,
86+
changed_properties: if resource.diff_properties.is_empty() { None } else { Some(resource.diff_properties) },
87+
})
88+
}
89+
}
90+
}
91+
}
92+
7293
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
7394
#[serde(deny_unknown_fields)]
7495
pub struct ResourceSetResponse {

0 commit comments

Comments
 (0)