Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 2 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 @@ -158,5 +160,4 @@ 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"
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
85 changes: 66 additions & 19 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,63 @@ 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 {
let merged_parameters = if parameters_file.is_some() && parameters.is_some() {
// Both parameters and parameters_file provided - merge them with inline taking precedence
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 {
None
};

// Parse both and merge
if let (Some(file_content), Some(inline_content)) = (file_params, parameters.as_ref()) {
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);
}
}
} else {
parameters.clone()
}
} else if let Some(file_name) = parameters_file {
// Only parameters_file provided
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 +121,22 @@ 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 {
parameters
};

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
90 changes: 85 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,87 @@ pub fn in_desired_state(test_result: &ResourceTestResult) -> bool {
}
}
}

/// Merge two parameter sets, with inline parameters taking precedence over file parameters.
///
/// # 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;

// Parse file parameters
let file_value: Value = match serde_json::from_str(file_params) {
Ok(json) => json,
Err(_) => {
// YAML
match serde_yaml::from_str::<serde_yaml::Value>(file_params) {
Ok(yaml) => serde_json::to_value(yaml)?,
Err(err) => {
return Err(DscError::Parser(format!("Failed to parse file parameters: {err}")));
}
}
}
};

// Parse inline parameters
let inline_value: Value = match serde_json::from_str(inline_params) {
Ok(json) => json,
Err(_) => {
// YAML
match serde_yaml::from_str::<serde_yaml::Value>(inline_params) {
Ok(yaml) => serde_json::to_value(yaml)?,
Err(err) => {
return Err(DscError::Parser(format!("Failed to parse inline parameters: {err}")));
}
}
}
};

// Both must be objects to merge
let Some(mut file_obj) = file_value.as_object().cloned() else {
return Err(DscError::Parser("File parameters must be a JSON object".to_string()));
};

let Some(inline_obj) = inline_value.as_object() else {
return Err(DscError::Parser("Inline parameters must be a JSON object".to_string()));
};

// Special handling for the "parameters" key - merge nested objects
if let (Some(file_params_value), Some(inline_params_value)) = (file_obj.get("parameters"), inline_obj.get("parameters")) {
if let (Some(mut file_params_obj), Some(inline_params_obj)) = (file_params_value.as_object().cloned(), inline_params_value.as_object()) {
// Merge the nested parameters objects
for (key, value) in inline_params_obj {
file_params_obj.insert(key.clone(), value.clone());
}
file_obj.insert("parameters".to_string(), Value::Object(file_params_obj));
} else {
// If one is not an object, inline takes precedence
file_obj.insert("parameters".to_string(), inline_params_value.clone());
}
} else if let Some(inline_params_value) = inline_obj.get("parameters") {
// Only inline has parameters
file_obj.insert("parameters".to_string(), inline_params_value.clone());
}

// Merge other top-level keys: inline parameters override file parameters
for (key, value) in inline_obj {
if key != "parameters" {
file_obj.insert(key.clone(), value.clone());
}
}

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