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/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 1e69302d7..ba838f8b6 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -138,4 +138,47 @@ Describe 'whatif tests' { $set_result.hadErrors | Should -BeFalse $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' } + + It 'Test/WhatIfDelete resource and WhatIfArgKind works' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: WhatIfDelete + type: Test/WhatIfDelete + properties: + _exist: false +"@ + $what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json + $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' + } } diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 0be52c23b..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::{GetResult, SetResult, TestResult, ExportResult, ResourceSetResponse}}, + invoke_result::{DeleteResult, DeleteResultKind, 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_what_if_metadata: Option = None; if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) { debug!("{}", t!("configure.mod.handlesExist")); start_datetime = chrono::Local::now(); @@ -520,76 +521,91 @@ 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 { + }, + }; + + start_datetime = chrono::Local::now(); + 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)); + progress.write_increment(1); + return Err(e); + }, + }; + + match delete_result { + DeleteResultKind::SyntheticWhatIf(test_result) => { + end_datetime = chrono::Local::now(); + set_result = test_result.into(); + }, + _ => { + if let DeleteResultKind::ResourceWhatIf(delete_res) = delete_result { + delete_what_if_metadata = Some(delete_res); + } + + 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())) - }; - 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())); } + // 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(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) ), - 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 c73868cbe..6f2236e32 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::{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,25 +439,34 @@ 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, 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, &ExecutionKind::Actual); - + let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, execution_type); + if execution_type == &ExecutionKind::WhatIf && !supports_whatif { + // 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)); + } 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)?; + DeleteResultKind::ResourceWhatIf(delete_result) + } else { + DeleteResultKind::ResourceActual + }; + 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..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::{ - ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult + DeleteResultKind, 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 { 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(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) -> Result<(), DscError>; + fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result; /// 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 { 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.clone(), execution_type) }, } } diff --git a/lib/dsc-lib/src/dscresources/invoke_result.rs b/lib/dsc-lib/src/dscresources/invoke_result.rs index c1170c781..ace548892 100644 --- a/lib/dsc-lib/src/dscresources/invoke_result.rs +++ b/lib/dsc-lib/src/dscresources/invoke_result.rs @@ -159,3 +159,29 @@ 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 +} + +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 +} diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 806e61aea..a94d739c5 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -823,6 +823,36 @@ ] } } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/WhatIfDelete", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "whatif-delete" + ] + }, + "delete": { + "executable": "dsctest", + "args": [ + "whatif-delete", + { + "whatIfArg": "-w" + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "what-if-delete" + ] + } + } } ] } 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 23bdf1fe9..21dfcad80 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)] @@ -285,6 +287,9 @@ fn main() { Schemas::WhatIf => { schema_for!(WhatIf) }, + Schemas::WhatIfDelete => { + schema_for!(WhatIfDelete) + } }; serde_json::to_string(&schema).unwrap() }, @@ -318,12 +323,25 @@ 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() }, + 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.rs b/tools/dsctest/src/whatif.rs index fd37bf1ee..06850d4b1 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: Option, } 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>, +}