Skip to content

Add outputs support in configuration #1010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions dsc/examples/hello_world.dsc.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ resource echo 'Microsoft.DSC.Debug/Echo@2025-01-01' = {
output: 'Hello, world!'
}
}

output exampleOutput string = echo.properties.output
5 changes: 5 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 34 additions & 0 deletions dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -105,6 +106,21 @@ pub struct Metadata {
pub other: Map<String, Value>,
}

#[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,
pub value_or_copy: ValueOrCopy,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
Expand All @@ -120,9 +136,12 @@ pub struct Configuration {
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<HashMap<String, Output>>,
}

#[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 @@ -162,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")]
Expand Down Expand Up @@ -300,6 +333,7 @@ impl Configuration {
variables: None,
resources: Vec::new(),
metadata: None,
outputs: None,
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions 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 dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Context {
pub variables: Map<String, Value>,
pub start_datetime: DateTime<Local>,
pub restart_required: Option<Vec<RestartRequired>>,
pub outputs: Map<String, Value>,
}

impl Context {
Expand All @@ -37,6 +38,7 @@ impl Context {
variables: Map::new(),
start_datetime: chrono::Local::now(),
restart_required: None,
outputs: Map::new(),
}
}
}
Expand Down
58 changes: 57 additions & 1 deletion dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand Down
Loading