Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions dsc/tests/dsc_copy.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
5 changes: 5 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 result is not an integer: %{expression}"

[discovery.commandDiscovery]
couldNotReadSetting = "Could not read 'resourcePath' setting"
Expand Down Expand Up @@ -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]
Expand Down
20 changes: 18 additions & 2 deletions lib/dsc-lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CopyMode>,
#[serde(skip_serializing_if = "Option::is_none", rename = "batchSize")]
pub batch_size: Option<i64>,
pub batch_size: Option<IntOrExpression>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
17 changes: 13 additions & 4 deletions lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -779,7 +779,7 @@ impl Configurator {
if let Some(parameters_input) = parameters_input {
trace!("parameters_input: {parameters_input}");
let input_parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;

for (name, value) in input_parameters {
if let Some(constraint) = parameters.get(&name) {
debug!("Validating parameter '{name}'");
Expand Down Expand Up @@ -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) = &parameter.default_value {
Expand Down Expand Up @@ -962,7 +962,16 @@ impl Configurator {
self.context.process_mode = ProcessMode::Copy;
self.context.copy_current_loop_name.clone_from(&copy.name);
let mut copy_resources = Vec::<Resource>::new();
for i in 0..copy.count {
let count: i64 = match &copy.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", expression = e).to_string()))
};
n.as_i64().ok_or_else(|| DscError::Parser(t!("configure.mod.copyCountResultNotInteger", expression = e).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 {
Expand Down
33 changes: 28 additions & 5 deletions lib/dsc-lib/src/functions/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +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 {
return Err(DscError::Parser(t!("functions.parameters.keyNotString", key = key).to_string()));
};
},
DataType::Int => {
let Some(_value) = value.as_i64() else {
return Err(DscError::Parser(t!("functions.parameters.keyNotInt", key = key).to_string()));
};
},
DataType::Bool => {
let Some(_value) = value.as_bool() else {
return Err(DscError::Parser(t!("functions.parameters.keyNotBool", key = key).to_string()));
};
},
DataType::Object => {
let Some(_value) = value.as_object() else {
return Err(DscError::Parser(t!("functions.parameters.keyNotObject", key = key).to_string()));
};
},
DataType::Array => {
let Some(_value) = value.as_array() else {
return Err(DscError::Parser(t!("functions.parameters.keyNotArray", key = key).to_string()));
};
},
_ => {
Ok(value.clone())
}
}
Ok(value.clone())
}
else {
Err(DscError::Parser(t!("functions.parameters.keyNotFound", key = key).to_string()))
Expand Down