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
172 changes: 172 additions & 0 deletions dsc/tests/dsc_parameters.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,176 @@ Describe 'Parameters tests' {
$errorMessage = Get-Content -Path $TestDrive/error.log -Raw
$errorMessage | Should -BeLike "*ERROR*Parameter input failure: JSON: missing field ````parameters````*"
}

It 'Parameters can reference other parameters in defaultValue: simple nested' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
basePrefix:
type: string
defaultValue: base
computedPrefix:
type: string
defaultValue: "[concat(parameters('basePrefix'), '-computed')]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('computedPrefix')]"
"@

$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -BeExactly 'base-computed'
}

It 'Parameters can reference other parameters in defaultValue: multi-level nested' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
environment:
type: string
defaultValue: dev
appName:
type: string
defaultValue: "[concat(parameters('environment'), '-myapp')]"
instanceName:
type: string
defaultValue: "[concat(parameters('appName'), '-001')]"
fullInstanceName:
type: string
defaultValue: "[concat('Instance: ', parameters('instanceName'))]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('fullInstanceName')]"
"@

$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -BeExactly 'Instance: dev-myapp-001'
}

It 'Parameters with circular dependencies are detected' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
paramA:
type: string
defaultValue: "[parameters('paramB')]"
paramB:
type: string
defaultValue: "[parameters('paramA')]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('paramA')]"
"@

$testError = & {$config_yaml | dsc config get -f - 2>&1}
$LASTEXITCODE | Should -Be 4
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
}

It 'Parameters with complex circular dependencies are detected' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
paramA:
type: string
defaultValue: "[parameters('paramB')]"
paramB:
type: string
defaultValue: "[parameters('paramC')]"
paramC:
type: string
defaultValue: "[parameters('paramA')]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('paramA')]"
"@

$testError = & {$config_yaml | dsc config get -f - 2>&1}
$LASTEXITCODE | Should -Be 4
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
}

It 'Parameters with nested references can be overridden by input' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
basePrefix:
type: string
defaultValue: base
computedPrefix:
type: string
defaultValue: "[concat(parameters('basePrefix'), '-computed')]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('computedPrefix')]"
"@
$params_json = @{ parameters = @{ basePrefix = 'override' }} | ConvertTo-Json

$out = $config_yaml | dsc config -p $params_json get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -BeExactly 'override-computed'
}

It 'Parameters nested references work with different data types: <type>' -TestCases @(
@{ type = 'string'; baseValue = 'test'; expectedOutput = 'prefix-test-suffix' }
@{ type = 'int'; baseValue = 42; expectedOutput = 'value-42' }
) {
param($type, $baseValue, $expectedOutput)

$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
baseParam:
type: $type
defaultValue: $baseValue
computedParam:
type: string
defaultValue: "[concat('prefix-', string(parameters('baseParam')), '-suffix')]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('computedParam')]"
"@

if ($type -eq 'string') {
$expectedOutput = 'prefix-test-suffix'
} else {
$expectedOutput = 'prefix-42-suffix'
}

$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -BeExactly $expectedOutput
}

It 'Parameters with unresolvable references produce error' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
computedParam:
type: string
defaultValue: "[parameters('nonExistentParam')]"
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[parameters('computedParam')]"
"@

$testError = & {$config_yaml | dsc config get -f - 2>&1}
$LASTEXITCODE | Should -Be 4
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
}
}
2 changes: 1 addition & 1 deletion dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ noParametersDefined = "No parameters defined in configuration"
processingParameter = "Processing parameter '%{name}'"
setDefaultParameter = "Set default parameter '%{name}'"
defaultStringNotDefined = "Default value as string is not defined"
noParametersInput = "No parameters input"
setSecureParameter = "Set secure parameter '%{name}'"
setParameter = "Set parameter '%{name}' to '%{value}'"
parameterNotDefined = "Parameter '%{name}' is not defined in configuration"
Expand All @@ -78,6 +77,7 @@ copyModeNotSupported = "Copy mode is not supported"
copyBatchSizeNotSupported = "Copy batch size is not supported"
copyNameResultNotString = "Copy name result is not a string"
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}'"

Expand Down
120 changes: 70 additions & 50 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::configure::config_doc::{ExecutionKind, Metadata, Resource};
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::discovery::discovery_trait::DiscoveryFilter;
Expand Down Expand Up @@ -757,7 +757,6 @@ impl Configurator {
}

fn set_parameters(&mut self, parameters_input: Option<&Value>, config: &Configuration) -> Result<(), DscError> {
// set default parameters first
let Some(parameters) = &config.parameters else {
if parameters_input.is_none() {
info!("{}", t!("configure.mod.noParameters"));
Expand All @@ -766,66 +765,87 @@ impl Configurator {
return Err(DscError::Validation(t!("configure.mod.noParametersDefined").to_string()));
};

for (name, parameter) in parameters {
debug!("{}", t!("configure.mod.processingParameter", name = name));
if let Some(default_value) = &parameter.default_value {
debug!("{}", t!("configure.mod.setDefaultParameter", name = name));
// default values can be expressions
let value = if default_value.is_string() {
if let Some(value) = default_value.as_str() {
self.context.process_mode = ProcessMode::ParametersDefault;
let result = self.statement_parser.parse_and_execute(value, &self.context)?;
self.context.process_mode = ProcessMode::Normal;
result
// process input parameters first
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}'");
check_length(&name, &value, constraint)?;
check_allowed_values(&name, &value, constraint)?;
check_number_limits(&name, &value, constraint)?;
// TODO: additional array constraints
// TODO: object constraints

validate_parameter_type(&name, &value, &constraint.parameter_type)?;
if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject {
info!("{}", t!("configure.mod.setSecureParameter", name = name));
} else {
return Err(DscError::Parser(t!("configure.mod.defaultStringNotDefined").to_string()));
info!("{}", t!("configure.mod.setParameter", name = name, value = value));
}
} else {
default_value.clone()
};
validate_parameter_type(name, &value, &parameter.parameter_type)?;
self.context.parameters.insert(name.clone(), (value, parameter.parameter_type.clone()));

self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
if let Some(parameters) = &mut self.config.parameters {
if let Some(parameter) = parameters.get_mut(&name) {
parameter.default_value = Some(value);
}
}
}
else {
return Err(DscError::Validation(t!("configure.mod.parameterNotDefined", name = name).to_string()));
}
}
}

let Some(parameters_input) = parameters_input else {
debug!("{}", t!("configure.mod.noParametersInput"));
return Ok(());
};
// Now process default values for parameters that weren't provided in input
let mut unresolved_parameters: HashMap<String, &Parameter> = parameters
.iter()
.filter(|(name, _)| !self.context.parameters.contains_key(*name))
.map(|(k, v)| (k.clone(), v))
.collect();

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 {
debug!("{}", t!("configure.mod.setDefaultParameter", name = name));
let value_result = if default_value.is_string() {
if let Some(value) = default_value.as_str() {
self.context.process_mode = ProcessMode::ParametersDefault;
let result = self.statement_parser.parse_and_execute(value, &self.context);
self.context.process_mode = ProcessMode::Normal;
result
} else {
return Err(DscError::Parser(t!("configure.mod.defaultStringNotDefined").to_string()));
}
} else {
Ok(default_value.clone())
};

trace!("parameters_input: {parameters_input}");
let parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
let Some(parameters_constraints) = &config.parameters else {
return Err(DscError::Validation(t!("configure.mod.noParametersDefined").to_string()));
};
for (name, value) in parameters {
if let Some(constraint) = parameters_constraints.get(&name) {
debug!("Validating parameter '{name}'");
check_length(&name, &value, constraint)?;
check_allowed_values(&name, &value, constraint)?;
check_number_limits(&name, &value, constraint)?;
// TODO: additional array constraints
// TODO: object constraints

validate_parameter_type(&name, &value, &constraint.parameter_type)?;
if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject {
info!("{}", t!("configure.mod.setSecureParameter", name = name));
if let Ok(value) = value_result {
validate_parameter_type(name, &value, &parameter.parameter_type)?;
self.context.parameters.insert(name.to_string(), (value, parameter.parameter_type.clone()));
resolved_in_this_pass.push(name.clone());
}
} else {
info!("{}", t!("configure.mod.setParameter", name = name, value = value));
resolved_in_this_pass.push(name.clone());
}
}

self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
// also update the configuration with the parameter value
if let Some(parameters) = &mut self.config.parameters {
if let Some(parameter) = parameters.get_mut(&name) {
parameter.default_value = Some(value);
}
}
if resolved_in_this_pass.is_empty() {
let unresolved_names: Vec<_> = unresolved_parameters.keys().map(std::string::String::as_str).collect();
return Err(DscError::Validation(t!("configure.mod.circularDependency", parameters = unresolved_names.join(", ")).to_string()));
}
else {
return Err(DscError::Validation(t!("configure.mod.parameterNotDefined", name = name).to_string()));

for name in &resolved_in_this_pass {
unresolved_parameters.remove(name);
}
}

Ok(())
}

Expand Down