Skip to content

Commit b8c3b18

Browse files
authored
Merge pull request #1159 from Gijsreyn/gh-1129/main/fix-nested-parameter
Fix nested parameters not found
2 parents 288336a + 1761932 commit b8c3b18

File tree

3 files changed

+243
-51
lines changed

3 files changed

+243
-51
lines changed

dsc/tests/dsc_parameters.tests.ps1

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,4 +405,176 @@ Describe 'Parameters tests' {
405405
$errorMessage = Get-Content -Path $TestDrive/error.log -Raw
406406
$errorMessage | Should -BeLike "*ERROR*Parameter input failure: JSON: missing field ````parameters````*"
407407
}
408+
409+
It 'Parameters can reference other parameters in defaultValue: simple nested' {
410+
$config_yaml = @"
411+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
412+
parameters:
413+
basePrefix:
414+
type: string
415+
defaultValue: base
416+
computedPrefix:
417+
type: string
418+
defaultValue: "[concat(parameters('basePrefix'), '-computed')]"
419+
resources:
420+
- name: Echo
421+
type: Microsoft.DSC.Debug/Echo
422+
properties:
423+
output: "[parameters('computedPrefix')]"
424+
"@
425+
426+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
427+
$LASTEXITCODE | Should -Be 0
428+
$out.results[0].result.actualState.output | Should -BeExactly 'base-computed'
429+
}
430+
431+
It 'Parameters can reference other parameters in defaultValue: multi-level nested' {
432+
$config_yaml = @"
433+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
434+
parameters:
435+
environment:
436+
type: string
437+
defaultValue: dev
438+
appName:
439+
type: string
440+
defaultValue: "[concat(parameters('environment'), '-myapp')]"
441+
instanceName:
442+
type: string
443+
defaultValue: "[concat(parameters('appName'), '-001')]"
444+
fullInstanceName:
445+
type: string
446+
defaultValue: "[concat('Instance: ', parameters('instanceName'))]"
447+
resources:
448+
- name: Echo
449+
type: Microsoft.DSC.Debug/Echo
450+
properties:
451+
output: "[parameters('fullInstanceName')]"
452+
"@
453+
454+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
455+
$LASTEXITCODE | Should -Be 0
456+
$out.results[0].result.actualState.output | Should -BeExactly 'Instance: dev-myapp-001'
457+
}
458+
459+
It 'Parameters with circular dependencies are detected' {
460+
$config_yaml = @"
461+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
462+
parameters:
463+
paramA:
464+
type: string
465+
defaultValue: "[parameters('paramB')]"
466+
paramB:
467+
type: string
468+
defaultValue: "[parameters('paramA')]"
469+
resources:
470+
- name: Echo
471+
type: Microsoft.DSC.Debug/Echo
472+
properties:
473+
output: "[parameters('paramA')]"
474+
"@
475+
476+
$testError = & {$config_yaml | dsc config get -f - 2>&1}
477+
$LASTEXITCODE | Should -Be 4
478+
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
479+
}
480+
481+
It 'Parameters with complex circular dependencies are detected' {
482+
$config_yaml = @"
483+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
484+
parameters:
485+
paramA:
486+
type: string
487+
defaultValue: "[parameters('paramB')]"
488+
paramB:
489+
type: string
490+
defaultValue: "[parameters('paramC')]"
491+
paramC:
492+
type: string
493+
defaultValue: "[parameters('paramA')]"
494+
resources:
495+
- name: Echo
496+
type: Microsoft.DSC.Debug/Echo
497+
properties:
498+
output: "[parameters('paramA')]"
499+
"@
500+
501+
$testError = & {$config_yaml | dsc config get -f - 2>&1}
502+
$LASTEXITCODE | Should -Be 4
503+
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
504+
}
505+
506+
It 'Parameters with nested references can be overridden by input' {
507+
$config_yaml = @"
508+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
509+
parameters:
510+
basePrefix:
511+
type: string
512+
defaultValue: base
513+
computedPrefix:
514+
type: string
515+
defaultValue: "[concat(parameters('basePrefix'), '-computed')]"
516+
resources:
517+
- name: Echo
518+
type: Microsoft.DSC.Debug/Echo
519+
properties:
520+
output: "[parameters('computedPrefix')]"
521+
"@
522+
$params_json = @{ parameters = @{ basePrefix = 'override' }} | ConvertTo-Json
523+
524+
$out = $config_yaml | dsc config -p $params_json get -f - | ConvertFrom-Json
525+
$LASTEXITCODE | Should -Be 0
526+
$out.results[0].result.actualState.output | Should -BeExactly 'override-computed'
527+
}
528+
529+
It 'Parameters nested references work with different data types: <type>' -TestCases @(
530+
@{ type = 'string'; baseValue = 'test'; expectedOutput = 'prefix-test-suffix' }
531+
@{ type = 'int'; baseValue = 42; expectedOutput = 'value-42' }
532+
) {
533+
param($type, $baseValue, $expectedOutput)
534+
535+
$config_yaml = @"
536+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
537+
parameters:
538+
baseParam:
539+
type: $type
540+
defaultValue: $baseValue
541+
computedParam:
542+
type: string
543+
defaultValue: "[concat('prefix-', string(parameters('baseParam')), '-suffix')]"
544+
resources:
545+
- name: Echo
546+
type: Microsoft.DSC.Debug/Echo
547+
properties:
548+
output: "[parameters('computedParam')]"
549+
"@
550+
551+
if ($type -eq 'string') {
552+
$expectedOutput = 'prefix-test-suffix'
553+
} else {
554+
$expectedOutput = 'prefix-42-suffix'
555+
}
556+
557+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
558+
$LASTEXITCODE | Should -Be 0
559+
$out.results[0].result.actualState.output | Should -BeExactly $expectedOutput
560+
}
561+
562+
It 'Parameters with unresolvable references produce error' {
563+
$config_yaml = @"
564+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
565+
parameters:
566+
computedParam:
567+
type: string
568+
defaultValue: "[parameters('nonExistentParam')]"
569+
resources:
570+
- name: Echo
571+
type: Microsoft.DSC.Debug/Echo
572+
properties:
573+
output: "[parameters('computedParam')]"
574+
"@
575+
576+
$testError = & {$config_yaml | dsc config get -f - 2>&1}
577+
$LASTEXITCODE | Should -Be 4
578+
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
579+
}
408580
}

dsc_lib/locales/en-us.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ noParametersDefined = "No parameters defined in configuration"
5353
processingParameter = "Processing parameter '%{name}'"
5454
setDefaultParameter = "Set default parameter '%{name}'"
5555
defaultStringNotDefined = "Default value as string is not defined"
56-
noParametersInput = "No parameters input"
5756
setSecureParameter = "Set secure parameter '%{name}'"
5857
setParameter = "Set parameter '%{name}' to '%{value}'"
5958
parameterNotDefined = "Parameter '%{name}' is not defined in configuration"
@@ -77,6 +76,7 @@ copyModeNotSupported = "Copy mode is not supported"
7776
copyBatchSizeNotSupported = "Copy batch size is not supported"
7877
copyNameResultNotString = "Copy name result is not a string"
7978
nameResultNotString = "Resource name result is not a string"
79+
circularDependency = "Circular dependency or unresolvable parameter references detected in parameters: %{parameters}"
8080
userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined"
8181
addingUserFunction = "Adding user function '%{name}'"
8282

dsc_lib/src/configure/mod.rs

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::configure::config_doc::{ExecutionKind, Metadata, Resource};
4+
use crate::configure::config_doc::{ExecutionKind, Metadata, Resource, Parameter};
55
use crate::configure::context::{Context, ProcessMode};
66
use crate::configure::{config_doc::RestartRequired, parameters::Input};
77
use crate::discovery::discovery_trait::DiscoveryFilter;
@@ -767,7 +767,6 @@ impl Configurator {
767767
}
768768

769769
fn set_parameters(&mut self, parameters_input: Option<&Value>, config: &Configuration) -> Result<(), DscError> {
770-
// set default parameters first
771770
let Some(parameters) = &config.parameters else {
772771
if parameters_input.is_none() {
773772
info!("{}", t!("configure.mod.noParameters"));
@@ -776,66 +775,87 @@ impl Configurator {
776775
return Err(DscError::Validation(t!("configure.mod.noParametersDefined").to_string()));
777776
};
778777

779-
for (name, parameter) in parameters {
780-
debug!("{}", t!("configure.mod.processingParameter", name = name));
781-
if let Some(default_value) = &parameter.default_value {
782-
debug!("{}", t!("configure.mod.setDefaultParameter", name = name));
783-
// default values can be expressions
784-
let value = if default_value.is_string() {
785-
if let Some(value) = default_value.as_str() {
786-
self.context.process_mode = ProcessMode::ParametersDefault;
787-
let result = self.statement_parser.parse_and_execute(value, &self.context)?;
788-
self.context.process_mode = ProcessMode::Normal;
789-
result
778+
// process input parameters first
779+
if let Some(parameters_input) = parameters_input {
780+
trace!("parameters_input: {parameters_input}");
781+
let input_parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
782+
783+
for (name, value) in input_parameters {
784+
if let Some(constraint) = parameters.get(&name) {
785+
debug!("Validating parameter '{name}'");
786+
check_length(&name, &value, constraint)?;
787+
check_allowed_values(&name, &value, constraint)?;
788+
check_number_limits(&name, &value, constraint)?;
789+
// TODO: additional array constraints
790+
// TODO: object constraints
791+
792+
validate_parameter_type(&name, &value, &constraint.parameter_type)?;
793+
if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject {
794+
info!("{}", t!("configure.mod.setSecureParameter", name = name));
790795
} else {
791-
return Err(DscError::Parser(t!("configure.mod.defaultStringNotDefined").to_string()));
796+
info!("{}", t!("configure.mod.setParameter", name = name, value = value));
792797
}
793-
} else {
794-
default_value.clone()
795-
};
796-
validate_parameter_type(name, &value, &parameter.parameter_type)?;
797-
self.context.parameters.insert(name.clone(), (value, parameter.parameter_type.clone()));
798+
799+
self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
800+
if let Some(parameters) = &mut self.config.parameters {
801+
if let Some(parameter) = parameters.get_mut(&name) {
802+
parameter.default_value = Some(value);
803+
}
804+
}
805+
}
806+
else {
807+
return Err(DscError::Validation(t!("configure.mod.parameterNotDefined", name = name).to_string()));
808+
}
798809
}
799810
}
800811

801-
let Some(parameters_input) = parameters_input else {
802-
debug!("{}", t!("configure.mod.noParametersInput"));
803-
return Ok(());
804-
};
812+
// Now process default values for parameters that weren't provided in input
813+
let mut unresolved_parameters: HashMap<String, &Parameter> = parameters
814+
.iter()
815+
.filter(|(name, _)| !self.context.parameters.contains_key(*name))
816+
.map(|(k, v)| (k.clone(), v))
817+
.collect();
818+
819+
while !unresolved_parameters.is_empty() {
820+
let mut resolved_in_this_pass = Vec::new();
821+
822+
for (name, parameter) in &unresolved_parameters {
823+
debug!("{}", t!("configure.mod.processingParameter", name = name));
824+
if let Some(default_value) = &parameter.default_value {
825+
debug!("{}", t!("configure.mod.setDefaultParameter", name = name));
826+
let value_result = if default_value.is_string() {
827+
if let Some(value) = default_value.as_str() {
828+
self.context.process_mode = ProcessMode::ParametersDefault;
829+
let result = self.statement_parser.parse_and_execute(value, &self.context);
830+
self.context.process_mode = ProcessMode::Normal;
831+
result
832+
} else {
833+
return Err(DscError::Parser(t!("configure.mod.defaultStringNotDefined").to_string()));
834+
}
835+
} else {
836+
Ok(default_value.clone())
837+
};
805838

806-
trace!("parameters_input: {parameters_input}");
807-
let parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
808-
let Some(parameters_constraints) = &config.parameters else {
809-
return Err(DscError::Validation(t!("configure.mod.noParametersDefined").to_string()));
810-
};
811-
for (name, value) in parameters {
812-
if let Some(constraint) = parameters_constraints.get(&name) {
813-
debug!("Validating parameter '{name}'");
814-
check_length(&name, &value, constraint)?;
815-
check_allowed_values(&name, &value, constraint)?;
816-
check_number_limits(&name, &value, constraint)?;
817-
// TODO: additional array constraints
818-
// TODO: object constraints
819-
820-
validate_parameter_type(&name, &value, &constraint.parameter_type)?;
821-
if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject {
822-
info!("{}", t!("configure.mod.setSecureParameter", name = name));
839+
if let Ok(value) = value_result {
840+
validate_parameter_type(name, &value, &parameter.parameter_type)?;
841+
self.context.parameters.insert(name.to_string(), (value, parameter.parameter_type.clone()));
842+
resolved_in_this_pass.push(name.clone());
843+
}
823844
} else {
824-
info!("{}", t!("configure.mod.setParameter", name = name, value = value));
845+
resolved_in_this_pass.push(name.clone());
825846
}
847+
}
826848

827-
self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
828-
// also update the configuration with the parameter value
829-
if let Some(parameters) = &mut self.config.parameters {
830-
if let Some(parameter) = parameters.get_mut(&name) {
831-
parameter.default_value = Some(value);
832-
}
833-
}
849+
if resolved_in_this_pass.is_empty() {
850+
let unresolved_names: Vec<_> = unresolved_parameters.keys().map(std::string::String::as_str).collect();
851+
return Err(DscError::Validation(t!("configure.mod.circularDependency", parameters = unresolved_names.join(", ")).to_string()));
834852
}
835-
else {
836-
return Err(DscError::Validation(t!("configure.mod.parameterNotDefined", name = name).to_string()));
853+
854+
for name in &resolved_in_this_pass {
855+
unresolved_parameters.remove(name);
837856
}
838857
}
858+
839859
Ok(())
840860
}
841861

0 commit comments

Comments
 (0)