From b03e2436c6dcc6efbac67a8e537bbc3698cad4ac Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 13:29:01 -0500 Subject: [PATCH 1/7] start delete changes --- dsc/tests/dsc_whatif.tests.ps1 | 20 ++++++++++++++ tools/dsctest/dsctest.dsc.manifests.json | 34 ++++++++++++++++++++++++ tools/dsctest/src/whatif.rs | 2 ++ 3 files changed, 56 insertions(+) diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 9ffc2e6ec..6c25e93c5 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -134,4 +134,24 @@ Describe 'whatif tests' { $set_result.hadErrors | Should -BeFalse $LASTEXITCODE | Should -Be 0 } + + It 'Test/WhatIfDelete resource with set operation and WhatIfArgKind works' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: WhatIfDelete + type: Test/WhatIfDelete + properties: + executionType: Actual + _exist: false +"@ + $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 + } } diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 806e61aea..5dea5fa4f 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -823,6 +823,40 @@ ] } } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/WhatIfDelete", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "whatif", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "delete": { + "executable": "dsctest", + "args": [ + "whatif", + { + "whatIfArg": "-w" + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "what-if" + ] + } + } } ] } diff --git a/tools/dsctest/src/whatif.rs b/tools/dsctest/src/whatif.rs index fd37bf1ee..bd9d22a0b 100644 --- a/tools/dsctest/src/whatif.rs +++ b/tools/dsctest/src/whatif.rs @@ -9,4 +9,6 @@ use serde::{Deserialize, Serialize}; pub struct WhatIf { #[serde(rename = "executionType")] pub execution_type: String, + #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] + pub exist: bool, } From 5e00e78f5b794161b989d5b05beac5770931e55a Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 14:03:33 -0500 Subject: [PATCH 2/7] update delete test resource --- tools/dsctest/dsctest.dsc.manifests.json | 6 +----- tools/dsctest/src/main.rs | 4 ++-- tools/dsctest/src/whatif.rs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 5dea5fa4f..a44f27c2b 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -831,11 +831,7 @@ "get": { "executable": "dsctest", "args": [ - "whatif", - { - "jsonInputArg": "--input", - "mandatory": true - } + "whatif" ] }, "delete": { diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 23bdf1fe9..0c9f5107e 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -318,9 +318,9 @@ fn main() { }, SubCommand::WhatIf { what_if } => { let result: WhatIf = if what_if { - WhatIf { execution_type: "WhatIf".to_string() } + WhatIf { execution_type: "WhatIf".to_string(), exist: None } } else { - WhatIf { execution_type: "Actual".to_string() } + WhatIf { execution_type: "Actual".to_string(), exist: None } }; serde_json::to_string(&result).unwrap() }, diff --git a/tools/dsctest/src/whatif.rs b/tools/dsctest/src/whatif.rs index bd9d22a0b..06850d4b1 100644 --- a/tools/dsctest/src/whatif.rs +++ b/tools/dsctest/src/whatif.rs @@ -10,5 +10,5 @@ pub struct WhatIf { #[serde(rename = "executionType")] pub execution_type: String, #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] - pub exist: bool, + pub exist: Option, } From 93d1a1176791d24311028449db50379d1e9b4ecd Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 16:09:05 -0500 Subject: [PATCH 3/7] implement what-if handling in delete via argkind --- dsc-bicep-ext/src/main.rs | 2 +- dsc/src/mcp/invoke_dsc_resource.rs | 4 +- dsc/src/resource_command.rs | 6 +- lib/dsc-lib/src/configure/mod.rs | 119 +++++++++--------- .../src/dscresources/command_resource.rs | 17 ++- lib/dsc-lib/src/dscresources/dscresource.rs | 16 +-- lib/dsc-lib/src/dscresources/invoke_result.rs | 17 +++ tools/dsctest/src/args.rs | 7 ++ tools/dsctest/src/main.rs | 19 ++- tools/dsctest/src/whatif_delete.rs | 15 +++ 10 files changed, 142 insertions(+), 80 deletions(-) create mode 100644 tools/dsctest/src/whatif_delete.rs 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..5cd171e97 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -97,8 +97,8 @@ impl McpServer { Ok(ResourceOperationResult::TestResult(result)) }, DscOperation::Delete => { - match resource.delete(&properties_json) { - Ok(()) => Ok(ResourceOperationResult::DeleteResult { success: true }), + 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..82444d245 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,8 +268,8 @@ pub fn delete(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, exit(EXIT_DSC_ERROR); } - match resource.delete(input) { - Ok(()) => {} + match resource.delete(input, &ExecutionKind::Actual) { + Ok(_) => {} Err(err) => { error!("{err}"); exit(EXIT_DSC_ERROR); diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 0be52c23b..f61b91db5 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -8,7 +8,7 @@ use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; use crate::dscresources::{ {dscresource::{Capability, Invoke, get_diff, validate_properties, get_adapter_input_kind}, - invoke_result::{GetResult, SetResult, TestResult, ExportResult, ResourceSetResponse}}, + invoke_result::{DeleteResult, GetResult, SetResult, TestResult, ExportResult, ResourceSetResponse}}, resource_manifest::{AdapterInputKind, Kind}, }; use crate::DscResource; @@ -506,6 +506,7 @@ impl Configurator { let start_datetime; let end_datetime; let mut set_result; + let mut delete_result: Option = None; if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) { debug!("{}", t!("configure.mod.handlesExist")); start_datetime = chrono::Local::now(); @@ -520,76 +521,76 @@ impl Configurator { end_datetime = chrono::Local::now(); } else if dsc_resource.capabilities.contains(&Capability::Delete) { debug!("{}", t!("configure.mod.implementsDelete")); - if self.context.execution_type == ExecutionKind::WhatIf { - // Let the resource handle WhatIf via set (-w), which may route to delete - start_datetime = chrono::Local::now(); - set_result = match dsc_resource.set(&desired, skip_test, &self.context.execution_type) { - Ok(result) => result, - Err(e) => { - progress.set_failure(get_failure_from_error(&e)); - progress.write_increment(1); - return Err(e); - }, - }; - end_datetime = chrono::Local::now(); - } else { - let before_result = match dsc_resource.get(&desired) { - Ok(result) => result, - Err(e) => { - progress.set_failure(get_failure_from_error(&e)); - progress.write_increment(1); - return Err(e); - }, - }; - start_datetime = chrono::Local::now(); - if let Err(e) = dsc_resource.delete(&desired) { + let before_result = match dsc_resource.get(&desired) { + Ok(result) => result, + Err(e) => { progress.set_failure(get_failure_from_error(&e)); progress.write_increment(1); return Err(e); - } - let after_result = match dsc_resource.get(&desired) { - Ok(result) => result, - Err(e) => { - progress.set_failure(get_failure_from_error(&e)); - progress.write_increment(1); - return Err(e); - }, - }; - // convert get result to set result - set_result = match before_result { - GetResult::Resource(before_response) => { - let GetResult::Resource(after_result) = after_result else { - return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) - }; - let diff = get_diff(&before_response.actual_state, &after_result.actual_state); - let mut before: Map = serde_json::from_value(before_response.actual_state)?; - // a `get` will return a `result` property, but an actual `set` will have that as `resources` - if before.contains_key("result") && !before.contains_key("resources") { - before.insert("resources".to_string(), before["result"].clone()); - before.remove("result"); - } - let before_value = serde_json::to_value(&before)?; - SetResult::Resource(ResourceSetResponse { - before_state: before_value.clone(), - after_state: after_result.actual_state, - changed_properties: Some(diff), - }) - }, - GetResult::Group(_) => { + }, + }; + start_datetime = chrono::Local::now(); + // returns a DeleteResult in WhatIf mode + delete_result = match dsc_resource.delete(&desired, &self.context.execution_type) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; + let after_result = match dsc_resource.get(&desired) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; + // convert get result to set result + set_result = match before_result { + GetResult::Resource(before_response) => { + let GetResult::Resource(after_result) = after_result else { return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) - }, - }; - end_datetime = chrono::Local::now(); - } + }; + let diff = get_diff(&before_response.actual_state, &after_result.actual_state); + let mut before: Map = serde_json::from_value(before_response.actual_state)?; + // a `get` will return a `result` property, but an actual `set` will have that as `resources` + if before.contains_key("result") && !before.contains_key("resources") { + before.insert("resources".to_string(), before["result"].clone()); + before.remove("result"); + } + let before_value = serde_json::to_value(&before)?; + SetResult::Resource(ResourceSetResponse { + before_state: before_value.clone(), + after_state: after_result.actual_state, + changed_properties: Some(diff), + }) + }, + GetResult::Group(_) => { + return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) + }, + }; + end_datetime = chrono::Local::now(); } else { return Err(DscError::NotImplemented(t!("configure.mod.deleteNotSupported", resource = resource.resource_type).to_string())); } + let mut other_metadata = Map::new(); + if self.context.execution_type == ExecutionKind::WhatIf { + if let Some(result) = delete_result { + if let Some(metadata) = result.metadata { + if let Some(what_if) = metadata.what_if { + other_metadata.insert("whatIf".to_string(), what_if); + } + } + } + } let mut metadata = Metadata { microsoft: Some( MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime) ), - other: Map::new(), + other: other_metadata, }; match &mut set_result { SetResult::Resource(resource_result) => { diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 0f8a08d5b..7603a6415 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}}; +use super::{dscresource::{get_diff, redact}, invoke_result::{DeleteResult, 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}; @@ -429,7 +429,7 @@ 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, target_resource: Option<&str>) -> Result<(), DscError> { +pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option<&str>, execution_type: &ExecutionKind) -> Result, DscError> { let Some(delete) = &resource.delete else { return Err(DscError::NotImplemented("delete".to_string())); }; @@ -440,14 +440,19 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, targ Some(r) => r, None => &resource.resource_type, }; - let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, &ExecutionKind::Actual); + let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); - 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(()) + 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())?; + let result = if execution_type == &ExecutionKind::WhatIf { + let delete_result: DeleteResult = serde_json::from_str(&stdout)?; + Some(delete_result) + } else { + None + }; + Ok(result) } /// Invoke the validate operation against a command resource. diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index e5b1fcb37..b8ec0687f 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -21,7 +21,7 @@ use super::{ command_resource, dscerror, invoke_result::{ - ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult + DeleteResult, ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult }, resource_manifest::{ import_manifest, ResourceManifest @@ -232,19 +232,19 @@ impl DscResource { Ok(test_result) } - fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str) -> Result<(), DscError> { + fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str, execution_type: &ExecutionKind) -> Result, DscError> { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; 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, execution_type); } return Err(DscError::NotSupported(t!("dscresources.dscresource.adapterDoesNotSupportDelete", adapter = adapter.type_name).to_string())); } configurator.invoke_set(false)?; - Ok(()) + Ok(None) } fn invoke_export_with_adapter(&self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { @@ -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,10 +469,10 @@ 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); + return self.invoke_delete_with_adapter(adapter, &self.type_name, filter, execution_type); } match &self.implemented_as { @@ -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, self.target_resource.as_deref(), execution_type) }, } } diff --git a/lib/dsc-lib/src/dscresources/invoke_result.rs b/lib/dsc-lib/src/dscresources/invoke_result.rs index c1170c781..83e959016 100644 --- a/lib/dsc-lib/src/dscresources/invoke_result.rs +++ b/lib/dsc-lib/src/dscresources/invoke_result.rs @@ -159,3 +159,20 @@ pub struct ResolveResult { /// The optional resolved parameters. pub parameters: Option>, } + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(deny_unknown_fields)] +#[dsc_repo_schema(base_name = "delete", folder_path = "outputs/resource")] +pub struct DeleteResult { + /// The return from the resource by the Delete method with what-if simulation. + #[serde(rename = "_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[dsc_repo_schema(base_name = "delete", folder_path = "outputs/resource")] +#[serde(deny_unknown_fields)] +pub struct DeleteWhatIfResult { + #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] + pub what_if: Option +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 6bc62da6e..f76898b19 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -20,6 +20,7 @@ pub enum Schemas { Trace, Version, WhatIf, + WhatIfDelete } #[derive(Debug, Parser)] @@ -140,5 +141,11 @@ 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-delete", about = "Check if it is a whatif delete operation")] + WhatIfDelete { + #[clap(name = "whatif", short, long, help = "Run as a whatif executionType instead of actual executionType")] + what_if: bool, } } diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 0c9f5107e..c4cb33eb6 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -17,6 +17,7 @@ mod sleep; mod trace; mod version; mod whatif; +mod whatif_delete; use args::{Args, Schemas, SubCommand}; use clap::Parser; @@ -36,6 +37,7 @@ use crate::sleep::Sleep; use crate::trace::Trace; use crate::version::Version; use crate::whatif::WhatIf; +use crate::whatif_delete::WhatIfDelete; use std::{thread, time::Duration}; #[allow(clippy::too_many_lines)] @@ -223,7 +225,6 @@ fn main() { }; metadata.name = Some(format!("Metadata example {}", i+1)); metadata.count = Some(i + 1); - println!("{}", serde_json::to_string(&metadata).unwrap()); } String::new() }, @@ -285,6 +286,9 @@ fn main() { Schemas::WhatIf => { schema_for!(WhatIf) }, + Schemas::WhatIfDelete => { + schema_for!(WhatIfDelete) + } }; serde_json::to_string(&schema).unwrap() }, @@ -324,6 +328,19 @@ fn main() { }; serde_json::to_string(&result).unwrap() }, + SubCommand::WhatIfDelete { what_if } => { + let result = if what_if { + let mut map = Map::::new(); + map.insert("whatIf".to_string(), serde_json::Value::Array(vec![ + serde_json::Value::String("Delete what-if message 1".to_string()), + serde_json::Value::String("Delete what-if message 2".to_string()), + ])); + WhatIfDelete { exist: None, metadata: Some(map) } + } else { + WhatIfDelete { exist: Some(false), metadata: None } + }; + serde_json::to_string(&result).unwrap() + } }; if !json.is_empty() { diff --git a/tools/dsctest/src/whatif_delete.rs b/tools/dsctest/src/whatif_delete.rs new file mode 100644 index 000000000..671d955a3 --- /dev/null +++ b/tools/dsctest/src/whatif_delete.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct WhatIfDelete { + #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] + pub exist: Option, + #[serde(rename="_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} From fbdbef775e48c6dbe7a9b6b12f70b982dec2dd56 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 16:09:35 -0500 Subject: [PATCH 4/7] update test --- dsc/tests/dsc_whatif.tests.ps1 | 1 - tools/dsctest/dsctest.dsc.manifests.json | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index b50ffe3e7..b6d5f86d4 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -142,7 +142,6 @@ Describe 'whatif tests' { - name: WhatIfDelete type: Test/WhatIfDelete properties: - executionType: Actual _exist: false "@ $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index a44f27c2b..a94d739c5 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -831,13 +831,13 @@ "get": { "executable": "dsctest", "args": [ - "whatif" + "whatif-delete" ] }, "delete": { "executable": "dsctest", "args": [ - "whatif", + "whatif-delete", { "whatIfArg": "-w" } @@ -849,7 +849,7 @@ "args": [ "schema", "-s", - "what-if" + "what-if-delete" ] } } From 472edd4ce8eaebba23537444cbaec2f27876e4f2 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 3 Feb 2026 17:04:27 -0500 Subject: [PATCH 5/7] add synthetic what-if delete support --- lib/dsc-lib/src/configure/mod.rs | 87 +++++++++++-------- .../src/dscresources/command_resource.rs | 28 ++++-- lib/dsc-lib/src/dscresources/dscresource.rs | 12 +-- lib/dsc-lib/src/dscresources/invoke_result.rs | 9 ++ 4 files changed, 87 insertions(+), 49 deletions(-) diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index f61b91db5..47816ec06 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -8,7 +8,7 @@ use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; use crate::dscresources::{ {dscresource::{Capability, Invoke, get_diff, validate_properties, get_adapter_input_kind}, - invoke_result::{DeleteResult, GetResult, SetResult, TestResult, ExportResult, ResourceSetResponse}}, + invoke_result::{DeleteResult, DeleteResultKind, GetResult, SetResult, TestResult, ExportResult, ResourceSetResponse}}, resource_manifest::{AdapterInputKind, Kind}, }; use crate::DscResource; @@ -506,7 +506,7 @@ impl Configurator { let start_datetime; let end_datetime; let mut set_result; - let mut delete_result: Option = None; + let mut delete_what_if_metadata: Option = None; if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) { debug!("{}", t!("configure.mod.handlesExist")); start_datetime = chrono::Local::now(); @@ -521,6 +521,7 @@ impl Configurator { end_datetime = chrono::Local::now(); } else if dsc_resource.capabilities.contains(&Capability::Delete) { debug!("{}", t!("configure.mod.implementsDelete")); + let before_result = match dsc_resource.get(&desired) { Ok(result) => result, Err(e) => { @@ -529,9 +530,9 @@ impl Configurator { return Err(e); }, }; + start_datetime = chrono::Local::now(); - // returns a DeleteResult in WhatIf mode - delete_result = match dsc_resource.delete(&desired, &self.context.execution_type) { + let delete_result = match dsc_resource.delete(&desired, &self.context.execution_type) { Ok(result) => result, Err(e) => { progress.set_failure(get_failure_from_error(&e)); @@ -539,53 +540,67 @@ impl Configurator { return Err(e); }, }; - let after_result = match dsc_resource.get(&desired) { - Ok(result) => result, - Err(e) => { - progress.set_failure(get_failure_from_error(&e)); - progress.write_increment(1); - return Err(e); + + match delete_result { + DeleteResultKind::SyntheticWhatIf(test_result) => { + end_datetime = chrono::Local::now(); + set_result = test_result.into(); }, - }; - // convert get result to set result - set_result = match before_result { - GetResult::Resource(before_response) => { - let GetResult::Resource(after_result) = after_result else { - return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) - }; - let diff = get_diff(&before_response.actual_state, &after_result.actual_state); - let mut before: Map = serde_json::from_value(before_response.actual_state)?; - // a `get` will return a `result` property, but an actual `set` will have that as `resources` - if before.contains_key("result") && !before.contains_key("resources") { - before.insert("resources".to_string(), before["result"].clone()); - before.remove("result"); + _ => { + if let DeleteResultKind::ResourceWhatIf(delete_res) = delete_result { + delete_what_if_metadata = Some(delete_res); } - let before_value = serde_json::to_value(&before)?; - SetResult::Resource(ResourceSetResponse { - before_state: before_value.clone(), - after_state: after_result.actual_state, - changed_properties: Some(diff), - }) - }, - GetResult::Group(_) => { - return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) + + let after_result = match dsc_resource.get(&desired) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; + end_datetime = chrono::Local::now(); + + set_result = match before_result { + GetResult::Resource(before_response) => { + let GetResult::Resource(after_result) = after_result else { + return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) + }; + let diff = get_diff(&before_response.actual_state, &after_result.actual_state); + let mut before: Map = serde_json::from_value(before_response.actual_state)?; + if before.contains_key("result") && !before.contains_key("resources") { + before.insert("resources".to_string(), before["result"].clone()); + before.remove("result"); + } + let before_value = serde_json::to_value(&before)?; + SetResult::Resource(ResourceSetResponse { + before_state: before_value.clone(), + after_state: after_result.actual_state, + changed_properties: Some(diff), + }) + }, + GetResult::Group(_) => { + return Err(DscError::NotSupported(t!("configure.mod.groupNotSupportedForDelete").to_string())) + }, + }; }, - }; - end_datetime = chrono::Local::now(); + } } else { return Err(DscError::NotImplemented(t!("configure.mod.deleteNotSupported", resource = resource.resource_type).to_string())); } + // Process metadata - only add whatIf if we have ResourceWhatIf variant let mut other_metadata = Map::new(); if self.context.execution_type == ExecutionKind::WhatIf { - if let Some(result) = delete_result { - if let Some(metadata) = result.metadata { + if let Some(delete_res) = delete_what_if_metadata { + if let Some(metadata) = delete_res.metadata { if let Some(what_if) = metadata.what_if { other_metadata.insert("whatIf".to_string(), what_if); } } } } + let mut metadata = Metadata { microsoft: Some( MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 7603a6415..a601de946 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -9,7 +9,17 @@ 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::{DeleteResult, ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{GetArgKind, SetDeleteArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; +use super::{ + dscresource::{get_diff, redact}, + invoke_result::{ + DeleteResult, DeleteResultKind, 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}; @@ -429,28 +439,32 @@ 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, target_resource: Option<&str>, execution_type: &ExecutionKind) -> Result, DscError> { +pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option, execution_type: &ExecutionKind) -> Result { let Some(delete) = &resource.delete else { return Err(DscError::NotImplemented("delete".to_string())); }; verify_json(resource, cwd, filter)?; - let resource_type = match target_resource { + let resource_type = match target_resource.as_deref() { Some(r) => r, None => &resource.resource_type, }; - let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); - + let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); + if execution_type == &ExecutionKind::WhatIf && !supports_whatif { + // we need to do a synthetic what-if here by calling test and returning a setResult + let test_result = invoke_test(resource, cwd, filter, target_resource.clone())?; + return Ok(DeleteResultKind::SyntheticWhatIf(test_result)); + } let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); 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())?; let result = if execution_type == &ExecutionKind::WhatIf { let delete_result: DeleteResult = serde_json::from_str(&stdout)?; - Some(delete_result) + DeleteResultKind::ResourceWhatIf(delete_result) } else { - None + DeleteResultKind::ResourceActual }; Ok(result) } diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index b8ec0687f..0a7079e2f 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -21,7 +21,7 @@ use super::{ command_resource, dscerror, invoke_result::{ - DeleteResult, ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult + DeleteResultKind, ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult }, resource_manifest::{ import_manifest, ResourceManifest @@ -232,7 +232,7 @@ impl DscResource { Ok(test_result) } - fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str, execution_type: &ExecutionKind) -> Result, DscError> { + fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str, execution_type: &ExecutionKind) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { @@ -244,7 +244,7 @@ impl DscResource { } configurator.invoke_set(false)?; - Ok(None) + Ok(DeleteResultKind::ResourceActual) } fn invoke_export_with_adapter(&self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { @@ -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, execution_type: &ExecutionKind) -> Result; /// 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, execution_type: &ExecutionKind) -> Result { 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, execution_type); @@ -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(), execution_type) + command_resource::invoke_delete(&resource_manifest, &self.directory, filter, self.target_resource.clone(), execution_type) }, } } diff --git a/lib/dsc-lib/src/dscresources/invoke_result.rs b/lib/dsc-lib/src/dscresources/invoke_result.rs index 83e959016..c42ab841e 100644 --- a/lib/dsc-lib/src/dscresources/invoke_result.rs +++ b/lib/dsc-lib/src/dscresources/invoke_result.rs @@ -176,3 +176,12 @@ pub struct DeleteWhatIfResult { #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] pub what_if: Option } + +pub enum DeleteResultKind { + /// Synthetic what-if created from test operation + SyntheticWhatIf(TestResult), + /// Native what-if result from resource + ResourceWhatIf(DeleteResult), + /// Actual delete from resource has no output + ResourceActual +} \ No newline at end of file From 7a0c8101e200eb8cad5016a515ed1e3a78407dcf Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 4 Feb 2026 10:31:59 -0500 Subject: [PATCH 6/7] add synthetic what-if test --- dsc/tests/dsc_whatif.tests.ps1 | 36 ++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index b6d5f86d4..73dccbdf6 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -135,7 +135,7 @@ Describe 'whatif tests' { $LASTEXITCODE | Should -Be 0 } - It 'Test/WhatIfDelete resource with set operation and WhatIfArgKind works' { + It 'Test/WhatIfDelete resource and WhatIfArgKind works' { $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: @@ -145,12 +145,36 @@ Describe 'whatif tests' { _exist: false "@ $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' + $LASTEXITCODE | Should -Be 0 $what_if_result.hadErrors | Should -BeFalse + $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' + $what_if_result.results[0].metadata.whatIf[0] | Should -BeExactly 'Delete what-if message 1' + $what_if_result.results[0].metadata.whatIf[1] | Should -BeExactly 'Delete what-if message 2' + $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].metadata.whatIf | Should -BeNullOrEmpty + } + + It 'Synthetic what-if for delete resource works' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Delete + type: Test/Delete + properties: + _exist: false +"@ + $out = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.hadErrors | Should -BeFalse + $out.results.Count | Should -Be 1 + $out.results[0].type | Should -BeExactly 'Test/Delete' + $out.results[0].result.beforeState.deleteCalled | Should -BeTrue + $out.results[0].result.beforeState._exist | Should -BeFalse + $out.results[0].result.afterState.deleteCalled | Should -BeNullOrEmpty + $out.results[0].result.afterState._exist | Should -BeFalse + $out.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' } } From 882143ad337d0ed8b9386daeb8702dddb980eb55 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 4 Feb 2026 14:40:30 -0500 Subject: [PATCH 7/7] address Copilot feedback --- lib/dsc-lib/src/dscresources/command_resource.rs | 2 +- lib/dsc-lib/src/dscresources/invoke_result.rs | 2 +- tools/dsctest/src/main.rs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 3ee86eeea..6f2236e32 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -452,7 +452,7 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, targ }; let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); if execution_type == &ExecutionKind::WhatIf && !supports_whatif { - // we need to do a synthetic what-if here by calling test and returning a setResult + // perform a synthetic what-if by calling test and wrapping the TestResult in DeleteResultKind::SyntheticWhatIf let test_result = invoke_test(resource, cwd, filter, target_resource.clone())?; return Ok(DeleteResultKind::SyntheticWhatIf(test_result)); } diff --git a/lib/dsc-lib/src/dscresources/invoke_result.rs b/lib/dsc-lib/src/dscresources/invoke_result.rs index c42ab841e..ace548892 100644 --- a/lib/dsc-lib/src/dscresources/invoke_result.rs +++ b/lib/dsc-lib/src/dscresources/invoke_result.rs @@ -184,4 +184,4 @@ pub enum DeleteResultKind { ResourceWhatIf(DeleteResult), /// Actual delete from resource has no output ResourceActual -} \ No newline at end of file +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index c4cb33eb6..21dfcad80 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -225,6 +225,7 @@ fn main() { }; metadata.name = Some(format!("Metadata example {}", i+1)); metadata.count = Some(i + 1); + println!("{}", serde_json::to_string(&metadata).unwrap()); } String::new() },