From 73d4cbfe8470846a615826fd42ba677e0aa28c2b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 25 Jul 2025 07:47:15 -0700 Subject: [PATCH 1/3] update config schema for output --- dsc_lib/src/configure/config_doc.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 88b2a4df..4b2bf4c0 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -105,6 +105,21 @@ pub struct Metadata { pub other: Map, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum ValueOrCopy { + Value(String), + Copy(Copy), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Output { + pub condition: Option, + pub r#type: DataType, + pub value_or_copy: ValueOrCopy, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Configuration { @@ -120,9 +135,12 @@ pub struct Configuration { pub resources: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct Parameter { #[serde(rename = "type")] pub parameter_type: DataType, @@ -300,6 +318,7 @@ impl Configuration { variables: None, resources: Vec::new(), metadata: None, + outputs: None, } } } From 2ea358f9e8e4a8c47074744fa8d5838072d6f4da Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 26 Jul 2025 17:49:27 -0700 Subject: [PATCH 2/3] connect output to result --- dsc_lib/locales/en-us.toml | 5 +++ dsc_lib/src/configure/config_doc.rs | 15 +++++++ dsc_lib/src/configure/config_result.rs | 14 +++++++ dsc_lib/src/configure/context.rs | 2 + dsc_lib/src/configure/mod.rs | 58 +++++++++++++++++++++++++- 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index b0f330aa..718e8149 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -71,6 +71,11 @@ propertyNotString = "Property '%{name}' with value '%{value}' is not a string" metadataMicrosoftDscIgnored = "Resource returned '_metadata' property 'Microsoft.DSC' which is ignored" metadataNotObject = "Resource returned '_metadata' property which is not an object" metadataRestartRequiredInvalid = "Resource returned '_metadata' property '_restartRequired' which contains invalid value: %{value}" +skippingOutput = "Skipping output for '%{name}' due to condition evaluating to false" +outputValueNotDefined = "Output value '%{name}' is not defined" +secureOutputSkipped = "Secure output '%{name}' is skipped" +outputTypeNotMatched = "Output '%{name}' type does not match expected type '%{expected_type}'" +copyNotSupported = "Copy for output '%{name}' is currently not supported" [discovery.commandDiscovery] couldNotReadSetting = "Could not read 'resourcePath' setting" diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 4b2bf4c0..ab72b1c3 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -7,6 +7,7 @@ use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashMap; +use std::fmt::Display; use crate::{dscerror::DscError, schemas::DscRepoSchema}; @@ -180,6 +181,20 @@ pub enum DataType { Array, } +impl Display for DataType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DataType::String => write!(f, "string"), + DataType::SecureString => write!(f, "secureString"), + DataType::Int => write!(f, "int"), + DataType::Bool => write!(f, "bool"), + DataType::Object => write!(f, "object"), + DataType::SecureObject => write!(f, "secureObject"), + DataType::Array => write!(f, "array"), + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub enum CopyMode { #[serde(rename = "serial")] diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index 6f2be74b..b5396ab2 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -3,6 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; use crate::configure::config_doc::{Configuration, Metadata}; @@ -54,6 +55,8 @@ pub struct ConfigurationGetResult { pub messages: Vec, #[serde(rename = "hadErrors")] pub had_errors: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, } impl ConfigurationGetResult { @@ -64,6 +67,7 @@ impl ConfigurationGetResult { results: Vec::new(), messages: Vec::new(), had_errors: false, + outputs: None, } } } @@ -85,6 +89,7 @@ impl From for ConfigurationGetResult { results, messages: test_result.messages, had_errors: test_result.had_errors, + outputs: test_result.outputs, } } } @@ -140,6 +145,8 @@ pub struct ConfigurationSetResult { pub messages: Vec, #[serde(rename = "hadErrors")] pub had_errors: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, } impl ConfigurationSetResult { @@ -150,6 +157,7 @@ impl ConfigurationSetResult { results: Vec::new(), messages: Vec::new(), had_errors: false, + outputs: None, } } } @@ -200,6 +208,8 @@ pub struct ConfigurationTestResult { pub messages: Vec, #[serde(rename = "hadErrors")] pub had_errors: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, } impl ConfigurationTestResult { @@ -210,6 +220,7 @@ impl ConfigurationTestResult { results: Vec::new(), messages: Vec::new(), had_errors: false, + outputs: None, } } } @@ -228,6 +239,8 @@ pub struct ConfigurationExportResult { pub messages: Vec, #[serde(rename = "hadErrors")] pub had_errors: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, } impl ConfigurationExportResult { @@ -238,6 +251,7 @@ impl ConfigurationExportResult { result: None, messages: Vec::new(), had_errors: false, + outputs: None, } } } diff --git a/dsc_lib/src/configure/context.rs b/dsc_lib/src/configure/context.rs index 1139abb8..635857a6 100644 --- a/dsc_lib/src/configure/context.rs +++ b/dsc_lib/src/configure/context.rs @@ -19,6 +19,7 @@ pub struct Context { pub variables: Map, pub start_datetime: DateTime, pub restart_required: Option>, + pub outputs: Map, } impl Context { @@ -37,6 +38,7 @@ impl Context { variables: Map::new(), start_datetime: chrono::Local::now(), restart_required: None, + outputs: Map::new(), } } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index ddece03f..b5a94568 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::configure::config_doc::{ExecutionKind, Metadata, Resource}; +use crate::configure::config_doc::{ExecutionKind, Metadata, Resource, ValueOrCopy}; use crate::configure::{config_doc::RestartRequired, parameters::Input}; use crate::dscerror::DscError; use crate::dscresources::invoke_result::ExportResult; @@ -376,6 +376,10 @@ impl Configurator { result.metadata = Some( self.get_result_metadata(Operation::Get) ); + self.process_output()?; + if !self.context.outputs.is_empty() { + result.outputs = Some(self.context.outputs.clone()); + } Ok(result) } @@ -533,6 +537,10 @@ impl Configurator { result.metadata = Some( self.get_result_metadata(Operation::Set) ); + self.process_output()?; + if !self.context.outputs.is_empty() { + result.outputs = Some(self.context.outputs.clone()); + } Ok(result) } @@ -607,6 +615,10 @@ impl Configurator { result.metadata = Some( self.get_result_metadata(Operation::Test) ); + self.process_output()?; + if !self.context.outputs.is_empty() { + result.outputs = Some(self.context.outputs.clone()); + } Ok(result) } @@ -665,6 +677,10 @@ impl Configurator { } result.result = Some(conf); + self.process_output()?; + if !self.context.outputs.is_empty() { + result.outputs = Some(self.context.outputs.clone()); + } Ok(result) } @@ -679,6 +695,46 @@ impl Configurator { Ok(false) } + pub fn process_output(&mut self) -> Result<(), DscError> { + if self.config.outputs.is_none() || self.context.execution_type == ExecutionKind::WhatIf { + return Ok(()); + } + if let Some(outputs) = &self.config.outputs { + for (name, output) in outputs { + if let Some(condition) = &output.condition { + let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; + if condition_result != Value::Bool(true) { + info!("{}", t!("configure.mod.skippingOutput", name = name)); + continue; + } + } + + match &output.value_or_copy { + ValueOrCopy::Value(value) => { + let value_result = self.statement_parser.parse_and_execute(&value, &self.context)?; + if output.r#type == DataType::SecureString || output.r#type == DataType::SecureObject { + warn!("{}", t!("configure.mod.secureOutputSkipped", name = name)); + continue; + } + if value_result.is_string() && output.r#type != DataType::String || + value_result.is_i64() && output.r#type != DataType::Int || + value_result.is_boolean() && output.r#type != DataType::Bool || + value_result.is_array() && output.r#type != DataType::Array || + value_result.is_object() && output.r#type != DataType::Object { + return Err(DscError::Validation(t!("configure.mod.outputTypeNotMatch", name = name, expected_type = output.r#type).to_string())); + } + self.context.outputs.insert(name.clone(), value_result); + }, + _ => { + warn!("{}", t!("configure.mod.copyNotSupported", name = name)); + continue; + } + } + } + } + Ok(()) + } + /// Set the mounted path for the configuration. /// /// # Arguments From 96e83662bc1163dab0684014c3b8003fa814cb3d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 27 Jul 2025 14:22:48 -0700 Subject: [PATCH 3/3] update bicep example to use output --- dsc/examples/hello_world.dsc.bicep | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dsc/examples/hello_world.dsc.bicep b/dsc/examples/hello_world.dsc.bicep index 6be5e348..030f3875 100644 --- a/dsc/examples/hello_world.dsc.bicep +++ b/dsc/examples/hello_world.dsc.bicep @@ -9,3 +9,5 @@ resource echo 'Microsoft.DSC.Debug/Echo@2025-01-01' = { output: 'Hello, world!' } } + +output exampleOutput string = echo.properties.output