Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dsc-bicep-ext/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions dsc/src/mcp/invoke_dsc_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
}
},
Expand Down
6 changes: 3 additions & 3 deletions dsc/src/resource_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
43 changes: 43 additions & 0 deletions dsc/tests/dsc_whatif.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
132 changes: 74 additions & 58 deletions lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -506,6 +506,7 @@ impl Configurator {
let start_datetime;
let end_datetime;
let mut set_result;
let mut delete_what_if_metadata: Option<DeleteResult> = None;
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
debug!("{}", t!("configure.mod.handlesExist"));
start_datetime = chrono::Local::now();
Expand All @@ -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<String, Value> = 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<String, Value> = 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) => {
Expand Down
35 changes: 27 additions & 8 deletions lib/dsc-lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<FullyQualifiedTypeName>, execution_type: &ExecutionKind) -> Result<DeleteResultKind, DscError> {
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.
Expand Down
16 changes: 8 additions & 8 deletions lib/dsc-lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<DeleteResultKind, 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(DeleteResultKind::ResourceActual)
}

fn invoke_export_with_adapter(&self, adapter: &FullyQualifiedTypeName, input: &str) -> Result<ExportResult, DscError> {
Expand Down Expand Up @@ -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<DeleteResultKind, DscError>;

/// Invoke the validate operation on the resource.
///
Expand Down Expand Up @@ -469,10 +469,10 @@ impl Invoke for DscResource {
}
}

fn delete(&self, filter: &str) -> Result<(), DscError> {
fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result<DeleteResultKind, 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 {
Expand All @@ -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)
},
}
}
Expand Down
Loading