Skip to content

Commit 6ce773f

Browse files
authored
Merge branch 'PowerShell:main' into main
2 parents ff492c8 + 45789c5 commit 6ce773f

File tree

20 files changed

+1888
-326
lines changed

20 files changed

+1888
-326
lines changed

dsc/examples/variables.dsc.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
2+
parameters:
3+
myParameter:
4+
type: string
5+
# the use of `concat()` here is just an example of using an expression for a defaultValue
6+
defaultValue: "[concat('world','!')]"
7+
variables:
8+
myOutput: "[concat('Hello ', parameters('myParameter'))]"
9+
myObject:
10+
test: baz
11+
resources:
12+
- name: test
13+
type: Test/Echo
14+
properties:
15+
output: "[concat('myOutput is: ', variables('myOutput'), ', myObject is: ', variables('myObject').test)]"

dsc/src/subcommand.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn config_get(configurator: &mut Configurator, format: &Option<OutputFormat>
4949
}
5050
},
5151
Err(err) => {
52-
error!("Error: {err}");
52+
error!("{err}");
5353
exit(EXIT_DSC_ERROR);
5454
}
5555
}
@@ -270,7 +270,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
270270
}
271271
};
272272

273-
if let Err(err) = configurator.set_parameters(&parameters) {
273+
if let Err(err) = configurator.set_context(&parameters) {
274274
error!("Error: Parameter input failure: {err}");
275275
exit(EXIT_INVALID_INPUT);
276276
}

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Expressions tests' {
5+
It 'Accessors work: <text>' -TestCases @(
6+
@{ text = "[parameters('test').hello]"; expected = '@{world=there}' }
7+
@{ text = "[parameters('test').hello.world]"; expected = 'there' }
8+
@{ text = "[parameters('test').array[0]]"; expected = 'one' }
9+
@{ text = "[parameters('test').array[1][1]]"; expected = 'three' }
10+
@{ text = "[parameters('test').objectArray[0].name]"; expected = 'one' }
11+
@{ text = "[parameters('test').objectArray[1].value[0]]"; expected = '2' }
12+
@{ text = "[parameters('test').objectArray[1].value[1].name]"; expected = 'three' }
13+
) {
14+
param($text, $expected)
15+
$yaml = @"
16+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
17+
parameters:
18+
test:
19+
type: object
20+
defaultValue:
21+
hello:
22+
world: there
23+
array:
24+
- one
25+
- [ 'two', 'three' ]
26+
objectArray:
27+
- name: one
28+
value: 1
29+
- name: two
30+
value:
31+
- 2
32+
- nestedObject:
33+
name: three
34+
value: 3
35+
resources:
36+
- name: echo
37+
type: Test/Echo
38+
properties:
39+
output: "$text"
40+
"@
41+
$debug = $yaml | dsc -l debug config get -f yaml 2>&1 | Out-String
42+
$out = $yaml | dsc config get | ConvertFrom-Json
43+
$LASTEXITCODE | Should -Be 0
44+
$out.results[0].result.actualState.output | Should -Be $expected -Because $debug
45+
}
46+
47+
It 'Invalid expressions: <expression>' -TestCases @(
48+
@{ expression = "[concat('A','B')].hello" }
49+
@{ expression = "[concat('A','B')](0)" }
50+
@{ expression = "[concat('a','b').hello]" }
51+
@{ expression = "[concat('a','b')[0]]" }
52+
) {
53+
param($expression)
54+
$yaml = @"
55+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
56+
resources:
57+
- name: echo
58+
type: Test/Echo
59+
properties:
60+
output: "$expression"
61+
"@
62+
$out = dsc config get -d $yaml 2>&1
63+
$LASTEXITCODE | Should -Be 2
64+
$out | Should -BeLike "*ERROR*"
65+
}
66+
}

dsc/tests/dsc_variables.tests.ps1

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Configruation variables tests' {
5+
It 'Variables example config works' {
6+
$configFile = "$PSSCriptRoot/../examples/variables.dsc.yaml"
7+
$out = dsc config get -p $configFile | ConvertFrom-Json
8+
$LASTEXITCODE | Should -Be 0
9+
$out.results[0].result.actualState.output | Should -BeExactly 'myOutput is: Hello world!, myObject is: baz'
10+
}
11+
12+
It 'Duplicated variable takes last value' {
13+
$configYaml = @'
14+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
15+
variables:
16+
myVariable: foo
17+
myVariable: bar
18+
resources:
19+
- name: test
20+
type: Test/Echo
21+
properties:
22+
output: "[variables('myVariable')]"
23+
'@
24+
$out = dsc config get -d $configYaml | ConvertFrom-Json
25+
Write-Verbose -Verbose $out
26+
$LASTEXITCODE | Should -Be 0
27+
$out.results[0].result.actualState.output | Should -Be 'bar'
28+
}
29+
30+
It 'Missing variable returns error' {
31+
$configYaml = @'
32+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
33+
variables:
34+
hello: world
35+
resources:
36+
- name: test
37+
type: Test/Echo
38+
properties:
39+
output: "[variables('myVariable')]"
40+
'@
41+
$out = dsc config get -d $configYaml 2>&1 | Out-String
42+
Write-Verbose -Verbose $out
43+
$LASTEXITCODE | Should -Be 2
44+
$out | Should -BeLike "*Variable 'myVariable' does not exist or has not been initialized yet*"
45+
}
46+
}

dsc_lib/src/configure/config_doc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use schemars::JsonSchema;
55
use serde::{Deserialize, Serialize};
66
use serde_json::{Map, Value};
7-
use std::collections::HashMap;
7+
use std::{collections::HashMap, hash::Hash};
88

99
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
1010
pub enum ContextKind {

dsc_lib/src/configure/context.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct Context {
1414
pub outputs: HashMap<String, Value>, // this is used by the `reference()` function to retrieve output
1515
pub parameters: HashMap<String, (Value, DataType)>,
1616
pub security_context: SecurityContextKind,
17-
_variables: HashMap<String, Value>,
17+
pub variables: HashMap<String, Value>,
1818
pub start_datetime: DateTime<Local>,
1919
}
2020

@@ -29,7 +29,7 @@ impl Context {
2929
SecurityContext::Admin => SecurityContextKind::Elevated,
3030
SecurityContext::User => SecurityContextKind::Restricted,
3131
},
32-
_variables: HashMap::new(),
32+
variables: HashMap::new(),
3333
start_datetime: chrono::Local::now(),
3434
}
3535
}

dsc_lib/src/configure/mod.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ impl Configurator {
479479
Ok(result)
480480
}
481481

482-
/// Set the parameters context for the configuration.
482+
/// Set the parameters and variables context for the configuration.
483483
///
484484
/// # Arguments
485485
///
@@ -488,12 +488,18 @@ impl Configurator {
488488
/// # Errors
489489
///
490490
/// This function will return an error if the parameters are invalid.
491-
pub fn set_parameters(&mut self, parameters_input: &Option<Value>) -> Result<(), DscError> {
492-
// set default parameters first
491+
pub fn set_context(&mut self, parameters_input: &Option<Value>) -> Result<(), DscError> {
493492
let config = serde_json::from_str::<Configuration>(self.json.as_str())?;
493+
self.set_parameters(parameters_input, &config)?;
494+
self.set_variables(&config)?;
495+
Ok(())
496+
}
497+
498+
fn set_parameters(&mut self, parameters_input: &Option<Value>, config: &Configuration) -> Result<(), DscError> {
499+
// set default parameters first
494500
let Some(parameters) = &config.parameters else {
495501
if parameters_input.is_none() {
496-
debug!("No parameters defined in configuration and no parameters input");
502+
info!("No parameters defined in configuration and no parameters input");
497503
return Ok(());
498504
}
499505
return Err(DscError::Validation("No parameters defined in configuration".to_string()));
@@ -543,6 +549,7 @@ impl Configurator {
543549
} else {
544550
info!("Set parameter '{name}' to '{value}'");
545551
}
552+
546553
self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone()));
547554
// also update the configuration with the parameter value
548555
if let Some(parameters) = &mut self.config.parameters {
@@ -558,6 +565,25 @@ impl Configurator {
558565
Ok(())
559566
}
560567

568+
fn set_variables(&mut self, config: &Configuration) -> Result<(), DscError> {
569+
let Some(variables) = &config.variables else {
570+
debug!("No variables defined in configuration");
571+
return Ok(());
572+
};
573+
574+
for (name, value) in variables {
575+
let new_value = if let Some(string) = value.as_str() {
576+
self.statement_parser.parse_and_execute(string, &self.context)?
577+
}
578+
else {
579+
value.clone()
580+
};
581+
info!("Set variable '{name}' to '{new_value}'");
582+
self.context.variables.insert(name.to_string(), new_value);
583+
}
584+
Ok(())
585+
}
586+
561587
fn get_result_metadata(&self, operation: Operation) -> Metadata {
562588
let end_datetime = chrono::Local::now();
563589
Metadata {

dsc_lib/src/functions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod parameters;
2222
pub mod reference;
2323
pub mod resource_id;
2424
pub mod sub;
25+
pub mod variables;
2526

2627
/// The kind of argument that a function accepts.
2728
#[derive(Debug, PartialEq)]
@@ -78,6 +79,7 @@ impl FunctionDispatcher {
7879
functions.insert("reference".to_string(), Box::new(reference::Reference{}));
7980
functions.insert("resourceId".to_string(), Box::new(resource_id::ResourceId{}));
8081
functions.insert("sub".to_string(), Box::new(sub::Sub{}));
82+
functions.insert("variables".to_string(), Box::new(variables::Variables{}));
8183
Self {
8284
functions,
8385
}

dsc_lib/src/functions/variables.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{AcceptedArgKind, Function};
7+
use serde_json::Value;
8+
use tracing::debug;
9+
10+
#[derive(Debug, Default)]
11+
pub struct Variables {}
12+
13+
impl Function for Variables {
14+
fn min_args(&self) -> usize {
15+
1
16+
}
17+
18+
fn max_args(&self) -> usize {
19+
1
20+
}
21+
22+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
23+
vec![AcceptedArgKind::String]
24+
}
25+
26+
fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
27+
debug!("variables function");
28+
if let Some(key) = args[0].as_str() {
29+
if context.variables.contains_key(key) {
30+
Ok(context.variables[key].clone())
31+
} else {
32+
Err(DscError::Parser(format!("Variable '{key}' does not exist or has not been initialized yet")))
33+
}
34+
} else {
35+
Err(DscError::Parser("Invalid argument".to_string()))
36+
}
37+
}
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use crate::configure::context::Context;
43+
use crate::parser::Statement;
44+
45+
#[test]
46+
fn valid_variable() {
47+
let mut parser = Statement::new().unwrap();
48+
let mut context = Context::new();
49+
context.variables.insert("hello".to_string(), "world".into());
50+
let result = parser.parse_and_execute("[variables('hello')]", &context).unwrap();
51+
assert_eq!(result, "world");
52+
}
53+
54+
#[test]
55+
fn invalid_resourceid() {
56+
let mut parser = Statement::new().unwrap();
57+
let result = parser.parse_and_execute("[variables('foo')]", &Context::new());
58+
assert!(result.is_err());
59+
}
60+
}

0 commit comments

Comments
 (0)