Skip to content
Open
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: 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