diff --git a/dsc/examples/variables.dsc.yaml b/dsc/examples/variables.dsc.yaml new file mode 100644 index 00000000..9b70dade --- /dev/null +++ b/dsc/examples/variables.dsc.yaml @@ -0,0 +1,15 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +parameters: + myParameter: + type: string + # the use of `concat()` here is just an example of using an expression for a defaultValue + defaultValue: "[concat('world','!')]" +variables: + myOutput: "[concat('Hello ', parameters('myParameter'))]" + myObject: + test: baz +resources: +- name: test + type: Test/Echo + properties: + output: "[concat('myOutput is: ', variables('myOutput'), ', myObject is: ', variables('myObject').test)]" diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index fc264383..ea64dce5 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -270,7 +270,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: } }; - if let Err(err) = configurator.set_parameters(¶meters) { + if let Err(err) = configurator.set_context(¶meters) { error!("Error: Parameter input failure: {err}"); exit(EXIT_INVALID_INPUT); } diff --git a/dsc/tests/dsc_variables.tests.ps1 b/dsc/tests/dsc_variables.tests.ps1 new file mode 100644 index 00000000..48b4999a --- /dev/null +++ b/dsc/tests/dsc_variables.tests.ps1 @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Configruation variables tests' { + It 'Variables example config works' { + $configFile = "$PSSCriptRoot/../examples/variables.dsc.yaml" + $out = dsc config get -p $configFile | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'myOutput is: Hello world!, myObject is: baz' + } + + It 'Duplicated variable takes last value' { + $configYaml = @' +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +variables: + myVariable: foo + myVariable: bar +resources: +- name: test + type: Test/Echo + properties: + output: "[variables('myVariable')]" +'@ + $out = dsc config get -d $configYaml | ConvertFrom-Json + Write-Verbose -Verbose $out + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -Be 'bar' + } + + It 'Missing variable returns error' { + $configYaml = @' +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +variables: + hello: world +resources: +- name: test + type: Test/Echo + properties: + output: "[variables('myVariable')]" +'@ + $out = dsc config get -d $configYaml 2>&1 | Out-String + Write-Verbose -Verbose $out + $LASTEXITCODE | Should -Be 2 + $out | Should -BeLike "*Variable 'myVariable' does not exist or has not been initialized yet*" + } +} diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 693b86ee..ceb4ea84 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::collections::HashMap; +use std::{collections::HashMap, hash::Hash}; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub enum ContextKind { diff --git a/dsc_lib/src/configure/context.rs b/dsc_lib/src/configure/context.rs index 6fd52f72..0ce678fe 100644 --- a/dsc_lib/src/configure/context.rs +++ b/dsc_lib/src/configure/context.rs @@ -14,7 +14,7 @@ pub struct Context { pub outputs: HashMap, // this is used by the `reference()` function to retrieve output pub parameters: HashMap, pub security_context: SecurityContextKind, - _variables: HashMap, + pub variables: HashMap, pub start_datetime: DateTime, } @@ -29,7 +29,7 @@ impl Context { SecurityContext::Admin => SecurityContextKind::Elevated, SecurityContext::User => SecurityContextKind::Restricted, }, - _variables: HashMap::new(), + variables: HashMap::new(), start_datetime: chrono::Local::now(), } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 2678104f..3a5d84a3 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -479,7 +479,7 @@ impl Configurator { Ok(result) } - /// Set the parameters context for the configuration. + /// Set the parameters and variables context for the configuration. /// /// # Arguments /// @@ -488,12 +488,18 @@ impl Configurator { /// # Errors /// /// This function will return an error if the parameters are invalid. - pub fn set_parameters(&mut self, parameters_input: &Option) -> Result<(), DscError> { - // set default parameters first + pub fn set_context(&mut self, parameters_input: &Option) -> Result<(), DscError> { let config = serde_json::from_str::(self.json.as_str())?; + self.set_parameters(parameters_input, &config)?; + self.set_variables(&config)?; + Ok(()) + } + + fn set_parameters(&mut self, parameters_input: &Option, config: &Configuration) -> Result<(), DscError> { + // set default parameters first let Some(parameters) = &config.parameters else { if parameters_input.is_none() { - debug!("No parameters defined in configuration and no parameters input"); + info!("No parameters defined in configuration and no parameters input"); return Ok(()); } return Err(DscError::Validation("No parameters defined in configuration".to_string())); @@ -543,6 +549,7 @@ impl Configurator { } else { info!("Set parameter '{name}' to '{value}'"); } + 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 { @@ -558,6 +565,25 @@ impl Configurator { Ok(()) } + fn set_variables(&mut self, config: &Configuration) -> Result<(), DscError> { + let Some(variables) = &config.variables else { + debug!("No variables defined in configuration"); + return Ok(()); + }; + + for (name, value) in variables { + let new_value = if let Some(string) = value.as_str() { + self.statement_parser.parse_and_execute(string, &self.context)? + } + else { + value.clone() + }; + info!("Set variable '{name}' to '{new_value}'"); + self.context.variables.insert(name.to_string(), new_value); + } + Ok(()) + } + fn get_result_metadata(&self, operation: Operation) -> Metadata { let end_datetime = chrono::Local::now(); Metadata { diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 205c22d4..5d631877 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -22,6 +22,7 @@ pub mod parameters; pub mod reference; pub mod resource_id; pub mod sub; +pub mod variables; /// The kind of argument that a function accepts. #[derive(Debug, PartialEq)] @@ -78,6 +79,7 @@ impl FunctionDispatcher { functions.insert("reference".to_string(), Box::new(reference::Reference{})); functions.insert("resourceId".to_string(), Box::new(resource_id::ResourceId{})); functions.insert("sub".to_string(), Box::new(sub::Sub{})); + functions.insert("variables".to_string(), Box::new(variables::Variables{})); Self { functions, } diff --git a/dsc_lib/src/functions/variables.rs b/dsc_lib/src/functions/variables.rs new file mode 100644 index 00000000..9aac9e54 --- /dev/null +++ b/dsc_lib/src/functions/variables.rs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{AcceptedArgKind, Function}; +use serde_json::Value; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Variables {} + +impl Function for Variables { + fn min_args(&self) -> usize { + 1 + } + + fn max_args(&self) -> usize { + 1 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], context: &Context) -> Result { + debug!("variables function"); + if let Some(key) = args[0].as_str() { + if context.variables.contains_key(key) { + Ok(context.variables[key].clone()) + } else { + Err(DscError::Parser(format!("Variable '{key}' does not exist or has not been initialized yet"))) + } + } else { + Err(DscError::Parser("Invalid argument".to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn valid_variable() { + let mut parser = Statement::new().unwrap(); + let mut context = Context::new(); + context.variables.insert("hello".to_string(), "world".into()); + let result = parser.parse_and_execute("[variables('hello')]", &context).unwrap(); + assert_eq!(result, "world"); + } + + #[test] + fn invalid_resourceid() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[variables('foo')]", &Context::new()); + assert!(result.is_err()); + } +} diff --git a/dsc_lib/src/parser/functions.rs b/dsc_lib/src/parser/functions.rs index 0c7409eb..839ac28a 100644 --- a/dsc_lib/src/parser/functions.rs +++ b/dsc_lib/src/parser/functions.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use serde_json::{Number, Value}; +use tracing::debug; use tree_sitter::Node; use crate::DscError; @@ -51,8 +52,10 @@ impl Function { return Err(DscError::Parser("Function name node not found".to_string())); }; let args = convert_args_node(statement_bytes, &function_args)?; + let name = name.utf8_text(statement_bytes)?; + debug!("Function name: {0}", name); Ok(Function{ - name: name.utf8_text(statement_bytes)?.to_string(), + name: name.to_string(), args}) } @@ -68,10 +71,12 @@ impl Function { for arg in args { match arg { FunctionArg::Expression(expression) => { + debug!("Arg is expression"); let value = expression.invoke(function_dispatcher, context)?; resolved_args.push(value.clone()); }, FunctionArg::Value(value) => { + debug!("Arg is value: '{:?}'", value); resolved_args.push(value.clone()); } } diff --git a/dsc_lib/src/parser/mod.rs b/dsc_lib/src/parser/mod.rs index 950b0ec5..d88540ab 100644 --- a/dsc_lib/src/parser/mod.rs +++ b/dsc_lib/src/parser/mod.rs @@ -69,6 +69,7 @@ impl Statement { let Ok(value) = child_node.utf8_text(statement_bytes) else { return Err(DscError::Parser("Error parsing string literal".to_string())); }; + debug!("Parsing string literal: {0}", value.to_string()); Ok(Value::String(value.to_string())) }, "escapedStringLiteral" => { @@ -76,9 +77,11 @@ impl Statement { let Ok(value) = child_node.utf8_text(statement_bytes) else { return Err(DscError::Parser("Error parsing escaped string literal".to_string())); }; + debug!("Parsing escaped string literal: {0}", value[1..].to_string()); Ok(Value::String(value[1..].to_string())) }, "expression" => { + debug!("Parsing expression"); let expression = Expression::new(statement_bytes, &child_node)?; Ok(expression.invoke(&self.function_dispatcher, context)?) },