From 227870bd36c4d4c88a6e9e0992c044ab3aa961ca Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 10 Oct 2025 22:12:56 -0700 Subject: [PATCH 1/3] Fix allowing `copy` count to accept an expression --- dsc/tests/dsc_copy.tests.ps1 | 29 ++++++++++++++++++++++ lib/dsc-lib/locales/en-us.toml | 5 ++++ lib/dsc-lib/src/configure/config_doc.rs | 20 +++++++++++++-- lib/dsc-lib/src/configure/mod.rs | 19 ++++++++++---- lib/dsc-lib/src/functions/parameters.rs | 33 ++++++++++++++++++++++--- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/dsc/tests/dsc_copy.tests.ps1 b/dsc/tests/dsc_copy.tests.ps1 index 968d3da0c..c7f458aec 100644 --- a/dsc/tests/dsc_copy.tests.ps1 +++ b/dsc/tests/dsc_copy.tests.ps1 @@ -234,4 +234,33 @@ resources: $out.results[1].name | Should -Be 'Server-1' $out.results[1].result.actualState.output | Should -Be 'Environment: test' } + + It 'Copy count using expression' { + $configYaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + serverCount: + type: int + defaultValue: 4 +resources: +- name: "[format('Server-{0}', copyIndex())]" + copy: + name: testLoop + count: "[parameters('serverCount')]" + type: Microsoft.DSC.Debug/Echo + properties: + output: Hello +'@ + $out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $testdrive/error.log -Raw | Out-String) + $out.results.Count | Should -Be 4 + $out.results[0].name | Should -Be 'Server-0' + $out.results[0].result.actualState.output | Should -Be 'Hello' + $out.results[1].name | Should -Be 'Server-1' + $out.results[1].result.actualState.output | Should -Be 'Hello' + $out.results[2].name | Should -Be 'Server-2' + $out.results[2].result.actualState.output | Should -Be 'Hello' + $out.results[3].name | Should -Be 'Server-3' + $out.results[3].result.actualState.output | Should -Be 'Hello' + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 9c1a71628..364d1e6e2 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -79,6 +79,7 @@ nameResultNotString = "Resource name result is not a string" circularDependency = "Circular dependency or unresolvable parameter references detected in parameters: %{parameters}" userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined" addingUserFunction = "Adding user function '%{name}'" +copyCountResultNotInteger = "Copy count is not an integer: %{value}" [discovery.commandDiscovery] couldNotReadSetting = "Could not read 'resourcePath' setting" @@ -444,6 +445,10 @@ description = "Retrieves parameters from the configuration" invoked = "parameters function" traceKey = "parameters key: %{key}" keyNotString = "Parameter '%{key}' is not a string" +keyNotInt = "Parameter '%{key}' is not an integer" +keyNotBool = "Parameter '%{key}' is not a boolean" +keyNotObject = "Parameter '%{key}' is not an object" +keyNotArray = "Parameter '%{key}' is not an array" keyNotFound = "Parameter '%{key}' not found in context" [functions.path] diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index 47ec09294..3a4b02f27 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -211,15 +211,31 @@ pub enum CopyMode { Parallel, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] +pub enum IntOrExpression { + Int(i64), + Expression(String), +} + +impl Display for IntOrExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IntOrExpression::Int(i) => write!(f, "{i}"), + IntOrExpression::Expression(s) => write!(f, "{s}"), + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Copy { pub name: String, - pub count: i64, + pub count: IntOrExpression, #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, #[serde(skip_serializing_if = "Option::is_none", rename = "batchSize")] - pub batch_size: Option, + pub batch_size: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 4ceb7f355..7746f0c69 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -3,7 +3,7 @@ use crate::configure::config_doc::{ExecutionKind, Metadata, Resource, Parameter}; use crate::configure::context::{Context, ProcessMode}; -use crate::configure::{config_doc::RestartRequired, parameters::Input}; +use crate::configure::{config_doc::{IntOrExpression, RestartRequired}, parameters::Input}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; use crate::dscresources::{ @@ -46,7 +46,7 @@ pub struct Configurator { /// # Arguments /// /// * `resource` - The resource to export. -/// * `conf` - The configuration to add the results to. +/// * `conf` - The configuration to add t`he results to. /// /// # Panics /// @@ -779,7 +779,7 @@ impl Configurator { if let Some(parameters_input) = parameters_input { trace!("parameters_input: {parameters_input}"); let input_parameters: HashMap = serde_json::from_value::(parameters_input.clone())?.parameters; - + for (name, value) in input_parameters { if let Some(constraint) = parameters.get(&name) { debug!("Validating parameter '{name}'"); @@ -818,7 +818,7 @@ impl Configurator { while !unresolved_parameters.is_empty() { let mut resolved_in_this_pass = Vec::new(); - + for (name, parameter) in &unresolved_parameters { debug!("{}", t!("configure.mod.processingParameter", name = name)); if let Some(default_value) = ¶meter.default_value { @@ -962,7 +962,16 @@ impl Configurator { self.context.process_mode = ProcessMode::Copy; self.context.copy_current_loop_name.clone_from(©.name); let mut copy_resources = Vec::::new(); - for i in 0..copy.count { + let count: i64 = match ©.count { + IntOrExpression::Int(i) => *i, + IntOrExpression::Expression(e) => { + let Value::Number(n) = self.statement_parser.parse_and_execute(e, &self.context)? else { + return Err(DscError::Parser(t!("configure.mod.copyCountResultNotInteger", value = ©.count).to_string())) + }; + n.as_i64().ok_or_else(|| DscError::Parser(t!("configure.mod.copyCountResultNotInteger", value = ©.count).to_string()))? + }, + }; + for i in 0..count { self.context.copy.insert(copy.name.clone(), i); let mut new_resource = resource.clone(); let Value::String(new_name) = self.statement_parser.parse_and_execute(&resource.name, &self.context)? else { diff --git a/lib/dsc-lib/src/functions/parameters.rs b/lib/dsc-lib/src/functions/parameters.rs index dd1ee34ca..740e50376 100644 --- a/lib/dsc-lib/src/functions/parameters.rs +++ b/lib/dsc-lib/src/functions/parameters.rs @@ -51,9 +51,36 @@ impl Function for Parameters { }; Ok(serde_json::to_value(secure_object)?) }, - _ => { - Ok(value.clone()) - } + DataType::String => { + let Some(value) = value.as_str() else { + return Err(DscError::Parser(t!("functions.parameters.keyNotString", key = key).to_string())); + }; + Ok(serde_json::to_value(value)?) + }, + DataType::Int => { + let Some(value) = value.as_i64() else { + return Err(DscError::Parser(t!("functions.parameters.keyNotInt", key = key).to_string())); + }; + Ok(serde_json::to_value(value)?) + }, + DataType::Bool => { + let Some(value) = value.as_bool() else { + return Err(DscError::Parser(t!("functions.parameters.keyNotBool", key = key).to_string())); + }; + Ok(serde_json::to_value(value)?) + }, + DataType::Object => { + let Some(value) = value.as_object() else { + return Err(DscError::Parser(t!("functions.parameters.keyNotObject", key = key).to_string())); + }; + Ok(serde_json::to_value(value)?) + }, + DataType::Array => { + let Some(value) = value.as_array() else { + return Err(DscError::Parser(t!("functions.parameters.keyNotArray", key = key).to_string())); + }; + Ok(serde_json::to_value(value)?) + }, } } else { From 2e63ab3207eeee7cf2d137e13511d28a7d324d3a Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 11 Oct 2025 13:47:20 -0700 Subject: [PATCH 2/3] remove accidental backtick --- lib/dsc-lib/src/configure/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 7746f0c69..237307f5b 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -46,7 +46,7 @@ pub struct Configurator { /// # Arguments /// /// * `resource` - The resource to export. -/// * `conf` - The configuration to add t`he results to. +/// * `conf` - The configuration to add the results to. /// /// # Panics /// From 7e0fa188eee281ae871e1d48a9551b53a6c989d8 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 20 Oct 2025 15:59:16 -0700 Subject: [PATCH 3/3] address copilot feedback --- lib/dsc-lib/locales/en-us.toml | 2 +- lib/dsc-lib/src/configure/mod.rs | 4 ++-- lib/dsc-lib/src/functions/parameters.rs | 20 ++++++++------------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 364d1e6e2..03e9c413d 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -79,7 +79,7 @@ nameResultNotString = "Resource name result is not a string" circularDependency = "Circular dependency or unresolvable parameter references detected in parameters: %{parameters}" userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined" addingUserFunction = "Adding user function '%{name}'" -copyCountResultNotInteger = "Copy count is not an integer: %{value}" +copyCountResultNotInteger = "Copy count result is not an integer: %{expression}" [discovery.commandDiscovery] couldNotReadSetting = "Could not read 'resourcePath' setting" diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 237307f5b..0f3ac5a81 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -966,9 +966,9 @@ impl Configurator { IntOrExpression::Int(i) => *i, IntOrExpression::Expression(e) => { let Value::Number(n) = self.statement_parser.parse_and_execute(e, &self.context)? else { - return Err(DscError::Parser(t!("configure.mod.copyCountResultNotInteger", value = ©.count).to_string())) + return Err(DscError::Parser(t!("configure.mod.copyCountResultNotInteger", expression = e).to_string())) }; - n.as_i64().ok_or_else(|| DscError::Parser(t!("configure.mod.copyCountResultNotInteger", value = ©.count).to_string()))? + n.as_i64().ok_or_else(|| DscError::Parser(t!("configure.mod.copyCountResultNotInteger", expression = e).to_string()))? }, }; for i in 0..count { diff --git a/lib/dsc-lib/src/functions/parameters.rs b/lib/dsc-lib/src/functions/parameters.rs index 740e50376..f2dffab9d 100644 --- a/lib/dsc-lib/src/functions/parameters.rs +++ b/lib/dsc-lib/src/functions/parameters.rs @@ -43,45 +43,41 @@ impl Function for Parameters { let secure_string = SecureString { secure_string: value.to_string(), }; - Ok(serde_json::to_value(secure_string)?) + return Ok(serde_json::to_value(secure_string)?); }, DataType::SecureObject => { let secure_object = SecureObject { secure_object: value.clone(), }; - Ok(serde_json::to_value(secure_object)?) + return Ok(serde_json::to_value(secure_object)?); }, DataType::String => { - let Some(value) = value.as_str() else { + let Some(_value) = value.as_str() else { return Err(DscError::Parser(t!("functions.parameters.keyNotString", key = key).to_string())); }; - Ok(serde_json::to_value(value)?) }, DataType::Int => { - let Some(value) = value.as_i64() else { + let Some(_value) = value.as_i64() else { return Err(DscError::Parser(t!("functions.parameters.keyNotInt", key = key).to_string())); }; - Ok(serde_json::to_value(value)?) }, DataType::Bool => { - let Some(value) = value.as_bool() else { + let Some(_value) = value.as_bool() else { return Err(DscError::Parser(t!("functions.parameters.keyNotBool", key = key).to_string())); }; - Ok(serde_json::to_value(value)?) }, DataType::Object => { - let Some(value) = value.as_object() else { + let Some(_value) = value.as_object() else { return Err(DscError::Parser(t!("functions.parameters.keyNotObject", key = key).to_string())); }; - Ok(serde_json::to_value(value)?) }, DataType::Array => { - let Some(value) = value.as_array() else { + let Some(_value) = value.as_array() else { return Err(DscError::Parser(t!("functions.parameters.keyNotArray", key = key).to_string())); }; - Ok(serde_json::to_value(value)?) }, } + Ok(value.clone()) } else { Err(DscError::Parser(t!("functions.parameters.keyNotFound", key = key).to_string()))