Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variables support #511

Merged
merged 3 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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','!')]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a difference between parameters and variables:
parameters have 2 subfields: type and defaultValue,
but looks like variables have just implied defaultValue and type is implicitly derived.
Should this be consistent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took this directly from ARM. variables are intended to just be used to keep values that get reused multiple times within the config and typically constructed from params based on examples I saw.

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' {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The essence of a variable in any language is that it's value can be changed; I don't see such example or tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, but that's not how it's used in an ARM template

$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
Loading