Skip to content

Commit 89c9578

Browse files
committed
Fix nested parameters not found
1 parent 784daea commit 89c9578

File tree

3 files changed

+248
-50
lines changed

3 files changed

+248
-50
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ copyModeNotSupported = "Copy mode is not supported"
7878
copyBatchSizeNotSupported = "Copy batch size is not supported"
7979
copyNameResultNotString = "Copy name result is not a string"
8080
nameResultNotString = "Resource name result is not a string"
81+
circularDependency = "Circular dependency or unresolvable parameter references detected in parameters: %{parameters}"
8182
userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined"
8283
addingUserFunction = "Adding user function '%{name}'"
8384

dsc_lib/src/configure/mod.rs

Lines changed: 75 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;
@@ -757,7 +757,6 @@ impl Configurator {
757757
}
758758

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

769-
for (name, parameter) in parameters {
770-
debug!("{}", t!("configure.mod.processingParameter", name = name));
771-
if let Some(default_value) = &parameter.default_value {
772-
debug!("{}", t!("configure.mod.setDefaultParameter", name = name));
773-
// default values can be expressions
774-
let value = if default_value.is_string() {
775-
if let Some(value) = default_value.as_str() {
776-
self.context.process_mode = ProcessMode::ParametersDefault;
777-
let result = self.statement_parser.parse_and_execute(value, &self.context)?;
778-
self.context.process_mode = ProcessMode::Normal;
779-
result
768+
// process input parameters first
769+
if let Some(parameters_input) = parameters_input {
770+
trace!("parameters_input: {parameters_input}");
771+
let input_parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
772+
773+
for (name, value) in input_parameters {
774+
if let Some(constraint) = parameters.get(&name) {
775+
debug!("Validating parameter '{name}'");
776+
check_length(&name, &value, constraint)?;
777+
check_allowed_values(&name, &value, constraint)?;
778+
check_number_limits(&name, &value, constraint)?;
779+
// TODO: additional array constraints
780+
// TODO: object constraints
781+
782+
validate_parameter_type(&name, &value, &constraint.parameter_type)?;
783+
if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject {
784+
info!("{}", t!("configure.mod.setSecureParameter", name = name));
780785
} else {
781-
return Err(DscError::Parser(t!("configure.mod.defaultStringNotDefined").to_string()));
786+
info!("{}", t!("configure.mod.setParameter", name = name, value = value));
782787
}
783-
} else {
784-
default_value.clone()
785-
};
786-
validate_parameter_type(name, &value, &parameter.parameter_type)?;
787-
self.context.parameters.insert(name.clone(), (value, parameter.parameter_type.clone()));
788+
789+
self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
790+
if let Some(parameters) = &mut self.config.parameters {
791+
if let Some(parameter) = parameters.get_mut(&name) {
792+
parameter.default_value = Some(value);
793+
}
794+
}
795+
}
796+
else {
797+
return Err(DscError::Validation(t!("configure.mod.parameterNotDefined", name = name).to_string()));
798+
}
788799
}
789800
}
790801

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

796-
trace!("parameters_input: {parameters_input}");
797-
let parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
798-
let Some(parameters_constraints) = &config.parameters else {
799-
return Err(DscError::Validation(t!("configure.mod.noParametersDefined").to_string()));
800-
};
801-
for (name, value) in parameters {
802-
if let Some(constraint) = parameters_constraints.get(&name) {
803-
debug!("Validating parameter '{name}'");
804-
check_length(&name, &value, constraint)?;
805-
check_allowed_values(&name, &value, constraint)?;
806-
check_number_limits(&name, &value, constraint)?;
807-
// TODO: additional array constraints
808-
// TODO: object constraints
809-
810-
validate_parameter_type(&name, &value, &constraint.parameter_type)?;
811-
if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject {
812-
info!("{}", t!("configure.mod.setSecureParameter", name = name));
829+
match value_result {
830+
Ok(value) => {
831+
validate_parameter_type(name, &value, &parameter.parameter_type)?;
832+
self.context.parameters.insert(name.to_string(), (value, parameter.parameter_type.clone()));
833+
resolved_in_this_pass.push(name.clone());
834+
}
835+
Err(_) => {
836+
continue;
837+
}
838+
}
813839
} else {
814-
info!("{}", t!("configure.mod.setParameter", name = name, value = value));
840+
resolved_in_this_pass.push(name.clone());
815841
}
842+
}
816843

817-
self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
818-
// also update the configuration with the parameter value
819-
if let Some(parameters) = &mut self.config.parameters {
820-
if let Some(parameter) = parameters.get_mut(&name) {
821-
parameter.default_value = Some(value);
822-
}
823-
}
844+
if resolved_in_this_pass.is_empty() {
845+
let unresolved_names: Vec<_> = unresolved_parameters.keys().map(|k| k.as_str()).collect();
846+
return Err(DscError::Validation(t!("configure.mod.circularDependency", parameters = unresolved_names.join(", ")).to_string()));
824847
}
825-
else {
826-
return Err(DscError::Validation(t!("configure.mod.parameterNotDefined", name = name).to_string()));
848+
849+
for name in &resolved_in_this_pass {
850+
unresolved_parameters.remove(name);
827851
}
828852
}
853+
829854
Ok(())
830855
}
831856

0 commit comments

Comments
 (0)