diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index fc9aa427..9eb0a073 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -41,7 +41,9 @@ Describe 'whatif tests' { $what_if_result.results.result.beforeState._exist | Should -Be $set_result.results.result.beforeState._exist $what_if_result.results.result.beforeState.keyPath | Should -Be $set_result.results.result.beforeState.keyPath $what_if_result.results.result.afterState.KeyPath | Should -Be $set_result.results.result.afterState.keyPath - $what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties + # can be changed back to the following once _metadata is pulled out of resource return + # $what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties + $what_if_result.results.result.changedProperties | Should -Be @('_metadata', '_exist') $what_if_result.hadErrors | Should -BeFalse $what_if_result.results.Count | Should -Be 1 $LASTEXITCODE | Should -Be 0 diff --git a/registry/registry.dsc.resource.json b/registry/registry.dsc.resource.json index 40a19885..92f454dd 100644 --- a/registry/registry.dsc.resource.json +++ b/registry/registry.dsc.resource.json @@ -39,6 +39,19 @@ } ] }, + "whatIf": { + "executable": "registry", + "args": [ + "config", + "set", + "-w", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, "exitCodes": { "0": "Success", "1": "Invalid parameter", diff --git a/registry/src/args.rs b/registry/src/args.rs index 7c0302cf..a0521ec9 100644 --- a/registry/src/args.rs +++ b/registry/src/args.rs @@ -22,6 +22,8 @@ pub enum ConfigSubCommand { Set { #[clap(short, long, required = true, help = "The registry JSON input.")] input: String, + #[clap(short = 'w', long, help = "Run as a what-if operation instead of applying the registry configuration")] + what_if: bool, }, #[clap(name = "delete", about = "Delete registry configuration.")] Delete { diff --git a/registry/src/config.rs b/registry/src/config.rs index ee1833c7..a18fee2d 100644 --- a/registry/src/config.rs +++ b/registry/src/config.rs @@ -14,12 +14,15 @@ pub enum RegistryValueData { QWord(u64), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(rename = "Registry", deny_unknown_fields)] pub struct Registry { /// The path to the registry key. #[serde(rename = "keyPath")] pub key_path: String, + /// The information from a config set --what-if operation. + #[serde(rename = "_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option, /// The name of the registry value. #[serde(rename = "valueName", skip_serializing_if = "Option::is_none")] pub value_name: Option, @@ -29,3 +32,10 @@ pub struct Registry { #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] pub exist: Option, } + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Metadata { + #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] + pub what_if: Option> +} diff --git a/registry/src/main.rs b/registry/src/main.rs index f0d6826d..989bf7c7 100644 --- a/registry/src/main.rs +++ b/registry/src/main.rs @@ -63,16 +63,24 @@ fn main() { } } }, - args::ConfigSubCommand::Set{input} => { - let reg_helper = match RegistryHelper::new(&input) { + args::ConfigSubCommand::Set{input, what_if} => { + let mut reg_helper = match RegistryHelper::new(&input) { Ok(reg_helper) => reg_helper, Err(err) => { eprintln!("Error: {err}"); exit(EXIT_INVALID_INPUT); } }; + if what_if { + reg_helper.enable_what_if(); + } match reg_helper.set() { - Ok(()) => {}, + Ok(reg_config) => { + if let Some(config) = reg_config { + let json = serde_json::to_string(&config).unwrap(); + println!("{json}"); + } + }, Err(err) => { eprintln!("Error: {err}"); exit(EXIT_REGISTRY_ERROR); diff --git a/registry/src/registry_helper.rs b/registry/src/registry_helper.rs index 66b73b47..e7975a1b 100644 --- a/registry/src/registry_helper.rs +++ b/registry/src/registry_helper.rs @@ -3,13 +3,14 @@ use registry::{Data, Hive, RegKey, Security, key, value}; use utfx::{U16CString, UCString}; -use crate::config::{Registry, RegistryValueData}; +use crate::config::{Metadata, Registry, RegistryValueData}; use crate::error::RegistryError; pub struct RegistryHelper { config: Registry, hive: Hive, subkey: String, + what_if: bool, } impl RegistryHelper { @@ -26,10 +27,15 @@ impl RegistryHelper { config: registry, hive, subkey: subkey.to_string(), + what_if: false } ) } + pub fn enable_what_if(&mut self) { + self.what_if = true; + } + pub fn get(&self) -> Result { let exist: bool; let (reg_key, _subkey) = match self.open(Security::Read) { @@ -40,9 +46,8 @@ impl RegistryHelper { exist = false; return Ok(Registry { key_path: self.config.key_path.clone(), - value_name: None, - value_data: None, exist: Some(exist), + ..Default::default() }); }, Err(e) => return Err(e), @@ -56,8 +61,8 @@ impl RegistryHelper { return Ok(Registry { key_path: self.config.key_path.clone(), value_name: Some(value_name.clone()), - value_data: None, exist: Some(exist), + ..Default::default() }); }, Err(e) => return Err(RegistryError::RegistryValue(e)), @@ -67,21 +72,20 @@ impl RegistryHelper { key_path: self.config.key_path.clone(), value_name: Some(value_name.clone()), value_data: Some(convert_reg_value(&value)?), - exist: None, + ..Default::default() }) } else { Ok(Registry { key_path: self.config.key_path.clone(), - value_name: None, - value_data: None, - exist: None, + ..Default::default() }) } } - pub fn set(&self) -> Result<(), RegistryError> { + pub fn set(&self) -> Result, RegistryError> { + let mut what_if_metadata: Vec = Vec::new(); let reg_key = match self.open(Security::Write) { - Ok((reg_key, _subkey)) => reg_key, + Ok((reg_key, _subkey)) => Some(reg_key), // handle NotFound error Err(RegistryError::RegistryKeyNotFound(_)) => { // if the key doesn't exist, some of the parent keys may @@ -91,58 +95,89 @@ impl RegistryHelper { let mut reg_key = parent_key; for subkey in subkeys { let Ok(path) = UCString::::from_str(subkey) else { - return Err(RegistryError::Utf16Conversion("subkey".to_string())); + return self.handle_error_or_what_if(RegistryError::Utf16Conversion("subkey".to_string())); }; - reg_key = reg_key.create(path, Security::CreateSubKey)?; + if self.what_if { + what_if_metadata.push(format!("key: {subkey} not found, would create it")); + } + else { + reg_key = reg_key.create(path, Security::CreateSubKey)?; + } + } + if self.what_if { + None + } + else { + Some(self.open(Security::Write)?.0) } - - self.open(Security::Write)?.0 }, - Err(e) => return Err(e), + Err(e) => return self.handle_error_or_what_if(e) }; if let Some(value_data) = &self.config.value_data { let Ok(value_name) = U16CString::from_str(self.config.value_name.as_ref().unwrap()) else { - return Err(RegistryError::Utf16Conversion("valueName".to_string())); + return self.handle_error_or_what_if(RegistryError::Utf16Conversion("valueName".to_string())); }; - match value_data { + let data = match value_data { RegistryValueData::String(s) => { let Ok(utf16) = U16CString::from_str(s) else { - return Err(RegistryError::Utf16Conversion("valueData".to_string())); + return self.handle_error_or_what_if(RegistryError::Utf16Conversion("valueData".to_string())); }; - reg_key.set_value(&value_name, &Data::String(utf16))?; + Data::String(utf16) }, RegistryValueData::ExpandString(s) => { let Ok(utf16) = U16CString::from_str(s) else { - return Err(RegistryError::Utf16Conversion("valueData".to_string())); + return self.handle_error_or_what_if(RegistryError::Utf16Conversion("valueData".to_string())); }; - reg_key.set_value(&value_name, &Data::ExpandString(utf16))?; + Data::ExpandString(utf16) }, RegistryValueData::Binary(b) => { - reg_key.set_value(&value_name, &Data::Binary(b.clone()))?; + Data::Binary(b.clone()) }, RegistryValueData::DWord(d) => { - reg_key.set_value(&value_name, &Data::U32(*d))?; + Data::U32(*d) }, RegistryValueData::MultiString(m) => { let mut m16: Vec> = Vec::>::new(); for s in m { let Ok(utf16) = U16CString::from_str(s) else { - return Err(RegistryError::Utf16Conversion("valueData".to_string())); + return self.handle_error_or_what_if(RegistryError::Utf16Conversion("valueData".to_string())); }; m16.push(utf16); } - reg_key.set_value(&value_name, &Data::MultiString(m16))?; + Data::MultiString(m16) }, RegistryValueData::QWord(q) => { - reg_key.set_value(&value_name, &Data::U64(*q))?; + Data::U64(*q) }, + }; + + if self.what_if { + return Ok(Some(Registry { + key_path: self.config.key_path.clone(), + value_data: Some(convert_reg_value(&data)?), + value_name: self.config.value_name.clone(), + metadata: if what_if_metadata.is_empty() { None } else { Some(Metadata { what_if: Some(what_if_metadata) })}, + ..Default::default() + })); } + + if let Some(reg_key) = reg_key { + reg_key.set_value(&value_name, &data)?; + }; } - Ok(()) + if self.what_if { + return Ok(Some(Registry { + key_path: self.config.key_path.clone(), + metadata: if what_if_metadata.is_empty() { None } else { Some(Metadata { what_if: Some(what_if_metadata) })}, + ..Default::default() + })); + } + + Ok(None) } pub fn remove(&self) -> Result<(), RegistryError> { @@ -215,6 +250,17 @@ impl RegistryHelper { Ok((parent_key, subkeys)) } + + fn handle_error_or_what_if(&self, error: RegistryError) -> Result, RegistryError> { + if self.what_if { + return Ok(Some(Registry { + key_path: self.config.key_path.clone(), + metadata: Some(Metadata { what_if: Some(vec![error.to_string()]) }), + ..Default::default() + })); + } + Err(error) + } } fn get_hive_from_path(path: &str) -> Result<(Hive, &str), RegistryError> { diff --git a/registry/tests/registry.config.whatif.tests.ps1 b/registry/tests/registry.config.whatif.tests.ps1 new file mode 100644 index 00000000..86a1a452 --- /dev/null +++ b/registry/tests/registry.config.whatif.tests.ps1 @@ -0,0 +1,134 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'registry config whatif tests' { + BeforeAll { + Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore + } + + AfterEach { + Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore + } + + It 'Can whatif a new deeply nested key' -Skip:(!$IsWindows) { + $json = @' + { + "keyPath": "HKCU\\1\\2\\3" + } +'@ + registry config set -w --input $json | Write-Host + $get_before = registry config get --input $json + $result = registry config set -w --input $json | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $result.keyPath | Should -Be 'HKCU\1\2\3' + $result._metadata.whatIf[0] | Should -Match '.*1.*' + $result._metadata.whatIf[1] | Should -Match '.*2.*' + $result._metadata.whatIf[2] | Should -Match '.*3.*' + $get_after = registry config get --input $json + $get_before | Should -EQ $get_after + } + + It 'Can whatif a new deeply nested key and value' -Skip:(!$IsWindows) { + $json = @' + { + "keyPath": "HKCU\\1\\2\\3", + "valueName": "Hello", + "valueData": { + "String": "World" + } + } +'@ + $result = registry config set -w --input $json | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $result.keyPath | Should -Be 'HKCU\1\2\3' + $result.valueName | Should -Be 'Hello' + $result.valueData.String | Should -Be 'World' + $result._metadata.whatIf[0] | Should -Match '.*1.*' + $result._metadata.whatIf[1] | Should -Match '.*2.*' + $result._metadata.whatIf[2] | Should -Match '.*3.*' + } + + It 'Can whatif an existing key with new value' -Skip:(!$IsWindows) { + $set_json = @' + { + "keyPath": "HKCU\\1\\2" + } +'@ + registry config set --input $set_json + $whatif_json = @' + { + "keyPath": "HKCU\\1\\2", + "valueName": "Hello", + "valueData": { + "String": "World" + } + } +'@ + $result = registry config set -w --input $whatif_json | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $result.keyPath | Should -Be 'HKCU\1\2' + $result.valueName | Should -Be 'Hello' + $result.valueData.String | Should -Be 'World' + ($result.psobject.properties | Measure-Object).Count | Should -Be 3 + } + + It 'Can whatif an existing deeply nested key and value' -Skip:(!$IsWindows) { + $set_json = @' + { + "keyPath": "HKCU\\1\\2\\3", + "valueName": "Hello", + "valueData": { + "String": "World" + } + } +'@ + registry config set --input $set_json + $whatif_json = @' + { + "keyPath": "HKCU\\1\\2\\3", + "valueName": "Hello", + "valueData": { + "String": "World-WhatIf" + } + } +'@ + $result = registry config set -w --input $whatif_json | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $result.keyPath | Should -Be 'HKCU\1\2\3' + $result.valueName | Should -Be 'Hello' + $result.valueData.String | Should -Be 'World-WhatIf' + ($result.psobject.properties | Measure-Object).Count | Should -Be 3 + } + + It 'Can whatif an existing key with nested values' -Skip:(!$IsWindows) { + $set_json = @' + { + "keyPath": "HKCU\\1\\2\\3", + "valueName": "Hello", + "valueData": { + "String": "World" + } + } +'@ + registry config set --input $set_json + $set_json = @' + { + "keyPath": "HKCU\\1\\2\\3", + "valueName": "Foo", + "valueData": { + "String": "Bar" + } + } +'@ + registry config set --input $set_json + $whatif_json = @' + { + "keyPath": "HKCU\\1\\2" + } +'@ + $result = registry config set -w --input $whatif_json | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $result.keyPath | Should -Be 'HKCU\1\2' + ($result.psobject.properties | Measure-Object).Count | Should -Be 1 + } +}