Skip to content

Commit b44699e

Browse files
authored
Merge pull request #382 from SteveL-MSFT/delete-exist
Update engine to call `delete` for resources that don't support `_exist` directly
2 parents 4d89c21 + 9fbcb1c commit b44699e

File tree

10 files changed

+249
-21
lines changed

10 files changed

+249
-21
lines changed

dsc/tests/dsc_config_set.tests.ps1

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'dsc config set tests' {
5+
It 'can use _exist with resources that support and do not support it' {
6+
$config_yaml = @"
7+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
8+
resources:
9+
- name: Exist
10+
type: Test/Exist
11+
properties:
12+
_exist: false
13+
- name: Delete
14+
type: Test/Delete
15+
properties:
16+
_exist: false
17+
"@
18+
$out = $config_yaml | dsc config set | ConvertFrom-Json
19+
$LASTEXITCODE | Should -Be 0
20+
$out.results[0].type | Should -BeExactly 'Test/Exist'
21+
$out.results[0].result.beforeState._exist | Should -BeFalse
22+
$out.results[0].result.afterState.state | Should -BeExactly 'Absent'
23+
$out.results[0].result.afterState._exist | Should -BeFalse
24+
$out.results[1].type | Should -BeExactly 'Test/Delete'
25+
$out.results[1].result.beforeState.deleteCalled | Should -BeTrue
26+
$out.results[1].result.beforeState._exist | Should -BeFalse
27+
$out.results[1].result.afterState.deleteCalled | Should -BeTrue
28+
$out.results[1].result.afterState._exist | Should -BeFalse
29+
}
30+
}

dsc_lib/src/configure/mod.rs

+62-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
use crate::configure::parameters::Input;
55
use crate::dscerror::DscError;
6-
use crate::dscresources::dscresource::Invoke;
6+
use crate::dscresources::dscresource::get_diff;
7+
use crate::dscresources::invoke_result::GetResult;
8+
use crate::dscresources::{dscresource::{Capability, Invoke}, invoke_result::{SetResult, ResourceSetResponse}};
79
use crate::dscresources::resource_manifest::Kind;
810
use crate::DscResource;
911
use crate::discovery::Discovery;
@@ -278,16 +280,67 @@ impl Configurator {
278280
return Err(DscError::ResourceNotFound(resource.resource_type));
279281
};
280282
debug!("resource_type {}", &resource.resource_type);
283+
284+
// see if the properties contains `_exist` and is false
285+
let exist = match &properties {
286+
Some(property_map) => {
287+
if let Some(exist) = property_map.get("_exist") {
288+
!matches!(exist, Value::Bool(false))
289+
} else {
290+
true
291+
}
292+
},
293+
_ => {
294+
true
295+
}
296+
};
297+
281298
let desired = add_metadata(&dsc_resource.kind, properties)?;
282299
trace!("desired: {desired}");
283-
let set_result = dsc_resource.set(&desired, skip_test)?;
284-
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
285-
let resource_result = config_result::ResourceSetResult {
286-
name: resource.name.clone(),
287-
resource_type: resource.resource_type.clone(),
288-
result: set_result,
289-
};
290-
result.results.push(resource_result);
300+
301+
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
302+
debug!("Resource handles _exist or _exist is true");
303+
let set_result = dsc_resource.set(&desired, skip_test)?;
304+
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
305+
let resource_result = config_result::ResourceSetResult {
306+
name: resource.name.clone(),
307+
resource_type: resource.resource_type.clone(),
308+
result: set_result,
309+
};
310+
result.results.push(resource_result);
311+
} else if dsc_resource.capabilities.contains(&Capability::Delete) {
312+
debug!("Resource implements delete and _exist is false");
313+
let before_result = dsc_resource.get(&desired)?;
314+
dsc_resource.delete(&desired)?;
315+
let after_result = dsc_resource.get(&desired)?;
316+
// convert get result to set result
317+
let set_result = match before_result {
318+
GetResult::Resource(before_response) => {
319+
let GetResult::Resource(after_result) = after_result else {
320+
return Err(DscError::NotSupported("Group resources not supported for delete".to_string()))
321+
};
322+
let before_value = serde_json::to_value(&before_response.actual_state)?;
323+
let after_value = serde_json::to_value(&after_result.actual_state)?;
324+
ResourceSetResponse {
325+
before_state: before_response.actual_state,
326+
after_state: after_result.actual_state,
327+
changed_properties: Some(get_diff(&before_value, &after_value)),
328+
}
329+
},
330+
GetResult::Group(_) => {
331+
return Err(DscError::NotSupported("Group resources not supported for delete".to_string()));
332+
},
333+
};
334+
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
335+
let resource_result = config_result::ResourceSetResult {
336+
name: resource.name.clone(),
337+
resource_type: resource.resource_type.clone(),
338+
result: SetResult::Resource(set_result),
339+
};
340+
result.results.push(resource_result);
341+
} else {
342+
return Err(DscError::NotImplemented(format!("Resource '{}' does not support `delete` and does not handle `_exist` as false", resource.resource_type)));
343+
}
291344
}
292345

293346
mem::drop(pb_span_enter);

dsc_lib/src/dscerror.rs

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ pub enum DscError {
7171
#[error("Not implemented: {0}")]
7272
NotImplemented(String),
7373

74+
#[error("Not supported: {0}")]
75+
NotSupported(String),
76+
7477
#[error("Number conversion error: {0}")]
7578
NumberConversion(#[from] std::num::TryFromIntError),
7679

dsc_lib/src/dscresources/command_resource.rs

+30-5
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
120120
verify_json(resource, cwd, desired)?;
121121

122122
// if resource doesn't implement a pre-test, we execute test first to see if a set is needed
123-
if !skip_test && !set.pre_test.unwrap_or_default() {
123+
if !skip_test && set.pre_test != Some(true) {
124124
info!("No pretest, invoking test {}", &resource.resource_type);
125125
let (in_desired_state, actual_state) = match invoke_test(resource, cwd, desired)? {
126126
TestResult::Group(group_response) => {
@@ -282,7 +282,8 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
282282
/// Error is returned if the underlying command returns a non-zero exit code.
283283
pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result<TestResult, DscError> {
284284
let Some(test) = &resource.test else {
285-
return Err(DscError::NotImplemented("test".to_string()));
285+
info!("Resource '{}' does not implement test, performing synthetic test", &resource.resource_type);
286+
return invoke_synthetic_test(resource, cwd, expected);
286287
};
287288

288289
verify_json(resource, cwd, expected)?;
@@ -375,6 +376,30 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
375376
}
376377
}
377378

379+
fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result<TestResult, DscError> {
380+
let get_result = invoke_get(resource, cwd, expected)?;
381+
let actual_state = match get_result {
382+
GetResult::Group(results) => {
383+
let mut result_array: Vec<Value> = Vec::new();
384+
for result in results {
385+
result_array.push(serde_json::to_value(&result)?);
386+
}
387+
Value::from(result_array)
388+
},
389+
GetResult::Resource(response) => {
390+
response.actual_state
391+
}
392+
};
393+
let expected_value: Value = serde_json::from_str(expected)?;
394+
let diff_properties = get_diff(&expected_value, &actual_state);
395+
Ok(TestResult::Resource(ResourceTestResponse {
396+
desired_state: expected_value,
397+
actual_state,
398+
in_desired_state: diff_properties.is_empty(),
399+
diff_properties,
400+
}))
401+
}
402+
378403
/// Invoke the delete operation against a command resource.
379404
///
380405
/// # Arguments
@@ -393,7 +418,7 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re
393418

394419
let mut env: Option<HashMap<String, String>> = None;
395420
let mut input_filter: Option<&str> = None;
396-
let mut get_args = resource.get.args.clone();
421+
let mut delete_args = delete.args.clone();
397422
verify_json(resource, cwd, filter)?;
398423
match &delete.input {
399424
InputKind::Env => {
@@ -403,12 +428,12 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re
403428
input_filter = Some(filter);
404429
},
405430
InputKind::Arg(arg_name) => {
406-
replace_token(&mut get_args, arg_name, filter)?;
431+
replace_token(&mut delete_args, arg_name, filter)?;
407432
},
408433
}
409434

410435
info!("Invoking delete '{}' using '{}'", &resource.resource_type, &delete.executable);
411-
let (exit_code, _stdout, stderr) = invoke_command(&delete.executable, get_args, input_filter, Some(cwd), env)?;
436+
let (exit_code, _stdout, stderr) = invoke_command(&delete.executable, delete_args, input_filter, Some(cwd), env)?;
412437
log_resource_traces(&stderr);
413438
if exit_code != 0 {
414439
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));

registry/registry.dsc.resource.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"executable": "registry",
3535
"args": [
3636
"config",
37-
"remove",
37+
"delete",
3838
"--input",
3939
"{json}"
4040
],

registry/tests/registry.config.set.tests.ps1

+43
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
# Licensed under the MIT License.
33

44
Describe 'registry config set tests' {
5+
AfterEach {
6+
if ($IsWindows) {
7+
Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore
8+
}
9+
}
10+
511
It 'Can set a deeply nested key and value' -Skip:(!$IsWindows) {
612
$json = @'
713
{
@@ -29,4 +35,41 @@ Describe 'registry config set tests' {
2935
$result.valueData.String | Should -Be 'World'
3036
($result.psobject.properties | Measure-Object).Count | Should -Be 3
3137
}
38+
39+
It 'delete called when _exist is false' -Skip:(!$IsWindows) {
40+
$config = @{
41+
'$schema' = 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json'
42+
resources = @(
43+
@{
44+
name = 'reg'
45+
type = 'Microsoft.Windows/Registry'
46+
properties = @{
47+
keyPath = 'HKCU\1\2'
48+
valueName = 'Test'
49+
valueData = @{
50+
String = 'Test'
51+
}
52+
_exist = $true
53+
}
54+
}
55+
)
56+
}
57+
58+
$out = dsc config set -d ($config | ConvertTo-Json -Depth 10)
59+
$LASTEXITCODE | Should -Be 0
60+
61+
$config.resources[0].properties._exist = $false
62+
$out = dsc config set -d ($config | ConvertTo-Json -Depth 10) | ConvertFrom-Json
63+
$LASTEXITCODE | Should -Be 0
64+
$out.results[0].result.afterState._exist | Should -Be $false
65+
66+
Get-ItemProperty -Path 'HKCU:\1\2' -Name 'Test' -ErrorAction Ignore | Should -BeNullOrEmpty
67+
68+
$config.resources[0].properties.valueName = $null
69+
$out = dsc config set -d ($config | ConvertTo-Json -Depth 10) | ConvertFrom-Json
70+
$LASTEXITCODE | Should -Be 0
71+
$out.results[0].result.afterState._exist | Should -Be $false
72+
73+
Get-Item -Path 'HKCU:\1\2' -ErrorAction Ignore | Should -BeNullOrEmpty
74+
}
3275
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json",
3+
"type": "Test/Delete",
4+
"version": "0.1.0",
5+
"get": {
6+
"executable": "dsctest",
7+
"args": [
8+
"delete",
9+
"--input",
10+
"{json}"
11+
],
12+
"input": {
13+
"arg": "{json}"
14+
}
15+
},
16+
"delete": {
17+
"executable": "dsctest",
18+
"args": [
19+
"delete",
20+
"--input",
21+
"{json}"
22+
],
23+
"input": {
24+
"arg": "{json}"
25+
}
26+
},
27+
"schema": {
28+
"command": {
29+
"executable": "dsctest",
30+
"args": [
31+
"schema",
32+
"-s",
33+
"delete"
34+
]
35+
}
36+
}
37+
}

tools/dsctest/src/args.rs

+13-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use clap::{Parser, Subcommand, ValueEnum};
55

66
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
77
pub enum Schemas {
8+
Delete,
89
Echo,
910
Exist,
1011
Sleep,
@@ -20,12 +21,24 @@ pub struct Args {
2021

2122
#[derive(Debug, PartialEq, Eq, Subcommand)]
2223
pub enum SubCommand {
24+
#[clap(name = "delete", about = "delete operation")]
25+
Delete {
26+
#[clap(name = "input", short, long, help = "The input to the delete command as JSON")]
27+
input: String,
28+
},
29+
2330
#[clap(name = "echo", about = "Return the input")]
2431
Echo {
2532
#[clap(name = "input", short, long, help = "The input to the echo command as JSON")]
2633
input: String,
2734
},
2835

36+
#[clap(name = "exist", about = "Check if a resource exists")]
37+
Exist {
38+
#[clap(name = "input", short, long, help = "The input to the exist command as JSON")]
39+
input: String,
40+
},
41+
2942
#[clap(name = "schema", about = "Get the JSON schema for a subcommand")]
3043
Schema {
3144
#[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")]
@@ -37,10 +50,4 @@ pub enum SubCommand {
3750
#[clap(name = "input", short, long, help = "The input to the sleep command as JSON")]
3851
input: String,
3952
},
40-
41-
#[clap(name = "exist", about = "Check if a resource exists")]
42-
Exist {
43-
#[clap(name = "input", short, long, help = "The input to the exist command as JSON")]
44-
input: String,
45-
},
4653
}

tools/dsctest/src/delete.rs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
8+
#[serde(deny_unknown_fields)]
9+
pub struct Delete {
10+
#[serde(rename = "deleteCalled", skip_serializing_if = "Option::is_none")]
11+
pub delete_called: Option<bool>,
12+
#[serde(rename = "_exist")]
13+
pub exist: bool,
14+
}

0 commit comments

Comments
 (0)