Skip to content

Commit 3c0ce24

Browse files
committed
add initial what-if functionality to registry resource
1 parent 5b9ce43 commit 3c0ce24

File tree

7 files changed

+134
-35
lines changed

7 files changed

+134
-35
lines changed

.vscode/launch.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
"type": "lldb",
99
"request": "launch",
1010
"name": "Debug config",
11-
"program": "${workspaceFolder}/config/target/debug/config",
11+
"program": "C:\\dsc\\dsc\\target\\debug\\dsc.exe",
1212
"args": [
13-
"list",
14-
"r*"
13+
"config",
14+
"set",
15+
"-w",
16+
"-d",
17+
"$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json\nresources:\n- name: Registry\n type: Microsoft.Windows/Registry\n properties:\n keyPath: 'HKCU\\1\\2'"
1518
],
16-
"cwd": "${workspaceFolder}"
19+
"cwd": "C:\\dsc"
1720
},
1821
{
1922
"name": "(macOS) Attach",

dsc/tests/dsc_whatif.tests.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ Describe 'whatif tests' {
3838
$what_if_result = $config_yaml | dsc config set -w | ConvertFrom-Json
3939
$set_result = $config_yaml | dsc config set | ConvertFrom-Json
4040
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'WhatIf'
41-
$what_if_result.results.result.beforeState._exist | Should -Be $set_result.results.result.beforeState._exist
41+
$what_if_result.results.result.beforeState._exist | Should -Be $false
4242
$what_if_result.results.result.beforeState.keyPath | Should -Be $set_result.results.result.beforeState.keyPath
4343
$what_if_result.results.result.afterState.KeyPath | Should -Be $set_result.results.result.afterState.keyPath
44-
$what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties
44+
$what_if_result.results.result.changedProperties | Should -Be @('changeType', 'depth', '_exist')
4545
$what_if_result.hadErrors | Should -BeFalse
4646
$what_if_result.results.Count | Should -Be 1
4747
$LASTEXITCODE | Should -Be 0

registry/registry.dsc.resource.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@
3939
}
4040
]
4141
},
42+
"whatIf": {
43+
"executable": "registry",
44+
"args": [
45+
"config",
46+
"set",
47+
"-w",
48+
{
49+
"jsonInputArg": "--input",
50+
"mandatory": true
51+
}
52+
],
53+
"return": "state"
54+
},
4255
"exitCodes": {
4356
"0": "Success",
4457
"1": "Invalid parameter",

registry/src/args.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub enum ConfigSubCommand {
2222
Set {
2323
#[clap(short, long, required = true, help = "The registry JSON input.")]
2424
input: String,
25+
#[clap(short = 'w', long, help = "Run as a what-if operation instead of applying the registry configuration")]
26+
what_if: bool,
2527
},
2628
#[clap(name = "delete", about = "Delete registry configuration.")]
2729
Delete {

registry/src/config.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ pub enum RegistryValueData {
1414
QWord(u64),
1515
}
1616

17-
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
17+
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1818
#[serde(rename = "Registry", deny_unknown_fields)]
1919
pub struct Registry {
2020
/// The path to the registry key.
2121
#[serde(rename = "keyPath")]
2222
pub key_path: String,
23+
/// The information from a config set --what-if operation.
24+
#[serde(flatten, skip_serializing_if = "Option::is_none")]
25+
pub what_if: Option<WhatIf>,
2326
/// The name of the registry value.
2427
#[serde(rename = "valueName", skip_serializing_if = "Option::is_none")]
2528
pub value_name: Option<String>,
@@ -29,3 +32,27 @@ pub struct Registry {
2932
#[serde(rename = "_exist", skip_serializing_if = "Option::is_none")]
3033
pub exist: Option<bool>,
3134
}
35+
36+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
37+
#[serde(deny_unknown_fields)]
38+
pub struct WhatIf {
39+
#[serde(rename = "changeType")]
40+
pub change_type: Action,
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub depth: Option<usize>,
43+
#[serde(rename = "proposedValueData", skip_serializing_if = "Option::is_none")]
44+
pub proposed_data: Option<RegistryValueData>,
45+
#[serde(skip_serializing_if = "Option::is_none")]
46+
pub message: Option<String>
47+
}
48+
49+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
50+
#[serde(deny_unknown_fields)]
51+
pub enum Action {
52+
#[serde(rename = "clobber")]
53+
Clobber,
54+
#[serde(rename = "error")]
55+
Error,
56+
#[serde(rename = "new")]
57+
New
58+
}

registry/src/main.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,21 @@ fn main() {
6363
}
6464
}
6565
},
66-
args::ConfigSubCommand::Set{input} => {
66+
args::ConfigSubCommand::Set{input, what_if} => {
6767
let reg_helper = match RegistryHelper::new(&input) {
6868
Ok(reg_helper) => reg_helper,
6969
Err(err) => {
70-
eprintln!("Error: {err}");
70+
eprintln!("{err}");
7171
exit(EXIT_INVALID_INPUT);
7272
}
7373
};
74-
match reg_helper.set() {
75-
Ok(()) => {},
74+
match reg_helper.set(what_if) {
75+
Ok(reg_config) => {
76+
if let Some(config) = reg_config {
77+
let json = serde_json::to_string(&config).unwrap();
78+
println!("{json}");
79+
}
80+
},
7681
Err(err) => {
7782
eprintln!("Error: {err}");
7883
exit(EXIT_REGISTRY_ERROR);

registry/src/registry_helper.rs

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use registry::{Data, Hive, RegKey, Security, key, value};
55
use utfx::{U16CString, UCString};
6-
use crate::config::{Registry, RegistryValueData};
6+
use crate::config::{Action, Registry, RegistryValueData, WhatIf};
77
use crate::error::RegistryError;
88

99
pub struct RegistryHelper {
@@ -40,9 +40,8 @@ impl RegistryHelper {
4040
exist = false;
4141
return Ok(Registry {
4242
key_path: self.config.key_path.clone(),
43-
value_name: None,
44-
value_data: None,
4543
exist: Some(exist),
44+
..Default::default()
4645
});
4746
},
4847
Err(e) => return Err(e),
@@ -56,8 +55,8 @@ impl RegistryHelper {
5655
return Ok(Registry {
5756
key_path: self.config.key_path.clone(),
5857
value_name: Some(value_name.clone()),
59-
value_data: None,
6058
exist: Some(exist),
59+
..Default::default()
6160
});
6261
},
6362
Err(e) => return Err(RegistryError::RegistryValue(e)),
@@ -67,19 +66,18 @@ impl RegistryHelper {
6766
key_path: self.config.key_path.clone(),
6867
value_name: Some(value_name.clone()),
6968
value_data: Some(convert_reg_value(&value)?),
70-
exist: None,
69+
..Default::default()
7170
})
7271
} else {
7372
Ok(Registry {
7473
key_path: self.config.key_path.clone(),
75-
value_name: None,
76-
value_data: None,
77-
exist: None,
74+
..Default::default()
7875
})
7976
}
8077
}
8178

82-
pub fn set(&self) -> Result<(), RegistryError> {
79+
pub fn set(&self, is_what_if: bool) -> Result<Option<Registry>, RegistryError> {
80+
let mut depth = None;
8381
let reg_key = match self.open(Security::Write) {
8482
Ok((reg_key, _subkey)) => reg_key,
8583
// handle NotFound error
@@ -88,6 +86,7 @@ impl RegistryHelper {
8886
// not exist either, so we need to find the valid parent key
8987
// and then create the subkeys that don't exist
9088
let (parent_key, subkeys) = self.get_valid_parent_key_and_subkeys()?;
89+
depth = Some(subkeys.len());
9190
let mut reg_key = parent_key;
9291
for subkey in subkeys {
9392
let Ok(path) = UCString::<u16>::from_str(subkey) else {
@@ -99,50 +98,84 @@ impl RegistryHelper {
9998

10099
self.open(Security::Write)?.0
101100
},
102-
Err(e) => return Err(e),
101+
Err(e) => return self.handle_what_if_error(e, is_what_if)
103102
};
104103

105104
if let Some(value_data) = &self.config.value_data {
106105
let Ok(value_name) = U16CString::from_str(self.config.value_name.as_ref().unwrap()) else {
107-
return Err(RegistryError::Utf16Conversion("valueName".to_string()));
106+
return self.handle_what_if_error(RegistryError::Utf16Conversion("valueName".to_string()), is_what_if);
108107
};
109108

110-
match value_data {
109+
let data = match value_data {
111110
RegistryValueData::String(s) => {
112111
let Ok(utf16) = U16CString::from_str(s) else {
113-
return Err(RegistryError::Utf16Conversion("valueData".to_string()));
112+
return self.handle_what_if_error(RegistryError::Utf16Conversion("valueData".to_string()), is_what_if);
114113
};
115-
reg_key.set_value(&value_name, &Data::String(utf16))?;
114+
Data::String(utf16)
116115
},
117116
RegistryValueData::ExpandString(s) => {
118117
let Ok(utf16) = U16CString::from_str(s) else {
119-
return Err(RegistryError::Utf16Conversion("valueData".to_string()));
118+
return self.handle_what_if_error(RegistryError::Utf16Conversion("valueData".to_string()), is_what_if);
120119
};
121-
reg_key.set_value(&value_name, &Data::ExpandString(utf16))?;
120+
Data::ExpandString(utf16)
122121
},
123122
RegistryValueData::Binary(b) => {
124-
reg_key.set_value(&value_name, &Data::Binary(b.clone()))?;
123+
Data::Binary(b.clone())
125124
},
126125
RegistryValueData::DWord(d) => {
127-
reg_key.set_value(&value_name, &Data::U32(*d))?;
126+
Data::U32(*d)
128127
},
129128
RegistryValueData::MultiString(m) => {
130129
let mut m16: Vec<UCString<u16>> = Vec::<UCString<u16>>::new();
131130
for s in m {
132131
let Ok(utf16) = U16CString::from_str(s) else {
133-
return Err(RegistryError::Utf16Conversion("valueData".to_string()));
132+
return self.handle_what_if_error(RegistryError::Utf16Conversion("valueData".to_string()), is_what_if);
134133
};
135134
m16.push(utf16);
136135
}
137-
reg_key.set_value(&value_name, &Data::MultiString(m16))?;
136+
Data::MultiString(m16)
138137
},
139138
RegistryValueData::QWord(q) => {
140-
reg_key.set_value(&value_name, &Data::U64(*q))?;
139+
Data::U64(*q)
141140
},
141+
};
142+
143+
if is_what_if {
144+
let change_type = if let Some(name_exists) = self.get()?.exist {
145+
if name_exists { Action::Clobber }
146+
else { Action::New }
147+
} else { Action::Clobber };
148+
return Ok(Some(Registry {
149+
key_path: self.config.key_path.clone(),
150+
what_if: Some(WhatIf {
151+
change_type,
152+
proposed_data: Some(convert_reg_value(&data)?),
153+
depth,
154+
message: None
155+
}),
156+
..Default::default()
157+
}));
142158
}
159+
reg_key.set_value(&value_name, &data)?;
143160
}
144161

145-
Ok(())
162+
if is_what_if {
163+
let change_type = if let Some(name_exists) = self.get()?.exist {
164+
if name_exists { Action::Clobber }
165+
else { Action::New }
166+
} else { Action::Clobber };
167+
return Ok(Some(Registry {
168+
key_path: self.config.key_path.clone(),
169+
what_if: Some(WhatIf {
170+
change_type,
171+
proposed_data: None,
172+
depth,
173+
message: None
174+
}),
175+
..Default::default()
176+
}));
177+
}
178+
Ok(None)
146179
}
147180

148181
pub fn remove(&self) -> Result<(), RegistryError> {
@@ -215,6 +248,22 @@ impl RegistryHelper {
215248

216249
Ok((parent_key, subkeys))
217250
}
251+
252+
fn handle_what_if_error(&self, error: RegistryError, is_what_if: bool) -> Result<Option<Registry>, RegistryError> {
253+
if is_what_if {
254+
return Ok(Some(Registry {
255+
key_path: self.config.key_path.clone(),
256+
what_if: Some(WhatIf {
257+
change_type: Action::Error,
258+
depth: None,
259+
proposed_data: None,
260+
message: Some(error.to_string())
261+
}),
262+
..Default::default()
263+
}));
264+
}
265+
Err(error)
266+
}
218267
}
219268

220269
fn get_hive_from_path(path: &str) -> Result<(Hive, &str), RegistryError> {
@@ -313,7 +362,7 @@ fn get_nonexisting_value() {
313362
#[test]
314363
fn set_and_remove_test_value() {
315364
let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DSCTest\\DSCSubKey","valueName":"TestValue","valueData": { "String": "Hello"} }"#).unwrap();
316-
reg_helper.set().unwrap();
365+
reg_helper.set(false).unwrap();
317366
let result = reg_helper.get().unwrap();
318367
assert_eq!(result.key_path, r#"HKCU\DSCTest\DSCSubKey"#);
319368
assert_eq!(result.value_name, Some("TestValue".to_string()));
@@ -340,7 +389,7 @@ fn set_and_remove_test_value() {
340389
#[test]
341390
fn delete_tree() {
342391
let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DSCTest2\\DSCSubKey","valueName":"TestValue","valueData": { "String": "Hello"} }"#).unwrap();
343-
reg_helper.set().unwrap();
392+
reg_helper.set(false).unwrap();
344393
let result = reg_helper.get().unwrap();
345394
assert_eq!(result.key_path, r#"HKCU\DSCTest2\DSCSubKey"#);
346395
assert_eq!(result.value_name, Some("TestValue".to_string()));

0 commit comments

Comments
 (0)