Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 3 additions & 1 deletion dsc/examples/hello_world.dsc.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

targetScope = 'desiredStateConfiguration'

param text string = 'Hello, world!'

// use workaround where Bicep currently requires version in date format
resource echo 'Microsoft.DSC.Debug/Echo@2025-08-27' = {
name: 'exampleEcho'
properties: {
output: 'Hello, world!'
output: text
}
}

Expand Down
3 changes: 3 additions & 0 deletions dsc/examples/hello_world.dsc.bicepparam
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using 'hello_world.dsc.bicep'

param text = 'This is a parameterized hello world!'
9 changes: 9 additions & 0 deletions dsc/examples/hello_world.dsc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"text": {
"value": "This is a parameterized hello world!"
}
}
}
3 changes: 0 additions & 3 deletions dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ mcpAbout = "Use DSC as a MCP server"
[main]
ctrlCReceived = "Ctrl-C received"
failedCtrlCHandler = "Failed to set Ctrl-C handler"
failedReadingParametersFile = "Failed to read parameters file"
readingParametersFromStdin = "Reading parameters from STDIN"
generatingCompleter = "Generating completion script for"
readingParametersFile = "Reading parameters from file"
mergingParameters = "Merging inline parameters with parameters file (inline takes precedence)"
failedMergingParameters = "Failed to merge parameters"
usingDscVersion = "Running DSC version"
Expand Down
41 changes: 7 additions & 34 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
use args::{Args, SubCommand};
use clap::{CommandFactory, Parser};
use clap_complete::generate;
use dsc_lib::progress::ProgressFormat;
use mcp::start_mcp_server;
use rust_i18n::{i18n, t};
use std::{io, io::Read, process::exit};
use std::{io, process::exit};
use sysinfo::{Process, RefreshKind, System, get_current_pid, ProcessRefreshKind};
use tracing::{error, info, warn, debug};
use dsc_lib::progress::ProgressFormat;

use crate::util::EXIT_INVALID_INPUT;
use crate::util::{EXIT_INVALID_INPUT, get_input};

#[cfg(debug_assertions)]
use crossterm::event;
Expand Down Expand Up @@ -54,38 +54,11 @@ fn main() {
generate(shell, &mut cmd, "dsc", &mut io::stdout());
},
SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_assert, as_include } => {
// Read parameters from file if provided
let file_params = if let Some(file_name) = &parameters_file {
if file_name == "-" {
info!("{}", t!("main.readingParametersFromStdin"));
let mut stdin = Vec::<u8>::new();
match io::stdin().read_to_end(&mut stdin) {
Ok(_) => {
match String::from_utf8(stdin) {
Ok(input) => Some(input),
Err(err) => {
error!("{}: {err}", t!("util.invalidUtf8"));
exit(EXIT_INVALID_INPUT);
}
}
},
Err(err) => {
error!("{}: {err}", t!("util.failedToReadStdin"));
exit(EXIT_INVALID_INPUT);
}
}
} else {
info!("{}: {file_name}", t!("main.readingParametersFile"));
match std::fs::read_to_string(file_name) {
Ok(content) => Some(content),
Err(err) => {
error!("{} '{file_name}': {err}", t!("main.failedReadingParametersFile"));
exit(util::EXIT_INVALID_INPUT);
}
}
}
} else {
let params = get_input(None, parameters_file.as_ref());
let file_params = if params.is_empty() {
None
} else {
Some(params)
};

let merged_parameters = match (file_params, parameters) {
Expand Down
51 changes: 49 additions & 2 deletions dsc/tests/dsc_parameters.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,53 @@ Describe 'Parameters tests' {
$errorMessage | Should -BeLike "*ERROR*Empty input provided*"
}

It 'Parameters in ARM syntax are supported' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
myString:
type: string
myObject:
type: object
myArray:
type: array
myInt:
type: int
myBool:
type: bool
resources:
- name: echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[concat(parameters('myString'), '-', parameters('myObject').prop1, '-', parameters('myArray')[0], parameters('myArray')[1], '-', string(parameters('myInt')), '-', string(parameters('myBool'))]"
"@
$params = @{
parameters = @{
myString = @{
value = 'Hello'
}
myObject = @{
value = @{
prop1 = 'World'
}
}
myArray = @{
value = @('Item1', 'Item2')
}
myInt = @{
value = 123
}
myBool = @{
value = $true
}
}
} | ConvertTo-Json -Compress -Depth 5

$out = $config_yaml | dsc -l trace config -p $params get -f - 2> $TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/error.log -Raw | Out-String)
$out.results[0].result.actualState.output | Should -BeExactly 'Hello-World-Item1Item2-123-true'
}

It 'Invalid parameters read from STDIN result in error' {
$params = @{
osFamily = 'Windows'
Expand All @@ -403,7 +450,7 @@ Describe 'Parameters tests' {
$LASTEXITCODE | Should -Be 4
$out | Should -BeNullOrEmpty
$errorMessage = Get-Content -Path $TestDrive/error.log -Raw
$errorMessage | Should -BeLike "*ERROR*Parameter input failure: JSON: missing field ````parameters````*"
$errorMessage | Should -BeLike "*ERROR*Invalid parameters format: missing field ````parameters````*"
}

It 'Parameters can reference other parameters in defaultValue: simple nested' {
Expand Down Expand Up @@ -935,7 +982,7 @@ parameters:
}
else {
$expectedOutput = "{0}-{1}" -f $fileValue, $inlineValue
}
}
$out.results[0].result.actualState.output | Should -BeExactly $expectedOutput
}

Expand Down
3 changes: 2 additions & 1 deletion extensions/bicep/.project.data.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"Kind": "Extension",
"CopyFiles": {
"All": [
"bicep.dsc.extension.json"
"bicep.dsc.extension.json",
"bicepparams.dsc.extension.json"
]
}
}
8 changes: 8 additions & 0 deletions extensions/bicep/bicep.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,12 @@ resource invalid 'Microsoft.DSC.Extension/Bicep:1.0' = {
$content | Should -Match "Importing file '$bicepFile' with extension 'Microsoft.DSC.Extension/Bicep'"
$content | Should -Match "BCP033"
}

It 'Example bicep parameters file should work' {
$bicepFile = Resolve-Path -Path "$PSScriptRoot\..\..\dsc\examples\hello_world.dsc.bicep"
$bicepParamFile = Resolve-Path -Path "$PSScriptRoot\..\..\dsc\examples\hello_world.dsc.bicepparam"
$out = dsc -l trace config --parameters-file $bicepParamFile get --file $bicepFile 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/error.log -Raw | Out-String)
$out.results[0].result.actualState.output | Should -BeExactly 'This is a parameterized hello world!'
}
}
18 changes: 18 additions & 0 deletions extensions/bicep/bicepparams.dsc.extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json",
"type": "Microsoft.DSC.Extension/BicepParameters",
"version": "0.1.0",
"description": "Enable passing Bicep parameters file directly to DSC, but requires bicep executable to be available.",
"condition": "[not(equals(tryWhich('bicep'), null()))]",
"import": {
"fileExtensions": ["bicepparam"],
"executable": "bicep",
"args": [
"build-params",
{
"fileArg": ""
},
"--stdout"
]
}
}
7 changes: 7 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ 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"

[configure.parameters]
importingParametersFromJson = "Importing parameters from `parameters_input` JSON"
importingParametersFromComplexInput = "Importing parameters from complex input"
importingParametersFromInput = "Importing parameters from simple input"
invalidParamsJsonFormat = "Invalid parameters JSON format: %{error}"
invalidParamsFormat = "Invalid parameters format: %{error}"

[discovery.commandDiscovery]
couldNotReadSetting = "Could not read 'resourcePath' setting"
appendingEnvPath = "Appending PATH to resourcePath"
Expand Down
5 changes: 3 additions & 2 deletions lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// Licensed under the MIT License.

use crate::configure::context::{Context, ProcessMode};
use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, RestartRequired, ValueOrCopy}, parameters::Input};
use crate::configure::parameters::import_parameters;
use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, RestartRequired, ValueOrCopy}};
use crate::discovery::discovery_trait::DiscoveryFilter;
use crate::dscerror::DscError;
use crate::dscresources::{
Expand Down Expand Up @@ -836,7 +837,7 @@ impl Configurator {
// process input parameters first
if let Some(parameters_input) = parameters_input {
trace!("parameters_input: {parameters_input}");
let input_parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
let input_parameters: HashMap<String, Value> = import_parameters(parameters_input)?;

for (name, value) in input_parameters {
if let Some(constraint) = parameters.get(&name) {
Expand Down
71 changes: 70 additions & 1 deletion lib/dsc-lib/src/configure/parameters.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::dscerror::DscError;
use rust_i18n::t;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, fmt::Display};
use tracing::trace;

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Input {
pub parameters: HashMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ComplexInput {
pub parameters: HashMap<String, InputObject>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct InputObject {
pub value: Value,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ParametersJson {
#[serde(rename = "parametersJson")]
pub parameters_json: String,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ParametersInput {
pub parameters_input: ParametersJson,
}

pub const SECURE_VALUE_REDACTED: &str = "<secureValue>";

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down Expand Up @@ -64,3 +88,48 @@ pub enum SecureKind {
#[serde(rename = "secureObject")]
SecureObject(SecureObject),
}

pub fn import_parameters(parameters: &Value) -> Result<HashMap<String, Value>, DscError> {
let input = match serde_json::from_value::<ParametersJson>(parameters.clone()) {
Ok(input) => {
trace!("{}", t!("configure.parameters.importingParametersFromJson"));
let param_map = match serde_json::from_str::<ComplexInput>(&input.parameters_json) {
Ok(param_map) => param_map,
Err(e) => {
return Err(DscError::Parser(t!("configure.parameters.invalidParamsJsonFormat", error = e).to_string()));
}
};
let mut result: HashMap<String, Value> = HashMap::new();
for (name, input_object) in param_map.parameters {
result.insert(name, input_object.value);
}
result
},
Err(_) => {
let input = match serde_json::from_value::<ComplexInput>(parameters.clone()) {
Ok(input) => {
trace!("{}", t!("configure.parameters.importingParametersFromComplexInput"));
let mut result: HashMap<String, Value> = HashMap::new();
for (name, input_object) in input.parameters {
result.insert(name, input_object.value);
}
result
},
Err(_) => {
let input = match serde_json::from_value::<Input>(parameters.clone()) {
Ok(input) => {
trace!("{}", t!("configure.parameters.importingParametersFromInput"));
input.parameters
}
Err(e) => {
return Err(DscError::Parser(t!("configure.parameters.invalidParamsFormat", error = e).to_string()));
}
};
input
}
};
input
},
};
Ok(input)
}
3 changes: 3 additions & 0 deletions lib/dsc-lib/src/extensions/dscextension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub enum Capability {
Secret,
/// The extension imports configuration from a different format.
Import,
/// The extension imports parameters from a different format.
ImportParameters,
}

impl Display for Capability {
Expand All @@ -51,6 +53,7 @@ impl Display for Capability {
Capability::Discover => write!(f, "Discover"),
Capability::Secret => write!(f, "Secret"),
Capability::Import => write!(f, "Import"),
Capability::ImportParameters => write!(f, "ImportParams"),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions lib/dsc-lib/src/extensions/extension_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub struct ExtensionManifest {
pub discover: Option<DiscoverMethod>,
/// Details how to call the Import method of the extension.
pub import: Option<ImportMethod>,
/// Details how to call the ImportParameters method of the extension.
#[serde(rename = "importParameters")]
pub import_parameters: Option<ImportMethod>,
/// Details how to call the Secret method of the extension.
pub secret: Option<SecretMethod>,
/// Mapping of exit codes to descriptions. Zero is always success and non-zero is always failure.
Expand Down
3 changes: 1 addition & 2 deletions lib/dsc-lib/src/extensions/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ impl DscExtension {
if self.import_file_extensions.as_ref().is_some_and(|exts| exts.contains(&file_extension)) {
debug!("{}", t!("extensions.dscextension.importingFile", file = file.display(), extension = self.type_name));
} else {
debug!("{}", t!("extensions.dscextension.importNotSupported", file = file.display(), extension = self.type_name));
return Err(DscError::NotSupported(
t!("extensions.dscextension.importNotSupported", file = file.display(), extension = self.type_name).to_string(),
));
Expand Down Expand Up @@ -101,7 +100,7 @@ impl DscExtension {
self.type_name.clone(),
Capability::Import.to_string()
))
}
}
}

fn process_import_args(args: Option<&Vec<ImportArgKind>>, file: &Path) -> Result<Option<Vec<String>>, DscError> {
Expand Down
2 changes: 1 addition & 1 deletion lib/dsc-lib/src/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
pub mod discover;
pub mod dscextension;
pub mod extension_manifest;
pub mod secret;
pub mod import;
pub mod secret;
Loading