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
4 changes: 3 additions & 1 deletion dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ 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"
foundProcesses = "Found processes"
failedToGetPid = "Could not get current process id"
Expand Down Expand Up @@ -165,5 +167,5 @@ failedToAbsolutizePath = "Error making config path absolute"
failedToGetParentPath = "Error reading config path parent"
dscConfigRootAlreadySet = "The current value of DSC_CONFIG_ROOT env var will be overridden"
settingDscConfigRoot = "Setting DSC_CONFIG_ROOT env var as"
stdinNotAllowedForBothParametersAndInput = "Cannot read from STDIN for both parameters and input."
removingUtf8Bom = "Removing UTF-8 BOM from input"
parametersNotObject = "Parameters must be an object"
4 changes: 2 additions & 2 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ pub enum SubCommand {
Config {
#[clap(subcommand)]
subcommand: ConfigSubCommand,
#[clap(short, long, help = t!("args.parameters").to_string(), conflicts_with = "parameters_file")]
#[clap(short, long, help = t!("args.parameters").to_string())]
parameters: Option<String>,
#[clap(short = 'f', long, help = t!("args.parametersFile").to_string(), conflicts_with = "parameters")]
#[clap(short = 'f', long, help = t!("args.parametersFile").to_string())]
parameters_file: Option<String>,
#[clap(short = 'r', long, help = t!("args.systemRoot").to_string())]
system_root: Option<String>,
Expand Down
53 changes: 34 additions & 19 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,15 @@ fn main() {
generate(shell, &mut cmd, "dsc", &mut io::stdout());
},
SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_assert, as_include } => {
if let Some(file_name) = parameters_file {
// 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();
let parameters = match io::stdin().read_to_end(&mut stdin) {
match io::stdin().read_to_end(&mut stdin) {
Ok(_) => {
match String::from_utf8(stdin) {
Ok(input) => {
input
},
Ok(input) => Some(input),
Err(err) => {
error!("{}: {err}", t!("util.invalidUtf8"));
exit(EXIT_INVALID_INPUT);
Expand All @@ -74,22 +73,38 @@ fn main() {
error!("{}: {err}", t!("util.failedToReadStdin"));
exit(EXIT_INVALID_INPUT);
}
};
subcommand::config(&subcommand, &Some(parameters), true, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format);
return;
}
info!("{}: {file_name}", t!("main.readingParametersFile"));
match std::fs::read_to_string(&file_name) {
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), false, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format),
Err(err) => {
error!("{} '{file_name}': {err}", t!("main.failedReadingParametersFile"));
exit(util::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 {
subcommand::config(&subcommand, &parameters, false, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format);
}
} else {
None
};

let merged_parameters = match (file_params, parameters) {
(Some(file_content), Some(inline_content)) => {
info!("{}", t!("main.mergingParameters"));
match util::merge_parameters(&file_content, &inline_content) {
Ok(merged) => Some(merged),
Err(err) => {
error!("{}: {err}", t!("main.failedMergingParameters"));
exit(EXIT_INVALID_INPUT);
}
}
},
(Some(file_content), None) => Some(file_content),
(None, Some(inline_content)) => Some(inline_content),
(None, None) => None,
};

subcommand::config(&subcommand, &merged_parameters, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format);
},
SubCommand::Extension { subcommand } => {
subcommand::extension(&subcommand, progress_format);
Expand Down
18 changes: 9 additions & 9 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,15 @@ fn initialize_config_root(path: Option<&String>) -> Option<String> {

#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, parameters_from_stdin: bool, mounted_path: Option<&String>, as_group: &bool, as_assert: &bool, as_include: &bool, progress_format: ProgressFormat) {
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, mounted_path: Option<&String>, as_group: &bool, as_assert: &bool, as_include: &bool, progress_format: ProgressFormat) {
let (new_parameters, json_string) = match subcommand {
ConfigSubCommand::Get { input, file, .. } |
ConfigSubCommand::Set { input, file, .. } |
ConfigSubCommand::Test { input, file, .. } |
ConfigSubCommand::Validate { input, file, .. } |
ConfigSubCommand::Export { input, file, .. } => {
let new_path = initialize_config_root(file.as_ref());
let document = get_input(input.as_ref(), new_path.as_ref(), parameters_from_stdin);
let document = get_input(input.as_ref(), new_path.as_ref());
if *as_include {
let (new_parameters, config_json) = match get_contents(&document) {
Ok((parameters, config_json)) => (parameters, config_json),
Expand All @@ -300,7 +300,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, parame
},
ConfigSubCommand::Resolve { input, file, .. } => {
let new_path = initialize_config_root(file.as_ref());
let document = get_input(input.as_ref(), new_path.as_ref(), parameters_from_stdin);
let document = get_input(input.as_ref(), new_path.as_ref());
let (new_parameters, config_json) = match get_contents(&document) {
Ok((parameters, config_json)) => (parameters, config_json),
Err(err) => {
Expand Down Expand Up @@ -398,7 +398,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, parame
};
if *as_include {
let new_path = initialize_config_root(file.as_ref());
let input = get_input(input.as_ref(), new_path.as_ref(), parameters_from_stdin);
let input = get_input(input.as_ref(), new_path.as_ref());
match serde_json::from_str::<Include>(&input) {
Ok(_) => {
// valid, so do nothing
Expand Down Expand Up @@ -554,7 +554,7 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat
},
ResourceSubCommand::Export { resource, version, input, file, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), file.as_ref(), false);
let parsed_input = get_input(input.as_ref(), file.as_ref());
resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
},
ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => {
Expand All @@ -567,23 +567,23 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat
error!("{}", t!("subcommand.jsonArrayNotSupported"));
exit(EXIT_INVALID_ARGS);
}
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
let parsed_input = get_input(input.as_ref(), path.as_ref());
resource_command::get(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
}
},
ResourceSubCommand::Set { resource, version, input, file: path, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
let parsed_input = get_input(input.as_ref(), path.as_ref());
resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
},
ResourceSubCommand::Test { resource, version, input, file: path, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
let parsed_input = get_input(input.as_ref(), path.as_ref());
resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
},
ResourceSubCommand::Delete { resource, version, input, file: path } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
let parsed_input = get_input(input.as_ref(), path.as_ref());
resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input);
},
}
Expand Down
112 changes: 107 additions & 5 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Op
info!("Trace-level is {:?}", tracing_setting.level);
}

pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_stdin: bool) -> String {
pub fn get_input(input: Option<&String>, file: Option<&String>) -> String {
trace!("Input: {input:?}, File: {file:?}");
let value = if let Some(input) = input {
debug!("{}", t!("util.readingInput"));
Expand All @@ -448,10 +448,6 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_
// check if need to read from STDIN
if path == "-" {
info!("{}", t!("util.readingInputFromStdin"));
if parameters_from_stdin {
error!("{}", t!("util.stdinNotAllowedForBothParametersAndInput"));
exit(EXIT_INVALID_INPUT);
}
let mut stdin = Vec::<u8>::new();
match std::io::stdin().read_to_end(&mut stdin) {
Ok(_) => {
Expand Down Expand Up @@ -582,3 +578,109 @@ pub fn in_desired_state(test_result: &ResourceTestResult) -> bool {
}
}
}

/// Parse input string as JSON or YAML and return a serde_json::Value.
///
/// # Arguments
///
/// * `input` - The input string to parse (JSON or YAML format)
/// * `context` - Context string for error messages (e.g., "file parameters", "inline parameters")
///
/// # Returns
///
/// * `Result<serde_json::Value, DscError>` - Parsed JSON value
///
/// # Errors
///
/// This function will return an error if the input cannot be parsed as valid JSON or YAML
fn parse_input_to_json_value(input: &str, context: &str) -> Result<serde_json::Value, DscError> {
match serde_json::from_str(input) {
Ok(json) => Ok(json),
Err(_) => {
match serde_yaml::from_str::<serde_yaml::Value>(input) {
Ok(yaml) => Ok(serde_json::to_value(yaml)?),
Err(err) => {
Err(DscError::Parser(t!(&format!("util.failedToParse{context}"), error = err.to_string()).to_string()))
}
}
}
}
}

/// Convert parameter input to a map, handling different formats.
///
/// # Arguments
///
/// * `params` - Parameter string to convert (JSON or YAML format)
/// * `context` - Context string for error messages
///
/// # Returns
///
/// * `Result<serde_json::Map<String, serde_json::Value>, DscError>` - Parameter map
///
/// # Errors
///
/// Returns an error if the input cannot be parsed or is not an object
fn params_to_map(params: &str, context: &str) -> Result<serde_json::Map<String, serde_json::Value>, DscError> {
let value = parse_input_to_json_value(params, context)?;

let Some(map) = value.as_object().cloned() else {
return Err(DscError::Parser(t!("util.parametersNotObject").to_string()));
};

Ok(map)
}

/// Merge two parameter sets, with inline parameters taking precedence over file parameters.
/// Top-level keys (like "parameters") are merged recursively, but parameter values themselves
/// are replaced (not merged) when specified inline.
///
/// # Arguments
///
/// * `file_params` - Parameters from file (JSON or YAML format)
/// * `inline_params` - Inline parameters (JSON or YAML format) that take precedence
///
/// # Returns
///
/// * `Result<String, DscError>` - Merged parameters as JSON string
///
/// # Errors
///
/// This function will return an error if:
/// - Either parameter set cannot be parsed as valid JSON or YAML
/// - The merged result cannot be serialized to JSON
pub fn merge_parameters(file_params: &str, inline_params: &str) -> Result<String, DscError> {
use serde_json::Value;

// Convert both parameter inputs to maps
let mut file_map = params_to_map(file_params, "FileParameters")?;
let inline_map = params_to_map(inline_params, "InlineParameters")?;

// Merge top-level keys
for (key, inline_value) in &inline_map {
if key == "parameters" {
// Special handling for "parameters" key - merge at parameter name level only
// Within each parameter name, inline replaces (not merges)
if let Some(file_params_value) = file_map.get_mut("parameters") {
if let (Some(file_params_obj), Some(inline_params_obj)) = (file_params_value.as_object_mut(), inline_value.as_object()) {
// For each parameter in inline, replace (not merge) in file
for (param_name, param_value) in inline_params_obj {
file_params_obj.insert(param_name.clone(), param_value.clone());
}
} else {
// If one is not an object, inline replaces completely
file_map.insert(key.clone(), inline_value.clone());
}
} else {
// "parameters" key doesn't exist in file, add it
file_map.insert(key.clone(), inline_value.clone());
}
} else {
// For other top-level keys, inline value replaces file value
file_map.insert(key.clone(), inline_value.clone());
}
}

let merged = Value::Object(file_map);
Ok(serde_json::to_string(&merged)?)
}
Loading