Skip to content

Commit

Permalink
Merge pull request #511 from SteveL-MSFT/variables
Browse files Browse the repository at this point in the history
Add variables support
  • Loading branch information
SteveL-MSFT authored Aug 8, 2024
2 parents 3ea98ff + 0627ed9 commit 47479f7
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 9 deletions.
15 changes: 15 additions & 0 deletions dsc/examples/variables.dsc.yaml
Original file line number Diff line number Diff line change
@@ -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)]"
2 changes: 1 addition & 1 deletion dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
}
};

if let Err(err) = configurator.set_parameters(&parameters) {
if let Err(err) = configurator.set_context(&parameters) {
error!("Error: Parameter input failure: {err}");
exit(EXIT_INVALID_INPUT);
}
Expand Down
46 changes: 46 additions & 0 deletions dsc/tests/dsc_variables.tests.ps1
Original file line number Diff line number Diff line change
@@ -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*"
}
}
2 changes: 1 addition & 1 deletion dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct Context {
pub outputs: HashMap<String, Value>, // this is used by the `reference()` function to retrieve output
pub parameters: HashMap<String, (Value, DataType)>,
pub security_context: SecurityContextKind,
_variables: HashMap<String, Value>,
pub variables: HashMap<String, Value>,
pub start_datetime: DateTime<Local>,
}

Expand All @@ -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(),
}
}
Expand Down
34 changes: 30 additions & 4 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand All @@ -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<Value>) -> Result<(), DscError> {
// set default parameters first
pub fn set_context(&mut self, parameters_input: &Option<Value>) -> Result<(), DscError> {
let config = serde_json::from_str::<Configuration>(self.json.as_str())?;
self.set_parameters(parameters_input, &config)?;
self.set_variables(&config)?;
Ok(())
}

fn set_parameters(&mut self, parameters_input: &Option<Value>, 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()));
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions dsc_lib/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
}
Expand Down
60 changes: 60 additions & 0 deletions dsc_lib/src/functions/variables.rs
Original file line number Diff line number Diff line change
@@ -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<AcceptedArgKind> {
vec![AcceptedArgKind::String]
}

fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
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());
}
}
7 changes: 6 additions & 1 deletion dsc_lib/src/parser/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use serde_json::{Number, Value};
use tracing::debug;
use tree_sitter::Node;

use crate::DscError;
Expand Down Expand Up @@ -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})
}

Expand All @@ -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());
}
}
Expand Down
3 changes: 3 additions & 0 deletions dsc_lib/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,19 @@ 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" => {
// need to remove the first character: [[ => [
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)?)
},
Expand Down

0 comments on commit 47479f7

Please sign in to comment.