Skip to content

Commit 997f9c1

Browse files
authored
Merge pull request #1010 from SteveL-MSFT/output
Add `outputs` support in configuration
2 parents f7e6e82 + 5c345ec commit 997f9c1

File tree

8 files changed

+149
-5
lines changed

8 files changed

+149
-5
lines changed

dsc/examples/hello_world.dsc.bicep

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ resource echo 'Microsoft.DSC.Debug/Echo@2025-08-27' = {
99
output: 'Hello, world!'
1010
}
1111
}
12+
13+
// This is waiting on https://github.com/Azure/bicep/issues/17670 to be resolved
14+
// output exampleOutput string = echo.properties.output

dsc/tests/dsc_osinfo.tests.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
14
Describe 'Tests for osinfo examples' {
25
It 'Config with default parameters and get works' {
36
$out = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml | ConvertFrom-Json -Depth 10

dsc/tests/dsc_output.tests.ps1

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'output tests' {
5+
It 'config with output property works' {
6+
$configYaml = @'
7+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
8+
variables:
9+
arrayVar:
10+
- 1
11+
- 2
12+
- 3
13+
resources:
14+
- name: echo
15+
type: Microsoft.DSC.Debug/Echo
16+
properties:
17+
output: This is a test
18+
outputs:
19+
simpleText:
20+
type: string
21+
value: Hello World
22+
expression:
23+
type: string
24+
value: "[reference(resourceId('Microsoft.DSC.Debug/Echo', 'echo')).output]"
25+
conditionSucceed:
26+
type: int
27+
condition: "[equals(1, 1)]"
28+
value: "[variables('arrayVar')[1]]"
29+
conditionFail:
30+
type: int
31+
condition: "[equals(1, 2)]"
32+
value: "[variables('arrayVar')[1]]"
33+
'@
34+
$out = dsc config get -i $configYaml | ConvertFrom-Json -Depth 10
35+
$LASTEXITCODE | Should -Be 0
36+
$out.outputs.simpleText | Should -Be 'Hello World'
37+
$out.outputs.expression | Should -Be 'This is a test'
38+
$out.outputs.conditionSucceed | Should -Be 2
39+
$out.outputs.conditionFail | Should -BeNullOrEmpty
40+
}
41+
}

lib/dsc-lib/locales/en-us.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ circularDependency = "Circular dependency or unresolvable parameter references d
8080
userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined"
8181
addingUserFunction = "Adding user function '%{name}'"
8282
copyCountResultNotInteger = "Copy count result is not an integer: %{expression}"
83+
skippingOutput = "Skipping output for '%{name}' due to condition evaluating to false"
84+
secureOutputSkipped = "Secure output '%{name}' is skipped"
85+
outputTypeNotMatch = "Output '%{name}' type does not match expected type '%{expected_type}'"
86+
copyNotSupported = "Copy for output '%{name}' is currently not supported"
8387

8488
[discovery.commandDiscovery]
8589
couldNotReadSetting = "Could not read 'resourcePath' setting"

lib/dsc-lib/src/configure/config_doc.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ pub struct UserFunctionOutput {
129129
pub value: String,
130130
}
131131

132+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
133+
#[serde(rename_all = "camelCase")]
134+
pub enum ValueOrCopy {
135+
Value(String),
136+
Copy(Copy),
137+
}
138+
139+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
140+
#[serde(deny_unknown_fields)]
141+
pub struct Output {
142+
pub condition: Option<String>,
143+
pub r#type: DataType,
144+
#[serde(flatten)]
145+
pub value_or_copy: ValueOrCopy,
146+
}
147+
132148
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
133149
#[serde(deny_unknown_fields)]
134150
pub struct Configuration {
@@ -140,15 +156,18 @@ pub struct Configuration {
140156
#[serde(skip_serializing_if = "Option::is_none")]
141157
pub functions: Option<Vec<UserFunction>>,
142158
#[serde(skip_serializing_if = "Option::is_none")]
143-
pub parameters: Option<HashMap<String, Parameter>>,
159+
pub metadata: Option<Metadata>,
144160
#[serde(skip_serializing_if = "Option::is_none")]
145-
pub variables: Option<Map<String, Value>>,
161+
pub outputs: Option<HashMap<String, Output>>,
162+
#[serde(skip_serializing_if = "Option::is_none")]
163+
pub parameters: Option<HashMap<String, Parameter>>,
146164
pub resources: Vec<Resource>,
147165
#[serde(skip_serializing_if = "Option::is_none")]
148-
pub metadata: Option<Metadata>,
166+
pub variables: Option<Map<String, Value>>,
149167
}
150168

151169
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
170+
#[serde(deny_unknown_fields)]
152171
pub struct Parameter {
153172
#[serde(rename = "type")]
154173
pub parameter_type: DataType,
@@ -358,6 +377,7 @@ impl Configuration {
358377
resources: Vec::new(),
359378
functions: None,
360379
variables: None,
380+
outputs: None,
361381
}
362382
}
363383
}

lib/dsc-lib/src/configure/config_result.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use schemars::JsonSchema;
55
use serde::{Deserialize, Serialize};
6+
use serde_json::{Map, Value};
67
use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult};
78
use crate::configure::config_doc::{Configuration, Metadata};
89

@@ -54,6 +55,8 @@ pub struct ConfigurationGetResult {
5455
pub messages: Vec<ResourceMessage>,
5556
#[serde(rename = "hadErrors")]
5657
pub had_errors: bool,
58+
#[serde(skip_serializing_if = "Option::is_none")]
59+
pub outputs: Option<Map<String, Value>>,
5760
}
5861

5962
impl ConfigurationGetResult {
@@ -64,6 +67,7 @@ impl ConfigurationGetResult {
6467
results: Vec::new(),
6568
messages: Vec::new(),
6669
had_errors: false,
70+
outputs: None,
6771
}
6872
}
6973
}
@@ -85,6 +89,7 @@ impl From<ConfigurationTestResult> for ConfigurationGetResult {
8589
results,
8690
messages: test_result.messages,
8791
had_errors: test_result.had_errors,
92+
outputs: test_result.outputs,
8893
}
8994
}
9095
}
@@ -140,6 +145,8 @@ pub struct ConfigurationSetResult {
140145
pub messages: Vec<ResourceMessage>,
141146
#[serde(rename = "hadErrors")]
142147
pub had_errors: bool,
148+
#[serde(skip_serializing_if = "Option::is_none")]
149+
pub outputs: Option<Map<String, Value>>,
143150
}
144151

145152
impl ConfigurationSetResult {
@@ -150,6 +157,7 @@ impl ConfigurationSetResult {
150157
results: Vec::new(),
151158
messages: Vec::new(),
152159
had_errors: false,
160+
outputs: None,
153161
}
154162
}
155163
}
@@ -200,6 +208,8 @@ pub struct ConfigurationTestResult {
200208
pub messages: Vec<ResourceMessage>,
201209
#[serde(rename = "hadErrors")]
202210
pub had_errors: bool,
211+
#[serde(skip_serializing_if = "Option::is_none")]
212+
pub outputs: Option<Map<String, Value>>,
203213
}
204214

205215
impl ConfigurationTestResult {
@@ -210,6 +220,7 @@ impl ConfigurationTestResult {
210220
results: Vec::new(),
211221
messages: Vec::new(),
212222
had_errors: false,
223+
outputs: None,
213224
}
214225
}
215226
}
@@ -228,6 +239,8 @@ pub struct ConfigurationExportResult {
228239
pub messages: Vec<ResourceMessage>,
229240
#[serde(rename = "hadErrors")]
230241
pub had_errors: bool,
242+
#[serde(skip_serializing_if = "Option::is_none")]
243+
pub outputs: Option<Map<String, Value>>,
231244
}
232245

233246
impl ConfigurationExportResult {
@@ -238,6 +251,7 @@ impl ConfigurationExportResult {
238251
result: None,
239252
messages: Vec::new(),
240253
had_errors: false,
254+
outputs: None,
241255
}
242256
}
243257
}

lib/dsc-lib/src/configure/context.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub struct Context {
2525
pub dsc_version: Option<String>,
2626
pub execution_type: ExecutionKind,
2727
pub extensions: Vec<DscExtension>,
28+
pub outputs: Map<String, Value>,
2829
pub parameters: HashMap<String, (Value, DataType)>,
2930
pub process_expressions: bool,
3031
pub process_mode: ProcessMode,
@@ -47,6 +48,7 @@ impl Context {
4748
dsc_version: None,
4849
execution_type: ExecutionKind::Actual,
4950
extensions: Vec::new(),
51+
outputs: Map::new(),
5052
parameters: HashMap::new(),
5153
process_expressions: true,
5254
process_mode: ProcessMode::Normal,

lib/dsc-lib/src/configure/mod.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::configure::config_doc::{ExecutionKind, Metadata, Resource, Parameter};
54
use crate::configure::context::{Context, ProcessMode};
6-
use crate::configure::{config_doc::{IntOrExpression, RestartRequired}, parameters::Input};
5+
use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, RestartRequired, ValueOrCopy}, parameters::Input};
76
use crate::discovery::discovery_trait::DiscoveryFilter;
87
use crate::dscerror::DscError;
98
use crate::dscresources::{
@@ -414,6 +413,10 @@ impl Configurator {
414413
result.metadata = Some(
415414
self.get_result_metadata(Operation::Get)
416415
);
416+
self.process_output()?;
417+
if !self.context.outputs.is_empty() {
418+
result.outputs = Some(self.context.outputs.clone());
419+
}
417420
Ok(result)
418421
}
419422

@@ -585,6 +588,10 @@ impl Configurator {
585588
result.metadata = Some(
586589
self.get_result_metadata(Operation::Set)
587590
);
591+
self.process_output()?;
592+
if !self.context.outputs.is_empty() {
593+
result.outputs = Some(self.context.outputs.clone());
594+
}
588595
Ok(result)
589596
}
590597

@@ -663,6 +670,10 @@ impl Configurator {
663670
result.metadata = Some(
664671
self.get_result_metadata(Operation::Test)
665672
);
673+
self.process_output()?;
674+
if !self.context.outputs.is_empty() {
675+
result.outputs = Some(self.context.outputs.clone());
676+
}
666677
Ok(result)
667678
}
668679

@@ -725,6 +736,10 @@ impl Configurator {
725736
}
726737

727738
result.result = Some(conf);
739+
self.process_output()?;
740+
if !self.context.outputs.is_empty() {
741+
result.outputs = Some(self.context.outputs.clone());
742+
}
728743
Ok(result)
729744
}
730745

@@ -739,6 +754,48 @@ impl Configurator {
739754
Ok(false)
740755
}
741756

757+
/// Process the outputs defined in the configuration.
758+
///
759+
/// # Errors
760+
///
761+
/// This function will return an error if the output processing fails.
762+
pub fn process_output(&mut self) -> Result<(), DscError> {
763+
if self.config.outputs.is_none() || self.context.execution_type == ExecutionKind::WhatIf {
764+
return Ok(());
765+
}
766+
if let Some(outputs) = &self.config.outputs {
767+
for (name, output) in outputs {
768+
if let Some(condition) = &output.condition {
769+
let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?;
770+
if condition_result != Value::Bool(true) {
771+
info!("{}", t!("configure.mod.skippingOutput", name = name));
772+
continue;
773+
}
774+
}
775+
776+
if let ValueOrCopy::Value(value) = &output.value_or_copy {
777+
let value_result = self.statement_parser.parse_and_execute(value, &self.context)?;
778+
if output.r#type == DataType::SecureString || output.r#type == DataType::SecureObject {
779+
warn!("{}", t!("configure.mod.secureOutputSkipped", name = name));
780+
continue;
781+
}
782+
// TODO: handle nullable when supported
783+
if value_result.is_string() && output.r#type != DataType::String ||
784+
value_result.is_i64() && output.r#type != DataType::Int ||
785+
value_result.is_boolean() && output.r#type != DataType::Bool ||
786+
value_result.is_array() && output.r#type != DataType::Array ||
787+
value_result.is_object() && output.r#type != DataType::Object {
788+
return Err(DscError::Validation(t!("configure.mod.outputTypeNotMatch", name = name, expected_type = output.r#type).to_string()));
789+
}
790+
self.context.outputs.insert(name.clone(), value_result);
791+
} else {
792+
warn!("{}", t!("configure.mod.copyNotSupported", name = name));
793+
}
794+
}
795+
}
796+
Ok(())
797+
}
798+
742799
/// Set the mounted path for the configuration.
743800
///
744801
/// # Arguments

0 commit comments

Comments
 (0)