Skip to content
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
3 changes: 3 additions & 0 deletions dsc/examples/hello_world.dsc.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ resource echo 'Microsoft.DSC.Debug/Echo@2025-08-27' = {
output: 'Hello, world!'
}
}

// This is waiting on https://github.com/Azure/bicep/issues/17670 to be resolved
// output exampleOutput string = echo.properties.output
3 changes: 3 additions & 0 deletions dsc/tests/dsc_osinfo.tests.ps1
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Tests for osinfo examples' {
It 'Config with default parameters and get works' {
$out = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml | ConvertFrom-Json -Depth 10
Expand Down
41 changes: 41 additions & 0 deletions dsc/tests/dsc_output.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'output tests' {
It 'config with output property works' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
variables:
arrayVar:
- 1
- 2
- 3
resources:
- name: echo
type: Microsoft.DSC.Debug/Echo
properties:
output: This is a test
outputs:
simpleText:
type: string
value: Hello World
expression:
type: string
value: "[reference(resourceId('Microsoft.DSC.Debug/Echo', 'echo')).output]"
conditionSucceed:
type: int
condition: "[equals(1, 1)]"
value: "[variables('arrayVar')[1]]"
conditionFail:
type: int
condition: "[equals(1, 2)]"
value: "[variables('arrayVar')[1]]"
'@
$out = dsc config get -i $configYaml | ConvertFrom-Json -Depth 10
$LASTEXITCODE | Should -Be 0
$out.outputs.simpleText | Should -Be 'Hello World'
$out.outputs.expression | Should -Be 'This is a test'
$out.outputs.conditionSucceed | Should -Be 2
$out.outputs.conditionFail | Should -BeNullOrEmpty
}
}
4 changes: 4 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ circularDependency = "Circular dependency or unresolvable parameter references d
userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined"
addingUserFunction = "Adding user function '%{name}'"
copyCountResultNotInteger = "Copy count result is not an integer: %{expression}"
skippingOutput = "Skipping output for '%{name}' due to condition evaluating to false"
secureOutputSkipped = "Secure output '%{name}' is skipped"
outputTypeNotMatch = "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"
Expand Down
26 changes: 23 additions & 3 deletions lib/dsc-lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ pub struct UserFunctionOutput {
pub value: String,
}

#[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<String>,
pub r#type: DataType,
#[serde(flatten)]
pub value_or_copy: ValueOrCopy,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
Expand All @@ -140,15 +156,18 @@ pub struct Configuration {
#[serde(skip_serializing_if = "Option::is_none")]
pub functions: Option<Vec<UserFunction>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<HashMap<String, Parameter>>,
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<Map<String, Value>>,
pub outputs: Option<HashMap<String, Output>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<HashMap<String, Parameter>>,
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
pub variables: Option<Map<String, Value>>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Parameter {
#[serde(rename = "type")]
pub parameter_type: DataType,
Expand Down Expand Up @@ -358,6 +377,7 @@ impl Configuration {
resources: Vec::new(),
functions: None,
variables: None,
outputs: None,
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions lib/dsc-lib/src/configure/config_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -54,6 +55,8 @@ pub struct ConfigurationGetResult {
pub messages: Vec<ResourceMessage>,
#[serde(rename = "hadErrors")]
pub had_errors: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Map<String, Value>>,
}

impl ConfigurationGetResult {
Expand All @@ -64,6 +67,7 @@ impl ConfigurationGetResult {
results: Vec::new(),
messages: Vec::new(),
had_errors: false,
outputs: None,
}
}
}
Expand All @@ -85,6 +89,7 @@ impl From<ConfigurationTestResult> for ConfigurationGetResult {
results,
messages: test_result.messages,
had_errors: test_result.had_errors,
outputs: test_result.outputs,
}
}
}
Expand Down Expand Up @@ -140,6 +145,8 @@ pub struct ConfigurationSetResult {
pub messages: Vec<ResourceMessage>,
#[serde(rename = "hadErrors")]
pub had_errors: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Map<String, Value>>,
}

impl ConfigurationSetResult {
Expand All @@ -150,6 +157,7 @@ impl ConfigurationSetResult {
results: Vec::new(),
messages: Vec::new(),
had_errors: false,
outputs: None,
}
}
}
Expand Down Expand Up @@ -200,6 +208,8 @@ pub struct ConfigurationTestResult {
pub messages: Vec<ResourceMessage>,
#[serde(rename = "hadErrors")]
pub had_errors: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Map<String, Value>>,
}

impl ConfigurationTestResult {
Expand All @@ -210,6 +220,7 @@ impl ConfigurationTestResult {
results: Vec::new(),
messages: Vec::new(),
had_errors: false,
outputs: None,
}
}
}
Expand All @@ -228,6 +239,8 @@ pub struct ConfigurationExportResult {
pub messages: Vec<ResourceMessage>,
#[serde(rename = "hadErrors")]
pub had_errors: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Map<String, Value>>,
}

impl ConfigurationExportResult {
Expand All @@ -238,6 +251,7 @@ impl ConfigurationExportResult {
result: None,
messages: Vec::new(),
had_errors: false,
outputs: None,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/dsc-lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct Context {
pub dsc_version: Option<String>,
pub execution_type: ExecutionKind,
pub extensions: Vec<DscExtension>,
pub outputs: Map<String, Value>,
pub parameters: HashMap<String, (Value, DataType)>,
pub process_expressions: bool,
pub process_mode: ProcessMode,
Expand All @@ -47,6 +48,7 @@ impl Context {
dsc_version: None,
execution_type: ExecutionKind::Actual,
extensions: Vec::new(),
outputs: Map::new(),
parameters: HashMap::new(),
process_expressions: true,
process_mode: ProcessMode::Normal,
Expand Down
61 changes: 59 additions & 2 deletions lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::configure::config_doc::{ExecutionKind, Metadata, Resource, Parameter};
use crate::configure::context::{Context, ProcessMode};
use crate::configure::{config_doc::{IntOrExpression, RestartRequired}, parameters::Input};
use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, RestartRequired, ValueOrCopy}, parameters::Input};
use crate::discovery::discovery_trait::DiscoveryFilter;
use crate::dscerror::DscError;
use crate::dscresources::{
Expand Down Expand Up @@ -414,6 +413,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)
}

Expand Down Expand Up @@ -585,6 +588,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)
}

Expand Down Expand Up @@ -663,6 +670,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)
}

Expand Down Expand Up @@ -725,6 +736,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)
}

Expand All @@ -739,6 +754,48 @@ impl Configurator {
Ok(false)
}

/// Process the outputs defined in the configuration.
///
/// # Errors
///
/// This function will return an error if the output processing fails.
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;
}
}

if let ValueOrCopy::Value(value) = &output.value_or_copy {
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;
}
// TODO: handle nullable when supported
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);
} else {
warn!("{}", t!("configure.mod.copyNotSupported", name = name));
}
}
}
Ok(())
}

/// Set the mounted path for the configuration.
///
/// # Arguments
Expand Down