Skip to content

Commit 47479f7

Browse files
authored
Merge pull request #511 from SteveL-MSFT/variables
Add variables support
2 parents 3ea98ff + 0627ed9 commit 47479f7

File tree

10 files changed

+166
-9
lines changed

10 files changed

+166
-9
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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_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+
}

dsc_lib/src/parser/functions.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
use serde_json::{Number, Value};
5+
use tracing::debug;
56
use tree_sitter::Node;
67

78
use crate::DscError;
@@ -51,8 +52,10 @@ impl Function {
5152
return Err(DscError::Parser("Function name node not found".to_string()));
5253
};
5354
let args = convert_args_node(statement_bytes, &function_args)?;
55+
let name = name.utf8_text(statement_bytes)?;
56+
debug!("Function name: {0}", name);
5457
Ok(Function{
55-
name: name.utf8_text(statement_bytes)?.to_string(),
58+
name: name.to_string(),
5659
args})
5760
}
5861

@@ -68,10 +71,12 @@ impl Function {
6871
for arg in args {
6972
match arg {
7073
FunctionArg::Expression(expression) => {
74+
debug!("Arg is expression");
7175
let value = expression.invoke(function_dispatcher, context)?;
7276
resolved_args.push(value.clone());
7377
},
7478
FunctionArg::Value(value) => {
79+
debug!("Arg is value: '{:?}'", value);
7580
resolved_args.push(value.clone());
7681
}
7782
}

dsc_lib/src/parser/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,19 @@ impl Statement {
6969
let Ok(value) = child_node.utf8_text(statement_bytes) else {
7070
return Err(DscError::Parser("Error parsing string literal".to_string()));
7171
};
72+
debug!("Parsing string literal: {0}", value.to_string());
7273
Ok(Value::String(value.to_string()))
7374
},
7475
"escapedStringLiteral" => {
7576
// need to remove the first character: [[ => [
7677
let Ok(value) = child_node.utf8_text(statement_bytes) else {
7778
return Err(DscError::Parser("Error parsing escaped string literal".to_string()));
7879
};
80+
debug!("Parsing escaped string literal: {0}", value[1..].to_string());
7981
Ok(Value::String(value[1..].to_string()))
8082
},
8183
"expression" => {
84+
debug!("Parsing expression");
8285
let expression = Expression::new(statement_bytes, &child_node)?;
8386
Ok(expression.invoke(&self.function_dispatcher, context)?)
8487
},

0 commit comments

Comments
 (0)