From 431bcd36f8436922eb68b57ec1306266d32b4217 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 27 Jan 2026 17:01:22 -0500 Subject: [PATCH 01/12] add tests for what-if changes --- dsc/tests/dsc_whatif.tests.ps1 | 65 ++++++++++++++++++++ tools/dsctest/dsctest.dsc.manifests.json | 67 +++++++++++++++++++++ tools/dsctest/src/args.rs | 13 +++- tools/dsctest/src/main.rs | 77 ++++++++++++++++++++++++ tools/dsctest/src/whatif_resource.rs | 26 ++++++++ 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 tools/dsctest/src/whatif_resource.rs diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 7841a01b1..d1f92a78f 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -97,4 +97,69 @@ Describe 'whatif tests' { $result.results.Count | Should -Be 1 $LASTEXITCODE | Should -Be 0 } + + It 'native set what-if with whatIfArg for Test/WhatIfNative resource' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: NativeWhatIf + type: Test/WhatIfNative + properties: + name: test + value: testvalue +"@ + $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' + $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' + $what_if_result.results[0].result.afterState.what_if_mode | Should -Be $true + $set_result.results[0].result.afterState.what_if_mode | Should -Be $false + $what_if_result.hadErrors | Should -BeFalse + $set_result.hadErrors | Should -BeFalse + $LASTEXITCODE | Should -Be 0 + } + + It 'synthetic set what-if without whatIfArg for Echo resource' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: SyntheticWhatIf + type: Microsoft.DSC.Debug/Echo + properties: + output: test +"@ + $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' + $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' + $what_if_result.hadErrors | Should -BeFalse + $set_result.hadErrors | Should -BeFalse + $LASTEXITCODE | Should -Be 0 + } + + It 'native delete what-if with whatIfArg for Test/WhatIfNative resource' { + $testInput = @{ + name = "deletetest" + value = "deletevalue" + } | ConvertTo-Json + + $what_if_result = $testInput | dsc resource delete -r Test/WhatIfNative --what-if 2>&1 + $LASTEXITCODE | Should -Be 0 + $what_if_result | Should -Match 'What-if.*delete' + + $actual_result = $testInput | dsc resource delete -r Test/WhatIfNative 2>&1 + $LASTEXITCODE | Should -Be 0 + } + + It 'synthetic delete what-if without whatIfArg for Test/Delete resource' { + $testInput = @{ + delete_called = $false + } | ConvertTo-Json + + $what_if_result = $testInput | dsc resource delete -r Test/Delete --what-if 2>&1 + $LASTEXITCODE | Should -Be 0 + + $actual_result = $testInput | dsc resource delete -r Test/Delete 2>&1 + $LASTEXITCODE | Should -Be 0 + } } diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index e790c1328..a3b5873d2 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -766,6 +766,73 @@ ] } } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/WhatIfNative", + "version": "0.1.0", + "description": "Test resource for validating native WhatIf functionality with whatIfArg", + "get": { + "executable": "dsctest", + "args": [ + "whatif-resource", + "get", + { + "jsonInputArg": "--input" + } + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "whatif-resource", + "set", + { + "whatIfArg": "--what-if" + }, + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "whatif-resource", + "test", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "delete": { + "executable": "dsctest", + "args": [ + "whatif-resource", + "delete", + { + "whatIfArg": "--what-if" + }, + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "whatif-resource" + ] + } + } } ] } diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index ed896dbc8..da62a56bb 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -19,6 +19,7 @@ pub enum Schemas { Trace, Version, WhatIf, + WhatIfResource, } #[derive(Debug, Parser)] @@ -133,5 +134,15 @@ pub enum SubCommand { WhatIf { #[clap(name = "whatif", short, long, help = "Run as a whatif executionType instead of actual executionType")] what_if: bool, - } + }, + + #[clap(name = "whatif-resource", about = "Test resource for native what-if with whatIfArg")] + WhatIfResource { + #[clap(name = "operation", help = "The operation to perform (get, set, test, delete)")] + operation: String, + #[clap(name = "what-if", long, help = "Run in what-if mode")] + what_if: bool, + #[clap(name = "input", long, help = "The input JSON")] + input: Option, + }, } diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 04e2abd81..12325f019 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -16,6 +16,7 @@ mod sleep; mod trace; mod version; mod whatif; +mod whatif_resource; use args::{Args, Schemas, SubCommand}; use clap::Parser; @@ -34,6 +35,7 @@ use crate::sleep::Sleep; use crate::trace::Trace; use crate::version::Version; use crate::whatif::WhatIf; +use crate::whatif_resource::WhatIfResource; use std::{thread, time::Duration}; #[allow(clippy::too_many_lines)] @@ -266,6 +268,9 @@ fn main() { Schemas::WhatIf => { schema_for!(WhatIf) }, + Schemas::WhatIfResource => { + schema_for!(WhatIfResource) + }, }; serde_json::to_string(&schema).unwrap() }, @@ -305,9 +310,81 @@ fn main() { }; serde_json::to_string(&result).unwrap() }, + SubCommand::WhatIfResource { operation, what_if, input } => { + handle_whatif_resource(&operation, what_if, input.as_deref()) + }, }; if !json.is_empty() { println!("{json}"); } } + +fn handle_whatif_resource(operation: &str, what_if: bool, input: Option<&str>) -> String { + let input_str = input.unwrap_or("{}"); + + match operation { + "get" => { + let resource = if input_str == "{}" { + WhatIfResource::new("default".to_string(), "initial".to_string(), false) + } else { + match serde_json::from_str::(input_str) { + Ok(r) => r, + Err(err) => { + eprintln!("Error parsing input JSON: {err}"); + std::process::exit(1); + } + } + }; + serde_json::to_string(&resource).unwrap() + }, + "set" => { + let mut resource = match serde_json::from_str::(input_str) { + Ok(r) => r, + Err(err) => { + eprintln!("Error parsing input JSON: {err}"); + std::process::exit(1); + } + }; + resource.what_if_mode = Some(what_if); + if what_if { + eprintln!("What-if: Would set resource to: {}", serde_json::to_string(&resource).unwrap()); + } + serde_json::to_string(&resource).unwrap() + }, + "test" => { + let resource = match serde_json::from_str::(input_str) { + Ok(r) => r, + Err(err) => { + eprintln!("Error parsing input JSON: {err}"); + std::process::exit(1); + } + }; + // Always return in desired state for test + let result = serde_json::json!({ + "desiredState": resource, + "actualState": resource, + "inDesiredState": true, + "diffProperties": [] + }); + serde_json::to_string(&result).unwrap() + }, + "delete" => { + let resource = match serde_json::from_str::(input_str) { + Ok(r) => r, + Err(err) => { + eprintln!("Error parsing input JSON: {err}"); + std::process::exit(1); + } + }; + if what_if { + eprintln!("What-if: Would delete resource: {}", serde_json::to_string(&resource).unwrap()); + } + String::new() + }, + _ => { + eprintln!("Unknown operation: {operation}"); + std::process::exit(1); + } + } +} diff --git a/tools/dsctest/src/whatif_resource.rs b/tools/dsctest/src/whatif_resource.rs new file mode 100644 index 000000000..d0631625a --- /dev/null +++ b/tools/dsctest/src/whatif_resource.rs @@ -0,0 +1,26 @@ +// 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 WhatIfResource { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub what_if_mode: Option, +} + +impl WhatIfResource { + pub fn new(name: String, value: String, what_if_mode: bool) -> Self { + WhatIfResource { + name: Some(name), + value: Some(value), + what_if_mode: Some(what_if_mode), + } + } +} From b95400a639e167b0bf89cffdc4b9c60a41dcf8a0 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 27 Jan 2026 17:02:27 -0500 Subject: [PATCH 02/12] start what-if changes --- lib/dsc-lib/locales/en-us.toml | 3 +- lib/dsc-lib/src/configure/mod.rs | 2 +- .../src/discovery/command_discovery.rs | 17 ++++- .../src/dscresources/command_resource.rs | 68 +++++++++++++++---- lib/dsc-lib/src/dscresources/dscresource.rs | 8 +-- .../src/dscresources/resource_manifest.rs | 29 ++++++++ 6 files changed, 107 insertions(+), 20 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 6291919c4..8289085e4 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -134,7 +134,7 @@ groupGetResponse = "Group get response: %{response}" failedParseJson = "Failed to parse JSON from 'get': executable = '%{executable}' stdout = '%{stdout}' stderr = '%{stderr}' -> %{err}" invokeSet = "Invoking set for '%{resource}'" noPretest = "No pretest, invoking test on '%{resource}'" -syntheticWhatIf = "cannot process what-if execution type, as resource implements pre-test and does not support what-if" +syntheticWhatIf = "Cannot process what-if execution: resource implements pre-test and does not support what-if. Add a 'whatIfArg' to the set method in the resource manifest to enable what-if support." setGetCurrent = "Getting current state for set by invoking get on '%{resource}' using '%{executable}'" setVerifyGet = "Verifying output of get on '%{resource}' using '%{executable}'" setVerifyOutput = "Verifying output of %{operation} '%{resource}' using '%{executable}'" @@ -147,6 +147,7 @@ testVerifyOutput = "Verifying output of test on '%{resource}' using '%{executabl testGroupTestResponse = "Import resource kind, returning group test response" testNoActualState = "No actual state returned" testNoDiff = "No diff properties returned" +syntheticDeleteWhatIf = "Resource '%{resource}' does not implement native what-if for delete, performing synthetic what-if using get" invokeDeleteUsing = "Invoking delete on '%{resource}' using '%{executable}'" invokeValidateConfig = "Invoking validate on '%{resource}' using '%{config}'" invokeValidateUsing = "Invoking validate on '%{resource}' using '%{executable}'" diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index e0c1f66d4..8e22391fb 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -542,7 +542,7 @@ impl Configurator { }, }; start_datetime = chrono::Local::now(); - if let Err(e) = dsc_resource.delete(&desired) { + if let Err(e) = dsc_resource.delete(&desired, &self.context.execution_type) { progress.set_failure(get_failure_from_error(&e)); progress.write_increment(1); return Err(e); diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index a10768a85..73673446a 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -760,10 +760,18 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result Result r, None => resource.resource_type.clone(), }; - let args = process_args(get.args.as_ref(), filter, &resource_type); + let args = process_args(get.args.as_ref(), filter, &resource_type, &ExecutionKind::Actual); if !filter.is_empty() { verify_json(resource, cwd, filter)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -82,6 +82,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.resource_type)); let operation_type: String; let mut is_synthetic_what_if = false; + let set_method = match execution_type { ExecutionKind::Actual => { operation_type = "set".to_string(); @@ -89,11 +90,23 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t }, ExecutionKind::WhatIf => { operation_type = "whatif".to_string(); - if resource.what_if.is_none() { + // Check if set args contain WhatIf ArgKind + let has_native_whatif = if let Some(set) = &resource.set { + super::resource_manifest::has_whatif_arg(&set.args) + } else { + false + }; + + // Fallback to deprecated whatIf operation if present and set doesn't have whatIfArg + if !has_native_whatif && resource.what_if.is_some() { + &resource.what_if + } else if !has_native_whatif { + // No native what-if support, use synthetic is_synthetic_what_if = true; &resource.set } else { - &resource.what_if + // Resource has native what-if via whatIfArg + &resource.set } } }; @@ -144,7 +157,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t Some(r) => r, None => resource.resource_type.clone(), }; - let args = process_args(get.args.as_ref(), desired, &resource_type); + let args = process_args(get.args.as_ref(), desired, &resource_type, execution_type); let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.resource_type, executable = &get.executable)); @@ -176,7 +189,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let args = process_args(set.args.as_ref(), desired, &resource_type); + let args = process_args(set.args.as_ref(), desired, &resource_type, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -284,7 +297,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, targ Some(r) => r, None => resource.resource_type.clone(), }; - let args = process_args(test.args.as_ref(), expected, &resource_type); + let args = process_args(test.args.as_ref(), expected, &resource_type, &ExecutionKind::Actual); let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.resource_type, executable = &test.executable)); @@ -411,11 +424,12 @@ fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str /// * `resource` - The resource manifest for the command resource. /// * `cwd` - The current working directory. /// * `filter` - The filter to apply to the resource in JSON. +/// * `execution_type` - Whether this is an actual delete or what-if. /// /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option<&str>) -> Result<(), DscError> { +pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, execution_type: &ExecutionKind, target_resource: Option<&str>) -> Result<(), DscError> { let Some(delete) = &resource.delete else { return Err(DscError::NotImplemented("delete".to_string())); }; @@ -426,10 +440,33 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, targ Some(r) => r, None => &resource.resource_type, }; - let args = process_args(delete.args.as_ref(), filter, resource_type); + + // Check if delete operation supports native what-if + let has_native_whatif = super::resource_manifest::has_whatif_arg(&delete.args); + + if execution_type == &ExecutionKind::WhatIf && !has_native_whatif { + // Synthetic what-if for delete: call get to show what would be deleted + debug!("{}", t!("dscresources.commandResource.syntheticDeleteWhatIf", resource = resource_type)); + let get_result = invoke_get(resource, cwd, filter, target_resource.map(|s| s.parse().unwrap()))?; + + // Log the current state that would be deleted + match get_result { + GetResult::Resource(response) => { + info!("What-if: Would delete resource in state: {}", serde_json::to_string_pretty(&response.actual_state)?); + }, + GetResult::Group(responses) => { + info!("What-if: Would delete {} resources", responses.len()); + } + } + return Ok(()); + } + + let args = process_args(delete.args.as_ref(), filter, resource_type, execution_type); let command_input = get_command_input(delete.input.as_ref(), filter)?; + let operation = if execution_type == &ExecutionKind::WhatIf { "what-if delete" } else { "delete" }; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); + debug!("Performing {} operation", operation); let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; Ok(()) @@ -461,7 +498,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &Path, config: &str, ta Some(r) => r, None => &resource.resource_type, }; - let args = process_args(validate.args.as_ref(), config, resource_type); + let args = process_args(validate.args.as_ref(), config, resource_type, &ExecutionKind::Actual); let command_input = get_command_input(validate.input.as_ref(), config)?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); @@ -549,9 +586,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_args(export.args.as_ref(), input, &resource_type); + args = process_args(export.args.as_ref(), input, &resource_type, &ExecutionKind::Actual); } else { - args = process_args(export.args.as_ref(), "", &resource_type); + args = process_args(export.args.as_ref(), "", &resource_type, &ExecutionKind::Actual); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; @@ -596,7 +633,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &Path, input: &str) -> R return Err(DscError::Operation(t!("dscresources.commandResource.resolveNotSupported", resource = &resource.resource_type).to_string())); }; - let args = process_args(resolve.args.as_ref(), input, &resource.resource_type); + let args = process_args(resolve.args.as_ref(), input, &resource.resource_type, &ExecutionKind::Actual); let command_input = get_command_input(resolve.input.as_ref(), input)?; info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.resource_type, executable = &resolve.executable)); @@ -810,7 +847,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &str) -> Option> { +pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -833,6 +870,11 @@ pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &st ArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); processed_args.push(resource_type.to_string()); + }, + ArgKind::WhatIf { what_if_arg } => { + if execution_type == &ExecutionKind::WhatIf { + processed_args.push(what_if_arg.clone()); + } } } } diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index e5b1fcb37..aeb9b8be9 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -238,7 +238,7 @@ impl DscResource { if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { if adapter.capabilities.contains(&Capability::Delete) { adapter.target_resource = Some(resource_name.clone()); - return adapter.delete(filter); + return adapter.delete(filter, &ExecutionKind::Actual); } return Err(DscError::NotSupported(t!("dscresources.dscresource.adapterDoesNotSupportDelete", adapter = adapter.type_name).to_string())); } @@ -336,7 +336,7 @@ pub trait Invoke { /// # Errors /// /// This function will return an error if the underlying resource fails. - fn delete(&self, filter: &str) -> Result<(), DscError>; + fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result<(), DscError>; /// Invoke the validate operation on the resource. /// @@ -469,7 +469,7 @@ impl Invoke for DscResource { } } - fn delete(&self, filter: &str) -> Result<(), DscError> { + fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result<(), DscError> { debug!("{}", t!("dscresources.dscresource.invokeDelete", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { return self.invoke_delete_with_adapter(adapter, &self.type_name, filter); @@ -484,7 +484,7 @@ impl Invoke for DscResource { return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_delete(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) + command_resource::invoke_delete(&resource_manifest, &self.directory, filter, execution_type, self.target_resource.as_deref()) }, } } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index d0dd3b48e..43caa70bb 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -112,6 +112,12 @@ pub enum ArgKind { /// The argument that accepts the resource type name. #[serde(rename = "resourceTypeArg")] resource_type_arg: String, + }, + /// The argument is passed when the resource is invoked in what-if mode. + WhatIf { + /// The argument to pass when in what-if mode. + #[serde(rename = "whatIfArg")] + what_if_arg: String, } } @@ -300,6 +306,12 @@ pub fn import_manifest(manifest: Value) -> Result { // if !manifest.schema_version.eq(MANIFEST_SCHEMA_VERSION) { // return Err(DscError::InvalidManifestSchemaVersion(manifest.schema_version, MANIFEST_SCHEMA_VERSION.to_string())); // } + + // Emit deprecation warning if whatIf operation is defined + if manifest.what_if.is_some() { + tracing::warn!("Resource '{}' uses deprecated 'whatIf' operation. Please migrate to using 'whatIfArg' in the set and/or delete method args instead.", manifest.resource_type); + } + Ok(manifest) } @@ -321,6 +333,23 @@ pub fn validate_semver(version: &str) -> Result<(), semver::Error> { Ok(()) } +/// Check if args contain a WhatIf ArgKind variant. +/// +/// # Arguments +/// +/// * `args` - The optional vector of ArgKind to check. +/// +/// # Returns +/// +/// * `bool` - True if args contain a WhatIf variant, false otherwise. +pub fn has_whatif_arg(args: &Option>) -> bool { + if let Some(args_vec) = args { + args_vec.iter().any(|arg| matches!(arg, ArgKind::WhatIf { .. })) + } else { + false + } +} + #[cfg(test)] mod test { use crate::schemas::dsc_repo::{DscRepoSchema, UnrecognizedSchemaUri}; From 40e01bbdd95f01088ff206c55d1c7e8fc497f0f3 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 28 Jan 2026 14:04:08 -0500 Subject: [PATCH 03/12] refactor whatIf argKind --- dsc/src/subcommand.rs | 1 - .../src/discovery/command_discovery.rs | 19 ---- .../src/dscresources/command_resource.rs | 87 +++++++++++++------ .../src/dscresources/resource_manifest.rs | 58 +++++++------ lib/dsc-lib/src/extensions/discover.rs | 8 +- 5 files changed, 94 insertions(+), 79 deletions(-) diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 1ba90ac1c..44d06a48d 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -784,7 +784,6 @@ pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adap (Capability::Get, "g"), (Capability::Set, "s"), (Capability::SetHandlesExist, "x"), - (Capability::WhatIf, "w"), (Capability::Test, "t"), (Capability::Delete, "d"), (Capability::Export, "e"), diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 73673446a..3914cf9a4 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -760,18 +760,6 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result Result r, None => resource.resource_type.clone(), }; - let args = process_args(get.args.as_ref(), filter, &resource_type, &ExecutionKind::Actual); + let args = process_get_args(get.args.as_ref(), filter, &resource_type); if !filter.is_empty() { verify_json(resource, cwd, filter)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -90,14 +90,14 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t }, ExecutionKind::WhatIf => { operation_type = "whatif".to_string(); - // Check if set args contain WhatIf ArgKind - let has_native_whatif = if let Some(set) = &resource.set { - super::resource_manifest::has_whatif_arg(&set.args) - } else { - false - }; - - // Fallback to deprecated whatIf operation if present and set doesn't have whatIfArg + // Check if set supports native what-if + let has_native_whatif = resource.set.as_ref() + .map_or(false, |set| { + let (_, supports_whatif) = process_set_delete_args(set.args.as_ref(), "", &resource.resource_type, execution_type); + supports_whatif + }); + + // Fallback to deprecated whatIf operation if present and set doesn't support whatIfArg if !has_native_whatif && resource.what_if.is_some() { &resource.what_if } else if !has_native_whatif { @@ -110,7 +110,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t } } }; - let Some(set) = set_method else { + let Some(mut set) = set_method.clone() else { return Err(DscError::NotImplemented("set".to_string())); }; verify_json(resource, cwd, desired)?; @@ -157,7 +157,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t Some(r) => r, None => resource.resource_type.clone(), }; - let args = process_args(get.args.as_ref(), desired, &resource_type, execution_type); + let args = process_get_args(get.args.as_ref(), desired, &resource_type); let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.resource_type, executable = &get.executable)); @@ -189,7 +189,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let args = process_args(set.args.as_ref(), desired, &resource_type, execution_type); + let (args, supports_whatif) = process_set_delete_args(set.args.as_ref(), desired, &resource_type, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -297,7 +297,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, targ Some(r) => r, None => resource.resource_type.clone(), }; - let args = process_args(test.args.as_ref(), expected, &resource_type, &ExecutionKind::Actual); + let args = process_get_args(test.args.as_ref(), expected, &resource_type); let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.resource_type, executable = &test.executable)); @@ -430,7 +430,7 @@ fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str /// /// Error is returned if the underlying command returns a non-zero exit code. pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, execution_type: &ExecutionKind, target_resource: Option<&str>) -> Result<(), DscError> { - let Some(delete) = &resource.delete else { + let Some(delete) = resource.delete.clone() else { return Err(DscError::NotImplemented("delete".to_string())); }; @@ -441,8 +441,8 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, exec None => &resource.resource_type, }; - // Check if delete operation supports native what-if - let has_native_whatif = super::resource_manifest::has_whatif_arg(&delete.args); + // Check if delete operation supports native what-if by checking args + let (args, has_native_whatif) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); if execution_type == &ExecutionKind::WhatIf && !has_native_whatif { // Synthetic what-if for delete: call get to show what would be deleted @@ -461,7 +461,6 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, exec return Ok(()); } - let args = process_args(delete.args.as_ref(), filter, resource_type, execution_type); let command_input = get_command_input(delete.input.as_ref(), filter)?; let operation = if execution_type == &ExecutionKind::WhatIf { "what-if delete" } else { "delete" }; @@ -498,7 +497,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &Path, config: &str, ta Some(r) => r, None => &resource.resource_type, }; - let args = process_args(validate.args.as_ref(), config, resource_type, &ExecutionKind::Actual); + let args = process_get_args(validate.args.as_ref(), config, resource_type); let command_input = get_command_input(validate.input.as_ref(), config)?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); @@ -586,9 +585,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_args(export.args.as_ref(), input, &resource_type, &ExecutionKind::Actual); + args = process_get_args(export.args.as_ref(), input, &resource_type); } else { - args = process_args(export.args.as_ref(), "", &resource_type, &ExecutionKind::Actual); + args = process_get_args(export.args.as_ref(), "", &resource_type); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; @@ -633,7 +632,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &Path, input: &str) -> R return Err(DscError::Operation(t!("dscresources.commandResource.resolveNotSupported", resource = &resource.resource_type).to_string())); }; - let args = process_args(resolve.args.as_ref(), input, &resource.resource_type, &ExecutionKind::Actual); + let args = process_get_args(resolve.args.as_ref(), input, &resource.resource_type); let command_input = get_command_input(resolve.input.as_ref(), input)?; info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.resource_type, executable = &resolve.executable)); @@ -847,7 +846,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> Option> { +pub fn process_get_args(args: Option<&Vec>, input: &str, resource_type: &str) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -856,10 +855,10 @@ pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &st let mut processed_args = Vec::::new(); for arg in arg_values { match arg { - ArgKind::String(s) => { + GetArgKind::String(s) => { processed_args.push(s.clone()); }, - ArgKind::Json { json_input_arg, mandatory } => { + GetArgKind::Json { json_input_arg, mandatory } => { if input.is_empty() && *mandatory != Some(true) { continue; } @@ -867,11 +866,43 @@ pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &st processed_args.push(json_input_arg.clone()); processed_args.push(input.to_string()); }, - ArgKind::ResourceType { resource_type_arg } => { + GetArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); processed_args.push(resource_type.to_string()); }, - ArgKind::WhatIf { what_if_arg } => { + } + } + + Some(processed_args) +} + +pub fn process_set_delete_args(args: Option<&Vec>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> (Option>, bool) { + let Some(arg_values) = args else { + debug!("{}", t!("dscresources.commandResource.noArgs")); + return (None, false); + }; + + let mut processed_args = Vec::::new(); + let mut supports_whatif = false; + for arg in arg_values { + match arg { + SetDeleteArgKind::String(s) => { + processed_args.push(s.clone()); + }, + SetDeleteArgKind::Json { json_input_arg, mandatory } => { + if input.is_empty() && *mandatory != Some(true) { + continue; + } + + processed_args.push(json_input_arg.clone()); + processed_args.push(input.to_string()); + }, + SetDeleteArgKind::ResourceType { resource_type_arg } => { + processed_args.push(resource_type_arg.clone()); + processed_args.push(resource_type.to_string()); + }, + SetDeleteArgKind::WhatIf { what_if_arg } => { + supports_whatif = true; if execution_type == &ExecutionKind::WhatIf { processed_args.push(what_if_arg.clone()); } @@ -879,7 +910,7 @@ pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &st } } - Some(processed_args) + (Some(processed_args), supports_whatif) } struct CommandInput { diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 43caa70bb..ea793d96f 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -96,8 +96,29 @@ pub struct ResourceManifest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(untagged)] -#[dsc_repo_schema(base_name = "commandArgs", folder_path = "definitions")] -pub enum ArgKind { +#[dsc_repo_schema(base_name = "commandArgs.get", folder_path = "definitions")] +pub enum GetArgKind { + /// The argument is a string. + String(String), + /// The argument accepts the JSON input object. + Json { + /// The argument that accepts the JSON input object. + #[serde(rename = "jsonInputArg")] + json_input_arg: String, + /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. + mandatory: Option, + }, + ResourceType { + /// The argument that accepts the resource type name. + #[serde(rename = "resourceTypeArg")] + resource_type_arg: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(untagged)] +#[dsc_repo_schema(base_name = "commandArgs.setDelete", folder_path = "definitions")] +pub enum SetDeleteArgKind { /// The argument is a string. String(String), /// The argument accepts the JSON input object. @@ -170,7 +191,7 @@ pub struct GetMethod { /// The command to run to get the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Get. - pub args: Option>, + pub args: Option>, /// How to pass optional input for a Get. #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, @@ -182,7 +203,7 @@ pub struct SetMethod { /// The command to run to set the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Set. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Set. pub input: Option, /// Whether to run the Test method before the Set method. True means the resource will perform its own test before running the Set method. @@ -202,7 +223,7 @@ pub struct TestMethod { /// The command to run to test the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Test. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Test. pub input: Option, /// The type of return value expected from the Test method. @@ -216,7 +237,7 @@ pub struct DeleteMethod { /// The command to run to delete the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Delete. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Delete. pub input: Option, } @@ -227,7 +248,7 @@ pub struct ValidateMethod { // TODO: enable validation via schema or command /// The command to run to validate the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Validate. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Validate. pub input: Option, } @@ -238,7 +259,7 @@ pub struct ExportMethod { /// The command to run to enumerate instances of the resource. pub executable: String, /// The arguments to pass to the command to perform a Export. - pub args: Option>, + pub args: Option>, /// How to pass input for a Export. pub input: Option, } @@ -249,7 +270,7 @@ pub struct ResolveMethod { /// The command to run to enumerate instances of the resource. pub executable: String, /// The arguments to pass to the command to perform a Export. - pub args: Option>, + pub args: Option>, /// How to pass input for a Export. pub input: Option, } @@ -302,7 +323,7 @@ pub struct ListMethod { pub fn import_manifest(manifest: Value) -> Result { // TODO: enable schema version validation, if not provided, use the latest // const MANIFEST_SCHEMA_VERSION: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json"; - let manifest = serde_json::from_value::(manifest)?; + let mut manifest = serde_json::from_value::(manifest)?; // if !manifest.schema_version.eq(MANIFEST_SCHEMA_VERSION) { // return Err(DscError::InvalidManifestSchemaVersion(manifest.schema_version, MANIFEST_SCHEMA_VERSION.to_string())); // } @@ -333,23 +354,6 @@ pub fn validate_semver(version: &str) -> Result<(), semver::Error> { Ok(()) } -/// Check if args contain a WhatIf ArgKind variant. -/// -/// # Arguments -/// -/// * `args` - The optional vector of ArgKind to check. -/// -/// # Returns -/// -/// * `bool` - True if args contain a WhatIf variant, false otherwise. -pub fn has_whatif_arg(args: &Option>) -> bool { - if let Some(args_vec) = args { - args_vec.iter().any(|arg| matches!(arg, ArgKind::WhatIf { .. })) - } else { - false - } -} - #[cfg(test)] mod test { use crate::schemas::dsc_repo::{DscRepoSchema, UnrecognizedSchemaUri}; diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 5aff5e121..d24714869 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -8,10 +8,10 @@ use crate::{ dscerror::DscError, dscresources::{ command_resource::{ - invoke_command, process_args + invoke_command, process_get_args }, dscresource::DscResource, - resource_manifest::ArgKind, + resource_manifest::GetArgKind, }, extensions::{ dscextension::{ @@ -34,7 +34,7 @@ pub struct DiscoverMethod { /// The command to run to get the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Get. - pub args: Option>, + pub args: Option>, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -68,7 +68,7 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let args = process_args(discover.args.as_ref(), "", self.type_name.as_ref()); + let args = process_get_args(discover.args.as_ref(), "", self.type_name.as_ref()); let (_exit_code, stdout, _stderr) = invoke_command( &discover.executable, args, From 563d2184337dcf17504f60764dfdf04266af8bce Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 28 Jan 2026 14:23:04 -0500 Subject: [PATCH 04/12] tweak tests --- dsc/tests/dsc_whatif.tests.ps1 | 60 +++++++++--------- tools/dsctest/dsctest.dsc.manifests.json | 37 +++--------- tools/dsctest/src/args.rs | 13 +--- tools/dsctest/src/main.rs | 77 ------------------------ tools/dsctest/src/whatif_resource.rs | 26 -------- 5 files changed, 39 insertions(+), 174 deletions(-) delete mode 100644 tools/dsctest/src/whatif_resource.rs diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index d1f92a78f..5209eeaf9 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -98,28 +98,44 @@ Describe 'whatif tests' { $LASTEXITCODE | Should -Be 0 } - It 'native set what-if with whatIfArg for Test/WhatIfNative resource' { - $config_yaml = @" + It 'Test/WhatIfNative resource with operation and WhatIfArgKind works' -TestCases @( + @{ operation = 'set' } + @{ operation = 'delete' } + ) { + param($operation) + + if ($operation -eq 'set') { + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: NativeWhatIf type: Test/WhatIfNative properties: - name: test - value: testvalue + executionType: Actual "@ - $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json - $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json - $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' - $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' - $what_if_result.results[0].result.afterState.what_if_mode | Should -Be $true - $set_result.results[0].result.afterState.what_if_mode | Should -Be $false - $what_if_result.hadErrors | Should -BeFalse - $set_result.hadErrors | Should -BeFalse - $LASTEXITCODE | Should -Be 0 + $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' + $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' + $what_if_result.results[0].result.afterState.execution_type | Should -BeExactly 'WhatIf' + $set_result.results[0].result.afterState.execution_type | Should -BeExactly 'Actual' + $what_if_result.hadErrors | Should -BeFalse + $set_result.hadErrors | Should -BeFalse + } + else { + $testInput = @{ + execution_type = "Actual" + } | ConvertTo-Json + + $what_if_result = $testInput | dsc resource delete -r Test/WhatIfNative --what-if 2>&1 + $LASTEXITCODE | Should -Be 0 + + $actual_result = $testInput | dsc resource delete -r Test/WhatIfNative 2>&1 + $LASTEXITCODE | Should -Be 0 + } } - It 'synthetic set what-if without whatIfArg for Echo resource' { + It 'Echo resource with synthetic what-if works' { $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: @@ -137,21 +153,7 @@ Describe 'whatif tests' { $LASTEXITCODE | Should -Be 0 } - It 'native delete what-if with whatIfArg for Test/WhatIfNative resource' { - $testInput = @{ - name = "deletetest" - value = "deletevalue" - } | ConvertTo-Json - - $what_if_result = $testInput | dsc resource delete -r Test/WhatIfNative --what-if 2>&1 - $LASTEXITCODE | Should -Be 0 - $what_if_result | Should -Match 'What-if.*delete' - - $actual_result = $testInput | dsc resource delete -r Test/WhatIfNative 2>&1 - $LASTEXITCODE | Should -Be 0 - } - - It 'synthetic delete what-if without whatIfArg for Test/Delete resource' { + It 'Test/Delete resource with synthetic delete what-if without whatIfArg works' { $testInput = @{ delete_called = $false } | ConvertTo-Json diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index a3b5873d2..3393aa569 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -775,36 +775,17 @@ "get": { "executable": "dsctest", "args": [ - "whatif-resource", - "get", - { - "jsonInputArg": "--input" - } + "whatif", + "get" ] }, "set": { "executable": "dsctest", "args": [ - "whatif-resource", + "whatif", "set", { - "whatIfArg": "--what-if" - }, - { - "jsonInputArg": "--input", - "mandatory": true - } - ], - "return": "state" - }, - "test": { - "executable": "dsctest", - "args": [ - "whatif-resource", - "test", - { - "jsonInputArg": "--input", - "mandatory": true + "whatIfArg": "-w" } ], "return": "state" @@ -812,14 +793,10 @@ "delete": { "executable": "dsctest", "args": [ - "whatif-resource", + "whatif", "delete", { - "whatIfArg": "--what-if" - }, - { - "jsonInputArg": "--input", - "mandatory": true + "whatIfArg": "-w" } ] }, @@ -829,7 +806,7 @@ "args": [ "schema", "-s", - "whatif-resource" + "whatif" ] } } diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index da62a56bb..ed896dbc8 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -19,7 +19,6 @@ pub enum Schemas { Trace, Version, WhatIf, - WhatIfResource, } #[derive(Debug, Parser)] @@ -134,15 +133,5 @@ pub enum SubCommand { WhatIf { #[clap(name = "whatif", short, long, help = "Run as a whatif executionType instead of actual executionType")] what_if: bool, - }, - - #[clap(name = "whatif-resource", about = "Test resource for native what-if with whatIfArg")] - WhatIfResource { - #[clap(name = "operation", help = "The operation to perform (get, set, test, delete)")] - operation: String, - #[clap(name = "what-if", long, help = "Run in what-if mode")] - what_if: bool, - #[clap(name = "input", long, help = "The input JSON")] - input: Option, - }, + } } diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 12325f019..04e2abd81 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -16,7 +16,6 @@ mod sleep; mod trace; mod version; mod whatif; -mod whatif_resource; use args::{Args, Schemas, SubCommand}; use clap::Parser; @@ -35,7 +34,6 @@ use crate::sleep::Sleep; use crate::trace::Trace; use crate::version::Version; use crate::whatif::WhatIf; -use crate::whatif_resource::WhatIfResource; use std::{thread, time::Duration}; #[allow(clippy::too_many_lines)] @@ -268,9 +266,6 @@ fn main() { Schemas::WhatIf => { schema_for!(WhatIf) }, - Schemas::WhatIfResource => { - schema_for!(WhatIfResource) - }, }; serde_json::to_string(&schema).unwrap() }, @@ -310,81 +305,9 @@ fn main() { }; serde_json::to_string(&result).unwrap() }, - SubCommand::WhatIfResource { operation, what_if, input } => { - handle_whatif_resource(&operation, what_if, input.as_deref()) - }, }; if !json.is_empty() { println!("{json}"); } } - -fn handle_whatif_resource(operation: &str, what_if: bool, input: Option<&str>) -> String { - let input_str = input.unwrap_or("{}"); - - match operation { - "get" => { - let resource = if input_str == "{}" { - WhatIfResource::new("default".to_string(), "initial".to_string(), false) - } else { - match serde_json::from_str::(input_str) { - Ok(r) => r, - Err(err) => { - eprintln!("Error parsing input JSON: {err}"); - std::process::exit(1); - } - } - }; - serde_json::to_string(&resource).unwrap() - }, - "set" => { - let mut resource = match serde_json::from_str::(input_str) { - Ok(r) => r, - Err(err) => { - eprintln!("Error parsing input JSON: {err}"); - std::process::exit(1); - } - }; - resource.what_if_mode = Some(what_if); - if what_if { - eprintln!("What-if: Would set resource to: {}", serde_json::to_string(&resource).unwrap()); - } - serde_json::to_string(&resource).unwrap() - }, - "test" => { - let resource = match serde_json::from_str::(input_str) { - Ok(r) => r, - Err(err) => { - eprintln!("Error parsing input JSON: {err}"); - std::process::exit(1); - } - }; - // Always return in desired state for test - let result = serde_json::json!({ - "desiredState": resource, - "actualState": resource, - "inDesiredState": true, - "diffProperties": [] - }); - serde_json::to_string(&result).unwrap() - }, - "delete" => { - let resource = match serde_json::from_str::(input_str) { - Ok(r) => r, - Err(err) => { - eprintln!("Error parsing input JSON: {err}"); - std::process::exit(1); - } - }; - if what_if { - eprintln!("What-if: Would delete resource: {}", serde_json::to_string(&resource).unwrap()); - } - String::new() - }, - _ => { - eprintln!("Unknown operation: {operation}"); - std::process::exit(1); - } - } -} diff --git a/tools/dsctest/src/whatif_resource.rs b/tools/dsctest/src/whatif_resource.rs deleted file mode 100644 index d0631625a..000000000 --- a/tools/dsctest/src/whatif_resource.rs +++ /dev/null @@ -1,26 +0,0 @@ -// 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 WhatIfResource { - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub value: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub what_if_mode: Option, -} - -impl WhatIfResource { - pub fn new(name: String, value: String, what_if_mode: bool) -> Self { - WhatIfResource { - name: Some(name), - value: Some(value), - what_if_mode: Some(what_if_mode), - } - } -} From a0a9f9d0e4ea72377370d4f67912824a6d1ed6ac Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Fri, 30 Jan 2026 17:09:34 -0500 Subject: [PATCH 05/12] only add set path for now --- dsc-bicep-ext/src/main.rs | 2 +- dsc/src/mcp/invoke_dsc_resource.rs | 2 +- dsc/src/resource_command.rs | 4 +- dsc/tests/dsc_whatif.tests.ps1 | 62 +++++-------------- lib/dsc-lib/locales/en-us.toml | 1 + .../src/dscresources/command_resource.rs | 7 ++- .../src/dscresources/resource_manifest.rs | 7 +-- tools/dsctest/dsctest.dsc.manifests.json | 18 +----- 8 files changed, 29 insertions(+), 74 deletions(-) diff --git a/dsc-bicep-ext/src/main.rs b/dsc-bicep-ext/src/main.rs index 9f6fce0ab..734bf3063 100644 --- a/dsc-bicep-ext/src/main.rs +++ b/dsc-bicep-ext/src/main.rs @@ -234,7 +234,7 @@ impl BicepExtension for BicepExtensionService { }; resource - .delete(&identifiers) + .delete(&identifiers, &ExecutionKind::Actual) .map_err(|e| Status::aborted(e.to_string()))?; Ok(Response::new(LocalExtensibilityOperationResponse { diff --git a/dsc/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs index 1285455a6..8a687780b 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -97,7 +97,7 @@ impl McpServer { Ok(ResourceOperationResult::TestResult(result)) }, DscOperation::Delete => { - match resource.delete(&properties_json) { + match resource.delete(&properties_json, &ExecutionKind::Actual) { Ok(()) => Ok(ResourceOperationResult::DeleteResult { success: true }), Err(e) => Err(McpError::internal_error(e.to_string(), None)), } diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index f9243b67b..83f42bc1e 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -168,7 +168,7 @@ pub fn set(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, inp } }; - if let Err(err) = resource.delete(input) { + if let Err(err) = resource.delete(input, &ExecutionKind::Actual) { error!("{err}"); exit(EXIT_DSC_ERROR); } @@ -268,7 +268,7 @@ pub fn delete(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, exit(EXIT_DSC_ERROR); } - match resource.delete(input) { + match resource.delete(input, &ExecutionKind::Actual) { Ok(()) => {} Err(err) => { error!("{err}"); diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 5209eeaf9..3a3aae373 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -98,41 +98,23 @@ Describe 'whatif tests' { $LASTEXITCODE | Should -Be 0 } - It 'Test/WhatIfNative resource with operation and WhatIfArgKind works' -TestCases @( - @{ operation = 'set' } - @{ operation = 'delete' } - ) { - param($operation) - - if ($operation -eq 'set') { - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: NativeWhatIf - type: Test/WhatIfNative - properties: - executionType: Actual + It 'Test/WhatIfNative resource with set operation and WhatIfArgKind works' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: WhatIfArgKind + type: Test/WhatIfArgKind + properties: + executionType: Actual "@ - $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json - $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json - $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' - $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' - $what_if_result.results[0].result.afterState.execution_type | Should -BeExactly 'WhatIf' - $set_result.results[0].result.afterState.execution_type | Should -BeExactly 'Actual' - $what_if_result.hadErrors | Should -BeFalse - $set_result.hadErrors | Should -BeFalse - } - else { - $testInput = @{ - execution_type = "Actual" - } | ConvertTo-Json - - $what_if_result = $testInput | dsc resource delete -r Test/WhatIfNative --what-if 2>&1 - $LASTEXITCODE | Should -Be 0 - - $actual_result = $testInput | dsc resource delete -r Test/WhatIfNative 2>&1 - $LASTEXITCODE | Should -Be 0 - } + $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' + $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' + $what_if_result.results[0].result.afterState.executionType | Should -BeExactly 'WhatIf' + $set_result.results[0].result.afterState.executionType | Should -BeExactly 'Actual' + $what_if_result.hadErrors | Should -BeFalse + $set_result.hadErrors | Should -BeFalse } It 'Echo resource with synthetic what-if works' { @@ -152,16 +134,4 @@ Describe 'whatif tests' { $set_result.hadErrors | Should -BeFalse $LASTEXITCODE | Should -Be 0 } - - It 'Test/Delete resource with synthetic delete what-if without whatIfArg works' { - $testInput = @{ - delete_called = $false - } | ConvertTo-Json - - $what_if_result = $testInput | dsc resource delete -r Test/Delete --what-if 2>&1 - $LASTEXITCODE | Should -Be 0 - - $actual_result = $testInput | dsc resource delete -r Test/Delete 2>&1 - $LASTEXITCODE | Should -Be 0 - } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index f1e04c1ff..08ac646ca 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -178,6 +178,7 @@ invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, an inDesiredStateNotBool = "'_inDesiredState' is not a boolean" exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation" runProcessError = "Failed to run process '%{executable}': %{error}" +whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. Please migrate to using 'whatIfArg' in the set and/or delete method args instead." [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 8ecfb21f5..ba45cc512 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -9,7 +9,7 @@ use serde_json::{Map, Value}; use std::{collections::HashMap, env, path::Path, process::Stdio}; use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, types::FullyQualifiedTypeName, util::canonicalize_which}; use crate::dscerror::DscError; -use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{GetArgKind, SetDeleteArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind, SetMethod, DeleteMethod}}; +use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{GetArgKind, SetDeleteArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; use tracing::{error, warn, info, debug, trace}; use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; @@ -99,6 +99,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t // Fallback to deprecated whatIf operation if present and set doesn't support whatIfArg if !has_native_whatif && resource.what_if.is_some() { + warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource.resource_type)); &resource.what_if } else if !has_native_whatif { // No native what-if support, use synthetic @@ -110,7 +111,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t } } }; - let Some(mut set) = set_method.clone() else { + let Some(set) = set_method.clone() else { return Err(DscError::NotImplemented("set".to_string())); }; verify_json(resource, cwd, desired)?; @@ -189,7 +190,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let (args, supports_whatif) = process_set_delete_args(set.args.as_ref(), desired, &resource_type, execution_type); + let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &resource_type, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index ea793d96f..d046fb8ba 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -323,16 +323,11 @@ pub struct ListMethod { pub fn import_manifest(manifest: Value) -> Result { // TODO: enable schema version validation, if not provided, use the latest // const MANIFEST_SCHEMA_VERSION: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json"; - let mut manifest = serde_json::from_value::(manifest)?; + let manifest = serde_json::from_value::(manifest)?; // if !manifest.schema_version.eq(MANIFEST_SCHEMA_VERSION) { // return Err(DscError::InvalidManifestSchemaVersion(manifest.schema_version, MANIFEST_SCHEMA_VERSION.to_string())); // } - // Emit deprecation warning if whatIf operation is defined - if manifest.what_if.is_some() { - tracing::warn!("Resource '{}' uses deprecated 'whatIf' operation. Please migrate to using 'whatIfArg' in the set and/or delete method args instead.", manifest.resource_type); - } - Ok(manifest) } diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 2b8d67f87..806e61aea 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -794,44 +794,32 @@ }, { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "type": "Test/WhatIfNative", + "type": "Test/WhatIfArgKind", "version": "0.1.0", "description": "Test resource for validating native WhatIf functionality with whatIfArg", "get": { "executable": "dsctest", "args": [ - "whatif", - "get" + "whatif" ] }, "set": { "executable": "dsctest", "args": [ "whatif", - "set", { "whatIfArg": "-w" } ], "return": "state" }, - "delete": { - "executable": "dsctest", - "args": [ - "whatif", - "delete", - { - "whatIfArg": "-w" - } - ] - }, "schema": { "command": { "executable": "dsctest", "args": [ "schema", "-s", - "whatif" + "what-if" ] } } From 87ce9610c3544e49e947cd4729a1055d27c8a730 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Mon, 2 Feb 2026 15:35:27 -0500 Subject: [PATCH 06/12] revert delete changes for now --- dsc-bicep-ext/src/main.rs | 2 +- dsc/src/mcp/invoke_dsc_resource.rs | 2 +- dsc/src/resource_command.rs | 4 +-- lib/dsc-lib/src/configure/mod.rs | 2 +- .../src/dscresources/command_resource.rs | 27 +++---------------- lib/dsc-lib/src/dscresources/dscresource.rs | 8 +++--- 6 files changed, 12 insertions(+), 33 deletions(-) diff --git a/dsc-bicep-ext/src/main.rs b/dsc-bicep-ext/src/main.rs index 734bf3063..9f6fce0ab 100644 --- a/dsc-bicep-ext/src/main.rs +++ b/dsc-bicep-ext/src/main.rs @@ -234,7 +234,7 @@ impl BicepExtension for BicepExtensionService { }; resource - .delete(&identifiers, &ExecutionKind::Actual) + .delete(&identifiers) .map_err(|e| Status::aborted(e.to_string()))?; Ok(Response::new(LocalExtensibilityOperationResponse { diff --git a/dsc/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs index 8a687780b..1285455a6 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -97,7 +97,7 @@ impl McpServer { Ok(ResourceOperationResult::TestResult(result)) }, DscOperation::Delete => { - match resource.delete(&properties_json, &ExecutionKind::Actual) { + match resource.delete(&properties_json) { Ok(()) => Ok(ResourceOperationResult::DeleteResult { success: true }), Err(e) => Err(McpError::internal_error(e.to_string(), None)), } diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 83f42bc1e..f9243b67b 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -168,7 +168,7 @@ pub fn set(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, inp } }; - if let Err(err) = resource.delete(input, &ExecutionKind::Actual) { + if let Err(err) = resource.delete(input) { error!("{err}"); exit(EXIT_DSC_ERROR); } @@ -268,7 +268,7 @@ pub fn delete(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, exit(EXIT_DSC_ERROR); } - match resource.delete(input, &ExecutionKind::Actual) { + match resource.delete(input) { Ok(()) => {} Err(err) => { error!("{err}"); diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 4f31955a2..0be52c23b 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -542,7 +542,7 @@ impl Configurator { }, }; start_datetime = chrono::Local::now(); - if let Err(e) = dsc_resource.delete(&desired, &self.context.execution_type) { + if let Err(e) = dsc_resource.delete(&desired) { progress.set_failure(get_failure_from_error(&e)); progress.write_increment(1); return Err(e); diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index ba45cc512..b1d339d1f 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -430,8 +430,8 @@ fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, execution_type: &ExecutionKind, target_resource: Option<&str>) -> Result<(), DscError> { - let Some(delete) = resource.delete.clone() else { +pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option<&str>) -> Result<(), DscError> { + let Some(delete) = &resource.delete else { return Err(DscError::NotImplemented("delete".to_string())); }; @@ -441,32 +441,11 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, exec Some(r) => r, None => &resource.resource_type, }; - - // Check if delete operation supports native what-if by checking args - let (args, has_native_whatif) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); - - if execution_type == &ExecutionKind::WhatIf && !has_native_whatif { - // Synthetic what-if for delete: call get to show what would be deleted - debug!("{}", t!("dscresources.commandResource.syntheticDeleteWhatIf", resource = resource_type)); - let get_result = invoke_get(resource, cwd, filter, target_resource.map(|s| s.parse().unwrap()))?; - - // Log the current state that would be deleted - match get_result { - GetResult::Resource(response) => { - info!("What-if: Would delete resource in state: {}", serde_json::to_string_pretty(&response.actual_state)?); - }, - GetResult::Group(responses) => { - info!("What-if: Would delete {} resources", responses.len()); - } - } - return Ok(()); - } + let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, &ExecutionKind::Actual); let command_input = get_command_input(delete.input.as_ref(), filter)?; - let operation = if execution_type == &ExecutionKind::WhatIf { "what-if delete" } else { "delete" }; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); - debug!("Performing {} operation", operation); let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; Ok(()) diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index aeb9b8be9..e5b1fcb37 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -238,7 +238,7 @@ impl DscResource { if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { if adapter.capabilities.contains(&Capability::Delete) { adapter.target_resource = Some(resource_name.clone()); - return adapter.delete(filter, &ExecutionKind::Actual); + return adapter.delete(filter); } return Err(DscError::NotSupported(t!("dscresources.dscresource.adapterDoesNotSupportDelete", adapter = adapter.type_name).to_string())); } @@ -336,7 +336,7 @@ pub trait Invoke { /// # Errors /// /// This function will return an error if the underlying resource fails. - fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result<(), DscError>; + fn delete(&self, filter: &str) -> Result<(), DscError>; /// Invoke the validate operation on the resource. /// @@ -469,7 +469,7 @@ impl Invoke for DscResource { } } - fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result<(), DscError> { + fn delete(&self, filter: &str) -> Result<(), DscError> { debug!("{}", t!("dscresources.dscresource.invokeDelete", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { return self.invoke_delete_with_adapter(adapter, &self.type_name, filter); @@ -484,7 +484,7 @@ impl Invoke for DscResource { return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_delete(&resource_manifest, &self.directory, filter, execution_type, self.target_resource.as_deref()) + command_resource::invoke_delete(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) }, } } From 7dae7297d834de8789ae19b7ecbcb1ac0e0a2a13 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Mon, 2 Feb 2026 16:46:46 -0500 Subject: [PATCH 07/12] add link to github issue in warning message --- lib/dsc-lib/locales/en-us.toml | 3 +-- .../src/dscresources/command_resource.rs | 17 ++++++++--------- .../src/dscresources/resource_manifest.rs | 1 - 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 08ac646ca..847e2ea2e 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -148,7 +148,6 @@ testVerifyOutput = "Verifying output of test on '%{resource}' using '%{executabl testGroupTestResponse = "Import resource kind, returning group test response" testNoActualState = "No actual state returned" testNoDiff = "No diff properties returned" -syntheticDeleteWhatIf = "Resource '%{resource}' does not implement native what-if for delete, performing synthetic what-if using get" invokeDeleteUsing = "Invoking delete on '%{resource}' using '%{executable}'" invokeValidateConfig = "Invoking validate on '%{resource}' using '%{config}'" invokeValidateUsing = "Invoking validate on '%{resource}' using '%{executable}'" @@ -178,7 +177,7 @@ invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, an inDesiredStateNotBool = "'_inDesiredState' is not a boolean" exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation" runProcessError = "Failed to run process '%{executable}': %{error}" -whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. Please migrate to using 'whatIfArg' in the set and/or delete method args instead." +whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. See https://github.com/PowerShell/DSC/issues/1361 for migration information." [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index b1d339d1f..af43e8cc0 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -97,17 +97,16 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t supports_whatif }); - // Fallback to deprecated whatIf operation if present and set doesn't support whatIfArg - if !has_native_whatif && resource.what_if.is_some() { - warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource.resource_type)); - &resource.what_if - } else if !has_native_whatif { - // No native what-if support, use synthetic - is_synthetic_what_if = true; + if has_native_whatif { &resource.set } else { - // Resource has native what-if via whatIfArg - &resource.set + if resource.what_if.is_some() { + warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource.resource_type)); + &resource.what_if + } else { + is_synthetic_what_if = true; + &resource.set + } } } }; diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index d046fb8ba..be63aa589 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -327,7 +327,6 @@ pub fn import_manifest(manifest: Value) -> Result { // if !manifest.schema_version.eq(MANIFEST_SCHEMA_VERSION) { // return Err(DscError::InvalidManifestSchemaVersion(manifest.schema_version, MANIFEST_SCHEMA_VERSION.to_string())); // } - Ok(manifest) } From c4b9d89b6380da2c489c1d1aa0247185fd5a131b Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Mon, 2 Feb 2026 16:49:12 -0500 Subject: [PATCH 08/12] rever i8n update --- lib/dsc-lib/locales/en-us.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 847e2ea2e..04e250a55 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -135,7 +135,7 @@ groupGetResponse = "Group get response: %{response}" failedParseJson = "Failed to parse JSON from 'get': executable = '%{executable}' stdout = '%{stdout}' stderr = '%{stderr}' -> %{err}" invokeSet = "Invoking set for '%{resource}'" noPretest = "No pretest, invoking test on '%{resource}'" -syntheticWhatIf = "Cannot process what-if execution: resource implements pre-test and does not support what-if. Add a 'whatIfArg' to the set method in the resource manifest to enable what-if support." +syntheticWhatIf = "cannot process what-if execution type, as resource implements pre-test and does not support what-if" setGetCurrent = "Getting current state for set by invoking get on '%{resource}' using '%{executable}'" setVerifyGet = "Verifying output of get on '%{resource}' using '%{executable}'" setVerifyOutput = "Verifying output of %{operation} '%{resource}' using '%{executable}'" From b9edefdab0ea7aedc6047b83e062ff2de11ed532 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Mon, 2 Feb 2026 16:59:56 -0500 Subject: [PATCH 09/12] address copilot feedback --- dsc/tests/dsc_whatif.tests.ps1 | 12 ++++++------ lib/dsc-lib/src/dscresources/command_resource.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 3a3aae373..9ffc2e6ec 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -100,12 +100,12 @@ Describe 'whatif tests' { It 'Test/WhatIfNative resource with set operation and WhatIfArgKind works' { $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: WhatIfArgKind - type: Test/WhatIfArgKind - properties: - executionType: Actual + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: WhatIfArgKind + type: Test/WhatIfArgKind + properties: + executionType: Actual "@ $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index af43e8cc0..0f8a08d5b 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -110,7 +110,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t } } }; - let Some(set) = set_method.clone() else { + let Some(set) = set_method.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; verify_json(resource, cwd, desired)?; From b8d380ca8e3b1fc5edc9a4ab852dfeeac802c9b4 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 13:10:27 -0500 Subject: [PATCH 10/12] fix test --- lib/dsc-lib/tests/integration/schemas/schema_for.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/dsc-lib/tests/integration/schemas/schema_for.rs b/lib/dsc-lib/tests/integration/schemas/schema_for.rs index ef35cc1cb..d503c522b 100644 --- a/lib/dsc-lib/tests/integration/schemas/schema_for.rs +++ b/lib/dsc-lib/tests/integration/schemas/schema_for.rs @@ -103,7 +103,8 @@ macro_rules! test_schema_for { #[allow(unused_must_use)] #[cfg(test)] mod resource_manifest { test_schema_for!(dsc_lib::dscresources::resource_manifest::Kind); - test_schema_for!(dsc_lib::dscresources::resource_manifest::ArgKind); + test_schema_for!(dsc_lib::dscresources::resource_manifest::GetArgKind); + test_schema_for!(dsc_lib::dscresources::resource_manifest::SetDeleteArgKind); test_schema_for!(dsc_lib::dscresources::resource_manifest::InputKind); test_schema_for!(dsc_lib::dscresources::resource_manifest::SchemaKind); test_schema_for!(dsc_lib::dscresources::resource_manifest::SchemaCommand); From 987919098b7373289df5160203900fe8a88418ea Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 13:41:16 -0500 Subject: [PATCH 11/12] fix pester tests --- dsc/tests/dsc_discovery.tests.ps1 | 2 +- dsc/tests/dsc_whatif.tests.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 3e750fa03..a0e6b6256 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -189,7 +189,6 @@ Describe 'tests for resource discovery' { @{ operation = 'delete' } @{ operation = 'export' } @{ operation = 'resolve' } - @{ operation = 'whatIf' } ) { param($operation) @@ -212,6 +211,7 @@ Describe 'tests for resource discovery' { $out.Count | Should -Be 1 $out.Type | Should -BeExactly 'Test/ExecutableNotFound' $out.Kind | Should -BeExactly 'resource' + (Get-Content -Path "$testdrive/error.txt" -Raw) (Get-Content -Path "$testdrive/error.txt" -Raw) | Should -Match "INFO.*?Executable 'doesNotExist' not found" } finally { diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 9ffc2e6ec..a5c22cac7 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -103,8 +103,8 @@ Describe 'whatif tests' { `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: WhatIfArgKind - type: Test/WhatIfArgKind - properties: + type: Test/WhatIfArgKind + properties: executionType: Actual "@ $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json From a09a7b03b4f4a394654bc413a9a9d3c5072aaeb6 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 4 Feb 2026 10:08:57 -0500 Subject: [PATCH 12/12] address Steve's feedback --- dsc/tests/dsc_whatif.tests.ps1 | 30 +++++++++++-------- .../src/dscresources/command_resource.rs | 14 +++++++-- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index a5c22cac7..1e69302d7 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -15,14 +15,15 @@ Describe 'whatif tests' { output: hello "@ $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' $what_if_result.results.result.beforeState.output | Should -Be $set_result.results.result.beforeState.output $what_if_result.results.result.afterState.output | Should -Be $set_result.results.result.afterState.output $what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties $what_if_result.hadErrors | Should -BeFalse $what_if_result.results.Count | Should -Be 1 - $LASTEXITCODE | Should -Be 0 } It 'config set whatif when actual state does not match desired state' -Skip:(!$IsWindows) { @@ -36,7 +37,9 @@ Describe 'whatif tests' { keyPath: 'HKCU\1\2' "@ $what_if_result = dsc config set -w -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $set_result = dsc config set -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' $what_if_result.results.result.beforeState._exist | Should -Be $set_result.results.result.beforeState._exist $what_if_result.results.result.beforeState.keyPath | Should -Be $set_result.results.result.beforeState.keyPath @@ -46,14 +49,12 @@ Describe 'whatif tests' { $what_if_result.results.result.changedProperties | Should -Be @('_metadata', '_exist') $what_if_result.hadErrors | Should -BeFalse $what_if_result.results.Count | Should -Be 1 - $LASTEXITCODE | Should -Be 0 - } It 'config set whatif for group resource' { $result = dsc config set -f $PSScriptRoot/../examples/groups.dsc.yaml -w 2>&1 - $result | Should -Match 'ERROR.*?Not implemented.*?what-if' $LASTEXITCODE | Should -Be 2 + $result | Should -Match 'ERROR.*?Not implemented.*?what-if' } It 'actual execution of WhatIf resource' { @@ -66,12 +67,12 @@ Describe 'whatif tests' { executionType: Actual "@ $result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $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 via ' -TestCases @( @@ -90,12 +91,12 @@ Describe 'whatif tests' { executionType: Actual "@ $result = $config_yaml | dsc config set $alias -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $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 } It 'Test/WhatIfNative resource with set operation and WhatIfArgKind works' { @@ -108,13 +109,15 @@ Describe 'whatif tests' { executionType: Actual "@ $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json - $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' - $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' $what_if_result.results[0].result.afterState.executionType | Should -BeExactly 'WhatIf' - $set_result.results[0].result.afterState.executionType | Should -BeExactly 'Actual' $what_if_result.hadErrors | Should -BeFalse + $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 $set_result.hadErrors | Should -BeFalse + $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' + $set_result.results[0].result.afterState.executionType | Should -BeExactly 'Actual' } It 'Echo resource with synthetic what-if works' { @@ -127,11 +130,12 @@ Describe 'whatif tests' { output: test "@ $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json - $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json - $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' - $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' + $LASTEXITCODE | Should -Be 0 $what_if_result.hadErrors | Should -BeFalse - $set_result.hadErrors | Should -BeFalse + $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' + $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 + $set_result.hadErrors | Should -BeFalse + $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' } } diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 0f8a08d5b..c73868cbe 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -815,11 +815,11 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option } } -/// Process the arguments for a command resource. +/// Process the arguments for a command resource's get operation. /// /// # Arguments /// -/// * `args` - The arguments to process +/// * `args` - The Get arguments to process /// * `value` - The value to use for JSON input arguments /// /// # Returns @@ -855,6 +855,16 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, resource_ty Some(processed_args) } +/// Process the arguments for a command resource's set or delete operation. +/// +/// # Arguments +/// +/// * `args` - The Set/Delete arguments to process +/// * `value` - The value to use for JSON input arguments +/// +/// # Returns +/// +/// A vector of strings representing the processed arguments pub fn process_set_delete_args(args: Option<&Vec>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> (Option>, bool) { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs"));