diff --git a/dsc/examples/reference.dsc.yaml b/dsc/examples/reference.dsc.yaml new file mode 100644 index 00000000..aa5977f0 --- /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 00000000..3b106014 --- /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 15cabf30..bbafde83 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 8c116eda..89c1c8ef 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -240,6 +240,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(), @@ -280,6 +281,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(), @@ -320,6 +322,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 0accfa02..71992ab3 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -16,6 +16,7 @@ pub mod envvar; pub mod mod_function; pub mod mul; pub mod parameters; +pub mod reference; pub mod resource_id; pub mod sub; @@ -68,6 +69,7 @@ impl FunctionDispatcher { functions.insert("mod".to_string(), Box::new(mod_function::Mod{})); 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 00000000..4f3f8b08 --- /dev/null +++ b/dsc_lib/src/functions/reference.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 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("[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("[reference('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 3610fc16..b16cc059 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())); } diff --git a/tree-sitter-dscexpression/grammar.js b/tree-sitter-dscexpression/grammar.js index b32d1fc9..754917b2 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_-]+/, }