From 7cd5b0462fb03028fa211e9e3fa2cf36d4487e87 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 7 Mar 2024 12:32:21 -0800 Subject: [PATCH 1/4] Add `reference()` function to get output from a resource --- dsc/examples/reference.dsc.yaml | 12 ++++++ dsc/tests/dsc_reference.tests.ps1 | 20 ++++++++++ dsc_lib/src/configure/context.rs | 4 +- dsc_lib/src/configure/mod.rs | 3 ++ dsc_lib/src/functions/mod.rs | 2 + dsc_lib/src/functions/reference.rs | 63 ++++++++++++++++++++++++++++++ dsc_lib/src/parser/expressions.rs | 4 ++ 7 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 dsc/examples/reference.dsc.yaml create mode 100644 dsc/tests/dsc_reference.tests.ps1 create mode 100644 dsc_lib/src/functions/reference.rs diff --git a/dsc/examples/reference.dsc.yaml b/dsc/examples/reference.dsc.yaml new file mode 100644 index 000000000..aa5977f01 --- /dev/null +++ b/dsc/examples/reference.dsc.yaml @@ -0,0 +1,12 @@ +# Simple example showing how to reference output from a resource to use in another +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +resources: +- name: os + type: Microsoft/OSInfo + properties: {} +- name: Echo + type: Test/Echo + properties: + output: "[concat('The OS is ', reference(resourceId('Microsoft/OSInfo','os')).actualState.family)]" + dependsOn: + - "[resourceId('Microsoft/OSInfo','os')]" diff --git a/dsc/tests/dsc_reference.tests.ps1 b/dsc/tests/dsc_reference.tests.ps1 new file mode 100644 index 000000000..3b106014b --- /dev/null +++ b/dsc/tests/dsc_reference.tests.ps1 @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Tests for config using reference function' { + It 'Reference works' { + $out = dsc config get -p $PSScriptRoot/../examples/reference.dsc.yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $os = if ($IsWindows) { + 'Windows' + } + elseif ($IsLinux) { + 'Linux' + } + else { + 'macOS' + } + + $out.results[1].result.actualState.Output | Should -BeExactly "The OS is $os" + } +} diff --git a/dsc_lib/src/configure/context.rs b/dsc_lib/src/configure/context.rs index 15cabf30d..bbafde83d 100644 --- a/dsc_lib/src/configure/context.rs +++ b/dsc_lib/src/configure/context.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; pub struct Context { pub parameters: HashMap, pub _variables: HashMap, - pub _outputs: HashMap, // This is eventually for References function to get output from resources + pub outputs: HashMap, // this is used by the `reference()` function to retrieve output } impl Context { @@ -16,7 +16,7 @@ impl Context { Self { parameters: HashMap::new(), _variables: HashMap::new(), - _outputs: HashMap::new(), + outputs: HashMap::new(), } } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index fea522b10..e5c064e3e 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -238,6 +238,7 @@ impl Configurator { let filter = add_metadata(&dsc_resource.kind, properties)?; trace!("filter: {filter}"); let get_result = dsc_resource.get(&filter)?; + self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&get_result)?); let resource_result = config_result::ResourceGetResult { name: resource.name.clone(), resource_type: resource.resource_type.clone(), @@ -276,6 +277,7 @@ impl Configurator { let desired = add_metadata(&dsc_resource.kind, properties)?; trace!("desired: {desired}"); let set_result = dsc_resource.set(&desired, skip_test)?; + self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?); let resource_result = config_result::ResourceSetResult { name: resource.name.clone(), resource_type: resource.resource_type.clone(), @@ -314,6 +316,7 @@ impl Configurator { let expected = add_metadata(&dsc_resource.kind, properties)?; trace!("expected: {expected}"); let test_result = dsc_resource.test(&expected)?; + self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&test_result)?); let resource_result = config_result::ResourceTestResult { name: resource.name.clone(), resource_type: resource.resource_type.clone(), diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 28eddc4b3..13a933fef 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -15,6 +15,7 @@ pub mod div; pub mod envvar; pub mod mul; pub mod parameters; +pub mod reference; pub mod resource_id; pub mod sub; @@ -66,6 +67,7 @@ impl FunctionDispatcher { functions.insert("envvar".to_string(), Box::new(envvar::Envvar{})); functions.insert("mul".to_string(), Box::new(mul::Mul{})); functions.insert("parameters".to_string(), Box::new(parameters::Parameters{})); + 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{})); Self { diff --git a/dsc_lib/src/functions/reference.rs b/dsc_lib/src/functions/reference.rs new file mode 100644 index 000000000..a3ffe9ef7 --- /dev/null +++ b/dsc_lib/src/functions/reference.rs @@ -0,0 +1,63 @@ +// 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 Reference {} + +impl Function for Reference { + 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!("reference function"); + if let Some(key) = args[0].as_str() { + if context.outputs.contains_key(key) { + Ok(context.outputs[key].clone()) + } else { + Err(DscError::Parser(format!( + "Invalid resourceId or resource has not executed yet: {}", + key + ))) + } + } else { + Err(DscError::Parser("Invalid argument".to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn valid_resourceid() { + let mut parser = Statement::new().unwrap(); + let mut context = Context::new(); + context.outputs.insert("foo:bar".to_string(), "baz".into()); + let result = parser.parse_and_execute("[referene('foo:bar')]", &context).unwrap(); + assert_eq!(result, "baz"); + } + + #[test] + fn invalid_resourceid() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[referene('foo:bar')]", &Context::new()); + assert!(result.is_err()); + } +} diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index 3610fc16b..dab0704e0 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -73,6 +73,10 @@ impl Expression { let mut value = result; for member in member_access { + if member == "." { + continue; + } + if !value.is_object() { return Err(DscError::Parser(format!("Member access '{member}' on non-object value"))); } From 70b30556b2397835c4e1216a7f46e558f8900e85 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 7 Mar 2024 12:51:24 -0800 Subject: [PATCH 2/4] fix clippy --- dsc_lib/src/functions/reference.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dsc_lib/src/functions/reference.rs b/dsc_lib/src/functions/reference.rs index a3ffe9ef7..458eff831 100644 --- a/dsc_lib/src/functions/reference.rs +++ b/dsc_lib/src/functions/reference.rs @@ -29,10 +29,7 @@ impl Function for Reference { if context.outputs.contains_key(key) { Ok(context.outputs[key].clone()) } else { - Err(DscError::Parser(format!( - "Invalid resourceId or resource has not executed yet: {}", - key - ))) + Err(DscError::Parser(format!("Invalid resourceId or resource has not executed yet: {key}"))) } } else { Err(DscError::Parser("Invalid argument".to_string())) From 3f219929b169ea039cd446f7dbefcdc16dfe40d5 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 7 Mar 2024 13:34:40 -0800 Subject: [PATCH 3/4] fix unit test --- dsc_lib/src/functions/reference.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/functions/reference.rs b/dsc_lib/src/functions/reference.rs index 458eff831..4f3f8b08d 100644 --- a/dsc_lib/src/functions/reference.rs +++ b/dsc_lib/src/functions/reference.rs @@ -47,14 +47,14 @@ mod tests { let mut parser = Statement::new().unwrap(); let mut context = Context::new(); context.outputs.insert("foo:bar".to_string(), "baz".into()); - let result = parser.parse_and_execute("[referene('foo:bar')]", &context).unwrap(); + let result = parser.parse_and_execute("[reference('foo:bar')]", &context).unwrap(); assert_eq!(result, "baz"); } #[test] fn invalid_resourceid() { let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[referene('foo:bar')]", &Context::new()); + let result = parser.parse_and_execute("[reference('foo:bar')]", &Context::new()); assert!(result.is_err()); } } From 298c663669e7dfe2b9789a6dbe979b89e487259c Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 7 Mar 2024 14:23:48 -0800 Subject: [PATCH 4/4] condense grammar for member access and fix parser to not include period --- dsc_lib/src/parser/expressions.rs | 6 +----- tree-sitter-dscexpression/grammar.js | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index dab0704e0..b16cc059e 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -39,7 +39,7 @@ impl Expression { } let mut result = vec![]; let mut cursor = members.walk(); - for member in members.children(&mut cursor) { + for member in members.named_children(&mut cursor) { if member.is_error() { return Err(DscError::Parser("Error parsing dot-notation member".to_string())); } @@ -73,10 +73,6 @@ impl Expression { let mut value = result; for member in member_access { - if member == "." { - continue; - } - if !value.is_object() { return Err(DscError::Parser(format!("Member access '{member}' on non-object value"))); } diff --git a/tree-sitter-dscexpression/grammar.js b/tree-sitter-dscexpression/grammar.js index b32d1fc9b..754917b29 100644 --- a/tree-sitter-dscexpression/grammar.js +++ b/tree-sitter-dscexpression/grammar.js @@ -32,8 +32,7 @@ module.exports = grammar({ number: $ => /-?\d+/, boolean: $ => choice('true', 'false'), - memberAccess: $ => repeat1($._member), - _member: $ => seq('.', $.memberName), + memberAccess: $ => seq('.', $.memberName, repeat(seq('.', $.memberName))), memberName: $ => /[a-zA-Z0-9_-]+/, }