From bbcd810fb2f220e80f57212979841ed53ce2c8f6 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 26 Oct 2023 13:51:36 -0700 Subject: [PATCH 01/15] add resourceId function --- dsc_lib/src/configure/depends_on.rs | 16 +++--- dsc_lib/src/dscerror.rs | 6 +-- dsc_lib/src/functions/mod.rs | 1 + dsc_lib/src/functions/resourceId.rs | 83 +++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 dsc_lib/src/functions/resourceId.rs diff --git a/dsc_lib/src/configure/depends_on.rs b/dsc_lib/src/configure/depends_on.rs index 41efc655..e8b5006b 100644 --- a/dsc_lib/src/configure/depends_on.rs +++ b/dsc_lib/src/configure/depends_on.rs @@ -1,28 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use regex::Regex; - use crate::configure::config_doc::Resource; use crate::configure::Configuration; use crate::DscError; /// Gets the invocation order of resources based on their dependencies -/// +/// /// # Arguments -/// +/// /// * `config` - The configuration to get the invocation order for -/// +/// /// # Returns -/// +/// /// * `Result, DscError>` - The invocation order of resources -/// +/// /// # Errors -/// +/// /// * `DscError::Validation` - The configuration is invalid pub fn get_resource_invocation_order(config: &Configuration) -> Result, DscError> { let mut order: Vec = Vec::new(); - let depends_on_regex = Regex::new(r"^\[resourceId\(\s*'(?[a-zA-Z0-9\.]+/[a-zA-Z0-9]+)'\s*,\s*'(?[a-zA-Z0-9 ]+)'\s*\)]$")?; for resource in &config.resources { // validate that the resource isn't specified more than once in the config if config.resources.iter().filter(|r| r.name == resource.name && r.resource_type == resource.resource_type).count() > 1 { @@ -32,6 +29,7 @@ pub fn get_resource_invocation_order(config: &Configuration) -> Result usize { + 2 + } + + fn max_args(&self) -> usize { + 2 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::String] + } + + fn invoke(&self, args: &[FunctionArg]) -> Result { + let mut result = String::new(); + // verify that the arguments do not contain a slash + for arg in args { + match arg { + FunctionArg::String(value) => { + if value.contains('/') { + return Err(DscError::Function("resourceId".to_string(), "Argument cannot contain a slash".to_string())); + } + + result.push_str(value); + }, + _ => { + return Err(DscError::Parser("Invalid argument type".to_string())); + } + } + result.push('/'); + } + Ok(FunctionResult::String(result)) + } +} + +#[cfg(test)] +mod tests { + use crate::parser::Statement; + + #[test] + fn strings() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[resourceId('a', 'b')]").unwrap(); + assert_eq!(result, "a/b"); + } + + #[test] + fn strings_with_dots() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[resourceId('a.b.c', 'd')]").unwrap(); + assert_eq!(result, "a.b.c/d"); + } + + #[test] + fn invalid_type() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[resourceId('a/b','c')]"); + assert!(result.is_err()); + } + + #[test] + fn invalid_name() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[resourceId('a','b/c')]"); + assert!(result.is_err()); + } + + #[test] + fn invalid_one_parameter() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[resourceId('a')]"); + assert!(result.is_err()); + } +} From 93d9f2a0c00dcf477d33959e84c7677c125b70fe Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 1 Nov 2023 20:19:43 -0700 Subject: [PATCH 02/15] connect dependsOn to new resourceId function using expressions --- dsc_lib/src/configure/config_doc.rs | 2 - dsc_lib/src/configure/depends_on.rs | 62 +++++++++++-------- dsc_lib/src/configure/mod.rs | 38 ++++++++++-- dsc_lib/src/functions/mod.rs | 3 +- .../{resourceId.rs => resource_id.rs} | 49 +++++++++------ 5 files changed, 103 insertions(+), 51 deletions(-) rename dsc_lib/src/functions/{resourceId.rs => resource_id.rs} (52%) diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index cbf1b513..a6b73508 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -66,8 +66,6 @@ pub struct Resource { /// The fully qualified name of the resource type #[serde(rename = "type")] pub resource_type: String, - // TODO: `apiVersion` is required by ARM but doesn't make sense here - /// A friendly name for the resource instance pub name: String, // friendly unique instance name #[serde(rename = "dependsOn", skip_serializing_if = "Option::is_none")] diff --git a/dsc_lib/src/configure/depends_on.rs b/dsc_lib/src/configure/depends_on.rs index e8b5006b..e85a6523 100644 --- a/dsc_lib/src/configure/depends_on.rs +++ b/dsc_lib/src/configure/depends_on.rs @@ -4,6 +4,7 @@ use crate::configure::config_doc::Resource; use crate::configure::Configuration; use crate::DscError; +use crate::parser::Statement; /// Gets the invocation order of resources based on their dependencies /// @@ -18,7 +19,7 @@ use crate::DscError; /// # Errors /// /// * `DscError::Validation` - The configuration is invalid -pub fn get_resource_invocation_order(config: &Configuration) -> Result, DscError> { +pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statement) -> Result, DscError> { let mut order: Vec = Vec::new(); for resource in &config.resources { // validate that the resource isn't specified more than once in the config @@ -29,13 +30,9 @@ pub fn get_resource_invocation_order(config: &Configuration) -> Result Result Result Result<(&str, &str), DscError> { + let parts: Vec<&str> = statement.split(':').collect(); + if parts.len() != 2 { + return Err(DscError::Validation(format!("'dependsOn' syntax is incorrect: {statement}"))); + } + Ok((parts[0], parts[1])) +} + #[cfg(test)] mod tests { + use crate::parser; + use super::*; #[test] @@ -103,7 +107,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config).unwrap(); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser).unwrap(); assert_eq!(order[0].name, "First"); assert_eq!(order[1].name, "Second"); } @@ -124,7 +129,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser); assert!(order.is_err()); } @@ -140,7 +146,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser); assert!(order.is_err()); } @@ -162,7 +169,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config).unwrap(); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser).unwrap(); assert_eq!(order[0].name, "First"); assert_eq!(order[1].name, "Second"); assert_eq!(order[2].name, "Third"); @@ -184,7 +192,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser); assert!(order.is_err()); } @@ -205,7 +214,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config).unwrap(); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser).unwrap(); assert_eq!(order[0].name, "First"); assert_eq!(order[1].name, "Second"); assert_eq!(order[2].name, "Third"); @@ -232,7 +242,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser); assert!(order.is_err()); } @@ -259,7 +270,8 @@ mod tests { "#; let config: Configuration = serde_yaml::from_str(config_yaml).unwrap(); - let order = get_resource_invocation_order(&config).unwrap(); + let mut parser = parser::Statement::new().unwrap(); + let order = get_resource_invocation_order(&config, &mut parser).unwrap(); assert_eq!(order[0].name, "First"); assert_eq!(order[1].name, "Second"); assert_eq!(order[2].name, "Third"); diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 27f0b412..11782b61 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -7,9 +7,11 @@ use crate::dscerror::DscError; use crate::dscresources::dscresource::Invoke; use crate::DscResource; use crate::discovery::Discovery; +use crate::parser::Statement; use self::config_doc::Configuration; use self::depends_on::get_resource_invocation_order; use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult, ResourceMessage, MessageLevel}; +use serde_json::Value; use std::collections::{HashMap, HashSet}; use tracing::debug; @@ -20,6 +22,7 @@ pub mod depends_on; pub struct Configurator { config: String, discovery: Discovery, + statement_parser: Statement, } #[derive(Debug, Clone, Copy)] @@ -70,6 +73,7 @@ impl Configurator { Ok(Configurator { config: config.to_owned(), discovery, + statement_parser: Statement::new()?, }) } @@ -91,12 +95,13 @@ impl Configurator { if had_errors { return Ok(result); } - for resource in get_resource_invocation_order(&config)? { + for resource in get_resource_invocation_order(&config, &mut self.statement_parser)? { let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); - let filter = serde_json::to_string(&resource.properties)?; + + let filter = serde_json::to_string(self.invoke_property_expressions(&resource.properties))?; let get_result = dsc_resource.get(&filter)?; let resource_result = config_result::ResourceGetResult { name: resource.name.clone(), @@ -127,7 +132,7 @@ impl Configurator { if had_errors { return Ok(result); } - for resource in get_resource_invocation_order(&config)? { + for resource in get_resource_invocation_order(&config, &mut self.statement_parser)? { let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; @@ -163,7 +168,7 @@ impl Configurator { if had_errors { return Ok(result); } - for resource in get_resource_invocation_order(&config)? { + for resource in get_resource_invocation_order(&config, &mut self.statement_parser)? { let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; @@ -253,7 +258,7 @@ impl Configurator { let config: Configuration = serde_json::from_str(self.config.as_str())?; let mut messages: Vec = Vec::new(); let mut has_errors = false; - + // Perform discovery of resources used in config let mut required_resources = config.resources.iter().map(|p| p.resource_type.to_lowercase()).collect::>(); required_resources.sort_unstable(); @@ -319,4 +324,27 @@ impl Configurator { Ok((config, messages, has_errors)) } + + fn invoke_property_expressions(&mut self, properties: &Option>) -> Result>, DscError> { + if properties.is_none() { + return Ok(None); + } + + let mut result: HashMap = HashMap::new(); + if let Some(properties) = properties { + for (name, value) in properties { + // if value is an object, we have to do it recursively + if let Value::Object(object) = value { + // convert serde map to hashmap + let object: HashMap = object.into_iter().collect(); + let value = self.invoke_property_expressions(Some(object))?; + result.insert(name, serde_json::to_value(value)?); + continue; + } + let value = self.statement_parser.parse_and_execute(&value.to_string())?; + result.insert(name, serde_json::from_str(&value)?); + } + } + Ok(Some(result)) + } } diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 138ab5c7..3324b250 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -9,7 +9,7 @@ use crate::parser::functions::{FunctionArg, FunctionResult}; pub mod base64; pub mod concat; -pub mod resourceId; +pub mod resource_id; /// The kind of argument that a function accepts. #[derive(Debug, PartialEq)] @@ -51,6 +51,7 @@ impl FunctionDispatcher { let mut functions: HashMap> = HashMap::new(); functions.insert("base64".to_string(), Box::new(base64::Base64{})); functions.insert("concat".to_string(), Box::new(concat::Concat{})); + functions.insert("resourceId".to_string(), Box::new(resource_id::ResourceId{})); Self { functions, } diff --git a/dsc_lib/src/functions/resourceId.rs b/dsc_lib/src/functions/resource_id.rs similarity index 52% rename from dsc_lib/src/functions/resourceId.rs rename to dsc_lib/src/functions/resource_id.rs index c81f197a..b686995c 100644 --- a/dsc_lib/src/functions/resourceId.rs +++ b/dsc_lib/src/functions/resource_id.rs @@ -22,22 +22,35 @@ impl Function for ResourceId { fn invoke(&self, args: &[FunctionArg]) -> Result { let mut result = String::new(); - // verify that the arguments do not contain a slash - for arg in args { - match arg { - FunctionArg::String(value) => { - if value.contains('/') { - return Err(DscError::Function("resourceId".to_string(), "Argument cannot contain a slash".to_string())); - } - - result.push_str(value); - }, - _ => { - return Err(DscError::Parser("Invalid argument type".to_string())); + // first argument is the type and must contain only 1 slash + match &args[0] { + FunctionArg::String(value) => { + let slash_count = value.chars().filter(|c| *c == '/').count(); + if slash_count != 1 { + return Err(DscError::Function("resourceId".to_string(), "Type argument must contain exactly one slash".to_string())); + } + result.push_str(value); + }, + _ => { + return Err(DscError::Parser("Invalid argument type".to_string())); + } + } + // ARM uses a slash separator, but here we use a colon which is not allowed for the type nor name + result.push(':'); + // second argument is the name and must contain no slashes + match &args[1] { + FunctionArg::String(value) => { + if value.contains('/') { + return Err(DscError::Function("resourceId".to_string(), "Name argument cannot contain a slash".to_string())); } + + result.push_str(value); + }, + _ => { + return Err(DscError::Parser("Invalid argument type".to_string())); } - result.push('/'); } + Ok(FunctionResult::String(result)) } } @@ -49,21 +62,21 @@ mod tests { #[test] fn strings() { let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[resourceId('a', 'b')]").unwrap(); - assert_eq!(result, "a/b"); + let result = parser.parse_and_execute("[resourceId('a/b', 'c')]").unwrap(); + assert_eq!(result, "a/b:c"); } #[test] fn strings_with_dots() { let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[resourceId('a.b.c', 'd')]").unwrap(); - assert_eq!(result, "a.b.c/d"); + let result = parser.parse_and_execute("[resourceId('a.b/c', 'd')]").unwrap(); + assert_eq!(result, "a.b/c:d"); } #[test] fn invalid_type() { let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[resourceId('a/b','c')]"); + let result = parser.parse_and_execute("[resourceId('a/b/c','d')]"); assert!(result.is_err()); } From 8bc08ef74f85b54a8c04c42e868bad2c46ea2025 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 2 Nov 2023 14:29:22 -0700 Subject: [PATCH 03/15] change from HashMap to serde Map --- dsc_lib/src/configure/config_doc.rs | 4 ++-- dsc_lib/src/configure/mod.rs | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index a6b73508..9296748c 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Map, Value}; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -73,7 +73,7 @@ pub struct Resource { pub depends_on: Option>, // `identity` can be used for run-as #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>, + pub properties: Option>, } // Defines the valid and recognized canonical URIs for the configuration schema diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 11782b61..8ec031b8 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -11,7 +11,7 @@ use crate::parser::Statement; use self::config_doc::Configuration; use self::depends_on::get_resource_invocation_order; use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult, ResourceMessage, MessageLevel}; -use serde_json::Value; +use serde_json::{Map, Value}; use std::collections::{HashMap, HashSet}; use tracing::debug; @@ -49,7 +49,7 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf let mut r = config_doc::Resource::new(); r.resource_type = resource.type_name.clone(); r.name = format!("{}-{i}", r.resource_type); - let props: HashMap = serde_json::from_value(instance.clone())?; + let props: Map = serde_json::from_value(instance.clone())?; r.properties = Some(props); conf.resources.push(r); @@ -101,7 +101,8 @@ impl Configurator { }; debug!("resource_type {}", &resource.resource_type); - let filter = serde_json::to_string(self.invoke_property_expressions(&resource.properties))?; + let properties = self.invoke_property_expressions(&resource.properties)?; + let filter = serde_json::to_string(&properties)?; let get_result = dsc_resource.get(&filter)?; let resource_result = config_result::ResourceGetResult { name: resource.name.clone(), @@ -325,24 +326,22 @@ impl Configurator { Ok((config, messages, has_errors)) } - fn invoke_property_expressions(&mut self, properties: &Option>) -> Result>, DscError> { + fn invoke_property_expressions(&mut self, properties: &Option>) -> Result>, DscError> { if properties.is_none() { return Ok(None); } - let mut result: HashMap = HashMap::new(); + let mut result: Map = Map::new(); if let Some(properties) = properties { for (name, value) in properties { // if value is an object, we have to do it recursively if let Value::Object(object) = value { - // convert serde map to hashmap - let object: HashMap = object.into_iter().collect(); - let value = self.invoke_property_expressions(Some(object))?; - result.insert(name, serde_json::to_value(value)?); + let value = self.invoke_property_expressions(&Some(object.clone()))?; + result.insert(name.clone(), serde_json::to_value(value)?); continue; } let value = self.statement_parser.parse_and_execute(&value.to_string())?; - result.insert(name, serde_json::from_str(&value)?); + result.insert(name.clone(), serde_json::from_str(&value)?); } } Ok(Some(result)) From cb851f116ce19ea081e0712dc7fc8e5726ee693b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 2 Nov 2023 14:58:32 -0700 Subject: [PATCH 04/15] add expression invocation for get/set/test --- dsc_lib/src/configure/mod.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 8ec031b8..afa97287 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -96,12 +96,11 @@ impl Configurator { return Ok(result); } for resource in get_resource_invocation_order(&config, &mut self.statement_parser)? { + let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); - - let properties = self.invoke_property_expressions(&resource.properties)?; let filter = serde_json::to_string(&properties)?; let get_result = dsc_resource.get(&filter)?; let resource_result = config_result::ResourceGetResult { @@ -134,11 +133,12 @@ impl Configurator { return Ok(result); } for resource in get_resource_invocation_order(&config, &mut self.statement_parser)? { + let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); - let desired = serde_json::to_string(&resource.properties)?; + let desired = serde_json::to_string(&properties)?; let set_result = dsc_resource.set(&desired, skip_test)?; let resource_result = config_result::ResourceSetResult { name: resource.name.clone(), @@ -170,11 +170,12 @@ impl Configurator { return Ok(result); } for resource in get_resource_invocation_order(&config, &mut self.statement_parser)? { + let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); - let expected = serde_json::to_string(&resource.properties)?; + let expected = serde_json::to_string(&properties)?; let test_result = dsc_resource.test(&expected)?; let resource_result = config_result::ResourceTestResult { name: resource.name.clone(), From be4a40ce51d126e67450cec2df3ea7f63b9a392a Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 2 Nov 2023 17:54:44 -0700 Subject: [PATCH 05/15] modify osinfo example to use concat for testing. update expression invocation to handle objects and arrays --- dsc/examples/assertion.dsc.yaml | 2 +- dsc/examples/osinfo_registry.dsc.yaml | 2 +- dsc_lib/src/configure/mod.rs | 37 ++++++++++++++++++++++----- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/dsc/examples/assertion.dsc.yaml b/dsc/examples/assertion.dsc.yaml index 25f868dd..d39ac363 100644 --- a/dsc/examples/assertion.dsc.yaml +++ b/dsc/examples/assertion.dsc.yaml @@ -20,7 +20,7 @@ resources: - name: system root type: Microsoft.Windows/Registry properties: - keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion + keyPath: "[concat('HKLM\','Software\Microsoft\Windows NT\','CurrentVersion')]" valueName: SystemRoot valueData: # this is deliberately set to z: drive so that the assertion fails diff --git a/dsc/examples/osinfo_registry.dsc.yaml b/dsc/examples/osinfo_registry.dsc.yaml index 612e8c96..df3f2ebe 100644 --- a/dsc/examples/osinfo_registry.dsc.yaml +++ b/dsc/examples/osinfo_registry.dsc.yaml @@ -7,7 +7,7 @@ resources: - name: windows product name type: Microsoft.Windows/Registry properties: - keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion + keyPath: "[concat('HKLM','\Software\Microsoft\Windows NT\','CurrentVersion')]" valueName: ProductName - name: system root type: Microsoft.Windows/Registry diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index afa97287..52c5fed3 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -335,14 +335,37 @@ impl Configurator { let mut result: Map = Map::new(); if let Some(properties) = properties { for (name, value) in properties { - // if value is an object, we have to do it recursively - if let Value::Object(object) = value { - let value = self.invoke_property_expressions(&Some(object.clone()))?; - result.insert(name.clone(), serde_json::to_value(value)?); - continue; + match value { + Value::Object(object) => { + let value = self.invoke_property_expressions(&Some(object.clone()))?; + result.insert(name.clone(), serde_json::to_value(value)?); + continue; + }, + Value::Array(array) => { + let mut result_array: Vec = Vec::new(); + for element in array { + match element { + Value::Object(object) => { + let value = self.invoke_property_expressions(&Some(object.clone()))?; + result_array.push(serde_json::to_value(value)?); + continue; + }, + Value::Array(_) => { + return Err(DscError::Parser("Nested arrays not supported".to_string())); + }, + _ => { + let value = self.statement_parser.parse_and_execute(&element.to_string())?; + result_array.push(serde_json::from_str(&value)?); + } + } + } + result.insert(name.clone(), serde_json::to_value(result_array)?); + }, + _ => { + let value = self.statement_parser.parse_and_execute(&value.to_string())?; + result.insert(name.clone(), serde_json::from_str(&value)?); + }, } - let value = self.statement_parser.parse_and_execute(&value.to_string())?; - result.insert(name.clone(), serde_json::from_str(&value)?); } } Ok(Some(result)) From fe817d0cf8b0441a37018262008bbd1c84dc9120 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 2 Nov 2023 18:35:28 -0700 Subject: [PATCH 06/15] fix example config with expression --- dsc/examples/osinfo_registry.dsc.json | 6 +++--- dsc/examples/osinfo_registry.dsc.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dsc/examples/osinfo_registry.dsc.json b/dsc/examples/osinfo_registry.dsc.json index 51e07206..f3103197 100644 --- a/dsc/examples/osinfo_registry.dsc.json +++ b/dsc/examples/osinfo_registry.dsc.json @@ -13,8 +13,8 @@ "name": "windows product name", "type": "Microsoft.Windows/Registry", "properties": { - "keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion", - "valueName": "ProductName" + "keyPath": "[concat('HKLM\\','Software\\Microsoft\\Windows NT\\','CurrentVersion')]", + "valueName": "ProductName" } }, { @@ -22,7 +22,7 @@ "type": "Microsoft.Windows/Registry", "properties": { "keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion", - "valueName": "SystemRoot" + "valueName": "SystemRoot" } } ] diff --git a/dsc/examples/osinfo_registry.dsc.yaml b/dsc/examples/osinfo_registry.dsc.yaml index df3f2ebe..ed527fab 100644 --- a/dsc/examples/osinfo_registry.dsc.yaml +++ b/dsc/examples/osinfo_registry.dsc.yaml @@ -7,7 +7,7 @@ resources: - name: windows product name type: Microsoft.Windows/Registry properties: - keyPath: "[concat('HKLM','\Software\Microsoft\Windows NT\','CurrentVersion')]" + keyPath: '[concat(''HKLM\'',''Software\Microsoft\Windows NT\'',''CurrentVersion'')]' valueName: ProductName - name: system root type: Microsoft.Windows/Registry From 45eed274043b46a2e4365587d721e25a3ac1c15a Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 2 Nov 2023 19:07:04 -0700 Subject: [PATCH 07/15] add tracing --- dsc/tests/dsc_config_get.tests.ps1 | 2 +- dsc_lib/src/functions/concat.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index 631746dd..ca480ffc 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -9,7 +9,7 @@ Describe 'dsc config get tests' { param($config) $jsonPath = Join-Path $PSScriptRoot '../examples' $config $config = Get-Content $jsonPath -Raw - $out = $config | dsc config get | ConvertFrom-Json + $out = $config | dsc -l debug config get | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.hadErrors | Should -BeFalse $out.results.Count | Should -Be 3 diff --git a/dsc_lib/src/functions/concat.rs b/dsc_lib/src/functions/concat.rs index d69d9740..3643fd28 100644 --- a/dsc_lib/src/functions/concat.rs +++ b/dsc_lib/src/functions/concat.rs @@ -3,6 +3,7 @@ use crate::DscError; use crate::functions::{Function, FunctionArg, FunctionResult, AcceptedArgKind}; +use tracing::debug; #[derive(Debug, Default)] pub struct Concat {} @@ -35,6 +36,7 @@ impl Function for Concat { } } } + debug!("concat result: {result}"); Ok(FunctionResult::String(result)) } } From 04ad821ccceb0c85aadc5dfd747fcf9161ca8611 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 15:35:15 -0700 Subject: [PATCH 08/15] fix invoking expressions and add dscecho resource --- .vscode/launch.json | 6 ++++ dsc/src/main.rs | 4 +-- dsc/tests/dsc_config_get.tests.ps1 | 2 +- dsc/tests/dsc_functions.tests.ps1 | 27 ++++++++++++++ dsc_lib/src/configure/mod.rs | 14 +++++--- tools/dsctest/dscecho.dsc.resource.json | 48 +++++++++++++++++++++++++ tools/dsctest/src/args.rs | 7 ++++ tools/dsctest/src/echo.rs | 11 ++++++ tools/dsctest/src/main.rs | 23 +++++++++--- 9 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 dsc/tests/dsc_functions.tests.ps1 create mode 100644 tools/dsctest/dscecho.dsc.resource.json create mode 100644 tools/dsctest/src/echo.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 70331889..c0fe6690 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,12 @@ ], "cwd": "${workspaceFolder}" }, + { + "name": "(macOS) Attach", + "type": "lldb", + "request": "attach", + "pid": "${command:pickMyProcess}", + }, { "name": "(Windows) Attach", "type": "cppvsdbg", diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 8ff8c1c9..fe621bca 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -138,7 +138,7 @@ fn terminate_subprocesses(sys: &System, process: &Process) { #[cfg(debug_assertions)] fn check_debug() { if env::var("DEBUG_DSC").is_ok() { - error!("attach debugger to pid {} and press a key to continue", std::process::id()); + eprintln!("attach debugger to pid {} and press a key to continue", std::process::id()); loop { let event = event::read().unwrap(); if let event::Event::Key(key) = event { @@ -147,7 +147,7 @@ fn check_debug() { break; } } else { - error!("Unexpected event: {event:?}"); + eprintln!("Unexpected event: {event:?}"); continue; } } diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index ca480ffc..631746dd 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -9,7 +9,7 @@ Describe 'dsc config get tests' { param($config) $jsonPath = Join-Path $PSScriptRoot '../examples' $config $config = Get-Content $jsonPath -Raw - $out = $config | dsc -l debug config get | ConvertFrom-Json + $out = $config | dsc config get | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.hadErrors | Should -BeFalse $out.results.Count | Should -Be 3 diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 new file mode 100644 index 00000000..c4c6b33b --- /dev/null +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'tests for function expressions' { + It 'function works: ' -TestCases @( + @{ text = "[concat('a', 'b')]"; expected = 'ab' } + @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } + @{ text = "[concat('a', 1, concat(2, 'b'))]"; expected = 'a12b' } + @{ text = "[base64('ab')]"; expected = 'YWI=' } + @{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' } + @{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' } + ) { + param($text, $expected) + + $escapedText = $text -replace "'", "''" + $config_yaml = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json + resources: + - name: Echo + type: Test/Echo + properties: + text: '$escapedText' +"@ + $out = $config_yaml | dsc config get | ConvertFrom-Json + $out.results[0].result.actualState.text | Should -Be $expected + } +} diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 52c5fed3..df922779 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -354,16 +354,22 @@ impl Configurator { return Err(DscError::Parser("Nested arrays not supported".to_string())); }, _ => { - let value = self.statement_parser.parse_and_execute(&element.to_string())?; - result_array.push(serde_json::from_str(&value)?); + let Some(statement) = element.as_str() else { + return Err(DscError::Parser("Array element could not be transformed as string".to_string())); + }; + let statement_result = self.statement_parser.parse_and_execute(statement)?; + result_array.push(Value::String(statement_result)); } } } result.insert(name.clone(), serde_json::to_value(result_array)?); }, _ => { - let value = self.statement_parser.parse_and_execute(&value.to_string())?; - result.insert(name.clone(), serde_json::from_str(&value)?); + let Some(statement) = value.as_str() else { + return Err(DscError::Parser("Property value could not be transformed as string".to_string())); + }; + let statement_result = self.statement_parser.parse_and_execute(statement)?; + result.insert(name.clone(), Value::String(statement_result)); }, } } diff --git a/tools/dsctest/dscecho.dsc.resource.json b/tools/dsctest/dscecho.dsc.resource.json new file mode 100644 index 00000000..2390934d --- /dev/null +++ b/tools/dsctest/dscecho.dsc.resource.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", + "type": "Test/Echo", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "echo", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "set": { + "executable": "dsctest", + "args": [ + "echo", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "test": { + "executable": "dsctest", + "args": [ + "echo", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "echo" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 310561bf..7acb03e9 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -5,6 +5,7 @@ use clap::{Parser, Subcommand, ValueEnum}; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum Schemas { + Echo, Sleep, } @@ -18,6 +19,12 @@ pub struct Args { #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum SubCommand { + #[clap(name = "echo", about = "Return the input")] + Echo { + #[clap(name = "input", short, long, help = "The input to the echo command")] + input: String, + }, + #[clap(name = "schema", about = "Get the JSON schema for a subcommand")] Schema { #[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")] diff --git a/tools/dsctest/src/echo.rs b/tools/dsctest/src/echo.rs new file mode 100644 index 00000000..e822648a --- /dev/null +++ b/tools/dsctest/src/echo.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Echo { + pub text: String, +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 6c4eb32d..1862ea7b 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -2,24 +2,39 @@ // Licensed under the MIT License. mod args; +mod echo; mod sleep; use args::{Args, Schemas, SubCommand}; use clap::Parser; use schemars::schema_for; +use crate::echo::Echo; use crate::sleep::Sleep; use std::{thread, time::Duration}; fn main() { let args = Args::parse(); let json = match args.subcommand { + SubCommand::Echo { input } => { + let echo = match serde_json::from_str::(&input) { + Ok(echo) => echo, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + serde_json::to_string(&echo).unwrap() + }, SubCommand::Schema { subcommand } => { - match subcommand { + let schema = match subcommand { + Schemas::Echo => { + schema_for!(Echo) + }, Schemas::Sleep => { - let schema = schema_for!(Sleep); - serde_json::to_string(&schema).unwrap() + schema_for!(Sleep) }, - } + }; + serde_json::to_string(&schema).unwrap() }, SubCommand::Sleep { input } => { let sleep = match serde_json::from_str::(&input) { From c2e0426724ec3d83f330f13e900ea6a5f2477fe7 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 15:37:00 -0700 Subject: [PATCH 09/15] revert change to use concat in sample config --- dsc/examples/assertion.dsc.yaml | 2 +- dsc/examples/osinfo_registry.dsc.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc/examples/assertion.dsc.yaml b/dsc/examples/assertion.dsc.yaml index d39ac363..25f868dd 100644 --- a/dsc/examples/assertion.dsc.yaml +++ b/dsc/examples/assertion.dsc.yaml @@ -20,7 +20,7 @@ resources: - name: system root type: Microsoft.Windows/Registry properties: - keyPath: "[concat('HKLM\','Software\Microsoft\Windows NT\','CurrentVersion')]" + keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion valueName: SystemRoot valueData: # this is deliberately set to z: drive so that the assertion fails diff --git a/dsc/examples/osinfo_registry.dsc.yaml b/dsc/examples/osinfo_registry.dsc.yaml index ed527fab..612e8c96 100644 --- a/dsc/examples/osinfo_registry.dsc.yaml +++ b/dsc/examples/osinfo_registry.dsc.yaml @@ -7,7 +7,7 @@ resources: - name: windows product name type: Microsoft.Windows/Registry properties: - keyPath: '[concat(''HKLM\'',''Software\Microsoft\Windows NT\'',''CurrentVersion'')]' + keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion valueName: ProductName - name: system root type: Microsoft.Windows/Registry From 9635be60190df0683f4c71f23d86564367907c4f Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 16:14:01 -0700 Subject: [PATCH 10/15] update error to provide more info --- dsc_lib/src/configure/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index df922779..16b0d19a 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -366,7 +366,7 @@ impl Configurator { }, _ => { let Some(statement) = value.as_str() else { - return Err(DscError::Parser("Property value could not be transformed as string".to_string())); + return Err(DscError::Parser(format!("Property value '{}' could not be transformed as string", value.to_string()))); }; let statement_result = self.statement_parser.parse_and_execute(statement)?; result.insert(name.clone(), Value::String(statement_result)); From a7d61edbe51f74a1ec54328ed444cf9e2481e4f0 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 16:30:19 -0700 Subject: [PATCH 11/15] fix clippy --- dsc_lib/src/configure/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 16b0d19a..46466fa4 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -366,7 +366,7 @@ impl Configurator { }, _ => { let Some(statement) = value.as_str() else { - return Err(DscError::Parser(format!("Property value '{}' could not be transformed as string", value.to_string()))); + return Err(DscError::Parser(format!("Property value '{value}' could not be transformed as string"))); }; let statement_result = self.statement_parser.parse_and_execute(statement)?; result.insert(name.clone(), Value::String(statement_result)); From 90f9264a1021f15e09f0c846a9755f2383c127da Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 16:48:32 -0700 Subject: [PATCH 12/15] handle ints, bools, etc.. --- dsc_lib/src/configure/mod.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 46466fa4..fa47c4f9 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -353,24 +353,34 @@ impl Configurator { Value::Array(_) => { return Err(DscError::Parser("Nested arrays not supported".to_string())); }, - _ => { + Value::String(_) => { + // use as_str() so that the enclosing quotes are not included for strings let Some(statement) = element.as_str() else { return Err(DscError::Parser("Array element could not be transformed as string".to_string())); }; let statement_result = self.statement_parser.parse_and_execute(statement)?; result_array.push(Value::String(statement_result)); } + _ => { + let statement_result = self.statement_parser.parse_and_execute(&value.to_string())?; + result_array.push(Value::String(statement_result)); + } } } result.insert(name.clone(), serde_json::to_value(result_array)?); }, - _ => { + Value::String(_) => { + // use as_str() so that the enclosing quotes are not included for strings let Some(statement) = value.as_str() else { return Err(DscError::Parser(format!("Property value '{value}' could not be transformed as string"))); }; let statement_result = self.statement_parser.parse_and_execute(statement)?; result.insert(name.clone(), Value::String(statement_result)); }, + _ => { + let statement_result = self.statement_parser.parse_and_execute(&value.to_string())?; + result.insert(name.clone(), Value::String(statement_result)); + }, } } } From b41d6a588cf583a2c93e5b3095f18cb773beed86 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 17:14:56 -0700 Subject: [PATCH 13/15] add more details in error --- dsc_lib/src/parser/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsc_lib/src/parser/mod.rs b/dsc_lib/src/parser/mod.rs index 777832e1..c2cd0292 100644 --- a/dsc_lib/src/parser/mod.rs +++ b/dsc_lib/src/parser/mod.rs @@ -42,15 +42,15 @@ impl Statement { /// This function will return an error if the statement fails to parse or execute. pub fn parse_and_execute(&mut self, statement: &str) -> Result { let Some(tree) = &mut self.parser.parse(statement, None) else { - return Err(DscError::Parser("Error parsing statement".to_string())); + return Err(DscError::Parser(format!("Error parsing statement: {statement}")); }; let root_node = tree.root_node(); if root_node.is_error() { - return Err(DscError::Parser("Error parsing statement root".to_string())); + return Err(DscError::Parser(format!("Error parsing statement root: {statement}"))); } let root_node_kind = root_node.kind(); if root_node_kind != "statement" { - return Err(DscError::Parser("Invalid statement".to_string())); + return Err(DscError::Parser(format!("Invalid statement: {statement}"))); } let Some(child_node) = root_node.named_child(0) else { return Err(DscError::Parser("Child node not found".to_string())); From 607d2f7964276808475c79e4a00046fa6dee8fcc Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 Nov 2023 19:33:10 -0700 Subject: [PATCH 14/15] fix missing parens --- dsc_lib/src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/parser/mod.rs b/dsc_lib/src/parser/mod.rs index c2cd0292..93cc74fb 100644 --- a/dsc_lib/src/parser/mod.rs +++ b/dsc_lib/src/parser/mod.rs @@ -42,7 +42,7 @@ impl Statement { /// This function will return an error if the statement fails to parse or execute. pub fn parse_and_execute(&mut self, statement: &str) -> Result { let Some(tree) = &mut self.parser.parse(statement, None) else { - return Err(DscError::Parser(format!("Error parsing statement: {statement}")); + return Err(DscError::Parser(format!("Error parsing statement: {statement}"))); }; let root_node = tree.root_node(); if root_node.is_error() { From 54a38804de353dc9eefdf55446e872163d0b5a20 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 6 Nov 2023 16:29:51 -0800 Subject: [PATCH 15/15] fix so that export will escape brackets if necessary --- dsc_lib/src/configure/mod.rs | 113 ++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index fa47c4f9..b9981109 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -44,13 +44,12 @@ pub enum ErrorAction { pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf: &mut Configuration) -> Result<(), DscError> { let export_result = resource.export()?; - for (i, instance) in export_result.actual_state.iter().enumerate() - { + for (i, instance) in export_result.actual_state.iter().enumerate() { let mut r = config_doc::Resource::new(); r.resource_type = resource.type_name.clone(); r.name = format!("{}-{i}", r.resource_type); let props: Map = serde_json::from_value(instance.clone())?; - r.properties = Some(props); + r.properties = escape_property_values(&props)?; conf.resources.push(r); } @@ -58,6 +57,66 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf Ok(()) } +// for values returned by resources, they may look like expressions, so we make sure to escape them in case +// they are re-used to apply configuration +fn escape_property_values(properties: &Map) -> Result>, DscError> { + let mut result: Map = Map::new(); + for (name, value) in properties { + match value { + Value::Object(object) => { + let value = escape_property_values(&object.clone())?; + result.insert(name.clone(), serde_json::to_value(value)?); + continue; + }, + Value::Array(array) => { + let mut result_array: Vec = Vec::new(); + for element in array { + match element { + Value::Object(object) => { + let value = escape_property_values(&object.clone())?; + result_array.push(serde_json::to_value(value)?); + continue; + }, + Value::Array(_) => { + return Err(DscError::Parser("Nested arrays not supported".to_string())); + }, + Value::String(_) => { + // use as_str() so that the enclosing quotes are not included for strings + let Some(statement) = element.as_str() else { + return Err(DscError::Parser("Array element could not be transformed as string".to_string())); + }; + if statement.starts_with('[') && statement.ends_with(']') { + result_array.push(Value::String(format!("[{statement}"))); + } else { + result_array.push(element.clone()); + } + } + _ => { + result_array.push(element.clone()); + } + } + } + result.insert(name.clone(), serde_json::to_value(result_array)?); + }, + Value::String(_) => { + // use as_str() so that the enclosing quotes are not included for strings + let Some(statement) = value.as_str() else { + return Err(DscError::Parser(format!("Property value '{value}' could not be transformed as string"))); + }; + if statement.starts_with('[') && statement.ends_with(']') { + result.insert(name.clone(), Value::String(format!("[{statement}"))); + } else { + result.insert(name.clone(), value.clone()); + } + }, + _ => { + result.insert(name.clone(), value.clone()); + }, + } + } + Ok(Some(result)) +} + impl Configurator { /// Create a new `Configurator` instance. /// @@ -188,27 +247,6 @@ impl Configurator { Ok(result) } - fn find_duplicate_resource_types(config: &Configuration) -> Vec - { - let mut map: HashMap<&String, i32> = HashMap::new(); - let mut result: HashSet = HashSet::new(); - let resource_list = &config.resources; - if resource_list.is_empty() { - return Vec::new(); - } - - for r in resource_list - { - let v = map.entry(&r.resource_type).or_insert(0); - *v += 1; - if *v > 1 { - result.insert(r.resource_type.clone()); - } - } - - result.into_iter().collect() - } - /// Invoke the export operation on a configuration. /// /// # Arguments @@ -256,6 +294,27 @@ impl Configurator { Ok(result) } + fn find_duplicate_resource_types(config: &Configuration) -> Vec + { + let mut map: HashMap<&String, i32> = HashMap::new(); + let mut result: HashSet = HashSet::new(); + let resource_list = &config.resources; + if resource_list.is_empty() { + return Vec::new(); + } + + for r in resource_list + { + let v = map.entry(&r.resource_type).or_insert(0); + *v += 1; + if *v > 1 { + result.insert(r.resource_type.clone()); + } + } + + result.into_iter().collect() + } + fn validate_config(&mut self) -> Result<(Configuration, Vec, bool), DscError> { let config: Configuration = serde_json::from_str(self.config.as_str())?; let mut messages: Vec = Vec::new(); @@ -362,8 +421,7 @@ impl Configurator { result_array.push(Value::String(statement_result)); } _ => { - let statement_result = self.statement_parser.parse_and_execute(&value.to_string())?; - result_array.push(Value::String(statement_result)); + result_array.push(element.clone()); } } } @@ -378,8 +436,7 @@ impl Configurator { result.insert(name.clone(), Value::String(statement_result)); }, _ => { - let statement_result = self.statement_parser.parse_and_execute(&value.to_string())?; - result.insert(name.clone(), Value::String(statement_result)); + result.insert(name.clone(), value.clone()); }, } }