From 37a6f44ada541223e6ee1c2c053263e857aa1544 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 24 Apr 2024 15:53:00 -0700 Subject: [PATCH 01/15] start include work --- dsc/include.dsc.resource.json | 0 dsc/src/args.rs | 3 +++ dsc/src/include.rs | 3 +++ dsc/src/main.rs | 1 + 4 files changed, 7 insertions(+) create mode 100644 dsc/include.dsc.resource.json create mode 100644 dsc/src/include.rs diff --git a/dsc/include.dsc.resource.json b/dsc/include.dsc.resource.json new file mode 100644 index 00000000..e69de29b diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 83cc6771..a898f08e 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -56,6 +56,9 @@ pub enum SubCommand { parameters_file: Option, #[clap(long, hide = true)] as_group: bool, + // Used for the Microsoft.DSC/Include resource + #[clap(long, hide = true)] + include: Option, }, #[clap(name = "resource", about = "Invoke a specific DSC resource")] Resource { diff --git a/dsc/src/include.rs b/dsc/src/include.rs new file mode 100644 index 00000000..5f34b52a --- /dev/null +++ b/dsc/src/include.rs @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + diff --git a/dsc/src/main.rs b/dsc/src/main.rs index cce6c0da..b5eb4de9 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -16,6 +16,7 @@ use crossterm::event; use std::env; pub mod args; +pub mod include; pub mod resource_command; pub mod subcommand; pub mod tablewriter; From 93a9428f9db8b57c913f8aba625fedc048e939fe Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 28 Apr 2024 17:26:57 -0700 Subject: [PATCH 02/15] Add `Microsoft.DSC/Include` resource with way to have resource accept trace level --- dsc/examples/include.dsc.yaml | 8 ++ dsc/include.dsc.resource.json | 68 +++++++++++ dsc/src/args.rs | 4 +- dsc/src/include.rs | 12 ++ dsc/src/main.rs | 106 +++++++++++++++++- dsc/src/subcommand.rs | 27 +++-- dsc/src/util.rs | 10 +- dsc_lib/src/configure/mod.rs | 5 + dsc_lib/src/dscresources/command_resource.rs | 19 +++- dsc_lib/src/dscresources/resource_manifest.rs | 7 +- 10 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 dsc/examples/include.dsc.yaml diff --git a/dsc/examples/include.dsc.yaml b/dsc/examples/include.dsc.yaml new file mode 100644 index 00000000..6138c48c --- /dev/null +++ b/dsc/examples/include.dsc.yaml @@ -0,0 +1,8 @@ +# This is a simple example of how to Include another configuration into this one + +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +resources: +- name: get os info + type: Microsoft.DSC/Include + properties: + configurationFile: osinfo_parameters.dsc.yaml diff --git a/dsc/include.dsc.resource.json b/dsc/include.dsc.resource.json index e69de29b..a84fc003 100644 --- a/dsc/include.dsc.resource.json +++ b/dsc/include.dsc.resource.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Microsoft.DSC/Include", + "version": "0.1.0", + "description": "Allows including a configuration file into current configuration.", + "kind": "Group", + "get": { + "executable": "dsc", + "args": [ + { + "traceLevelArg": "--trace-level" + }, + "config", + "--as-group", + "--as-include", + "get" + ], + "input": "stdin" + }, + "set": { + "executable": "dsc", + "args": [ + { + "traceLevelArg": "--trace-level" + }, + "config", + "--as-group", + "--as-include", + "set" + ], + "input": "stdin", + "implementsPretest": true, + "return": "state" + }, + "test": { + "executable": "dsc", + "args": [ + { + "traceLevelArg": "--trace-level" + }, + "config", + "--as-group", + "--as-include", + "test" + ], + "input": "stdin", + "return": "state" + }, + "exitCodes": { + "0": "Success", + "1": "Invalid argument", + "2": "Resource error", + "3": "JSON Serialization error", + "4": "Invalid input format", + "5": "Resource instance failed schema validation", + "6": "Command cancelled" + }, + "schema": { + "command": { + "executable": "dsc", + "args": [ + "schema", + "--type", + "include" + ] + } + } + } diff --git a/dsc/src/args.rs b/dsc/src/args.rs index a898f08e..9208c2a5 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -54,11 +54,12 @@ pub enum SubCommand { parameters: Option, #[clap(short = 'f', long, help = "Parameters to pass to the configuration as a JSON or YAML file", conflicts_with = "parameters")] parameters_file: Option, + // Used to inform when DSC is used as a group resource to modify it's output #[clap(long, hide = true)] as_group: bool, // Used for the Microsoft.DSC/Include resource #[clap(long, hide = true)] - include: Option, + as_include: bool, }, #[clap(name = "resource", about = "Invoke a specific DSC resource")] Resource { @@ -208,6 +209,7 @@ pub enum DscType { TestResult, DscResource, ResourceManifest, + Include, Configuration, ConfigurationGetResult, ConfigurationSetResult, diff --git a/dsc/src/include.rs b/dsc/src/include.rs index 5f34b52a..bfb91452 100644 --- a/dsc/src/include.rs +++ b/dsc/src/include.rs @@ -1,3 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct Include { + /// The path to the file to include. Path is relative to the file containing the include + /// and not allowed to reference parent directories. If a configuration document is used + /// instead of a file, then the path is relative to the current working directory. + #[serde(rename = "configurationFile")] + pub configuration_file: String, + pub parameters_file: Option, +} diff --git a/dsc/src/main.rs b/dsc/src/main.rs index b5eb4de9..9ffa4288 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -5,7 +5,11 @@ use args::{Args, SubCommand}; use atty::Stream; use clap::{CommandFactory, Parser}; use clap_complete::generate; +use dsc_lib::configure::config_doc::Configuration; +use crate::include::Include; use std::io::{self, Read}; +use std::fs::File; +use std::path::Path; use std::process::exit; use sysinfo::{Process, ProcessExt, RefreshKind, System, SystemExt, get_current_pid, ProcessRefreshKind}; use tracing::{error, info, warn, debug}; @@ -36,7 +40,7 @@ fn main() { debug!("Running dsc {}", env!("CARGO_PKG_VERSION")); - let input = if atty::is(Stream::Stdin) { + let mut input = if atty::is(Stream::Stdin) { None } else { info!("Reading input from STDIN"); @@ -66,11 +70,15 @@ fn main() { let mut cmd = Args::command(); generate(shell, &mut cmd, "dsc", &mut io::stdout()); }, - SubCommand::Config { subcommand, parameters, parameters_file, as_group } => { + SubCommand::Config { subcommand, parameters, parameters_file, as_group, as_include} => { + if as_include { + input = Some(read_include_file(&input)); + } + if let Some(file_name) = parameters_file { info!("Reading parameters from file {}", file_name); match std::fs::read_to_string(file_name) { - Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group), + Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group, &as_include), Err(err) => { error!("Error: Failed to read parameters file: {err}"); exit(util::EXIT_INVALID_INPUT); @@ -78,7 +86,7 @@ fn main() { } } else { - subcommand::config(&subcommand, ¶meters, &input, &as_group); + subcommand::config(&subcommand, ¶meters, &input, &as_group, &as_include); } }, SubCommand::Resource { subcommand } => { @@ -100,6 +108,96 @@ fn main() { exit(util::EXIT_SUCCESS); } +fn read_include_file(input: &Option) -> String { + let Some(include) = input else { + error!("Error: Include requires input from STDIN"); + exit(util::EXIT_INVALID_INPUT); + }; + + // deserialize the Include input + let include: Include = match serde_json::from_str(include) { + Ok(include) => include, + Err(err) => { + error!("Error: Failed to deserialize Include input: {err}"); + exit(util::EXIT_INVALID_INPUT); + } + }; + + let path = Path::new(&include.configuration_file); + if path.is_absolute() { + error!("Error: Include path must be relative: {}", include.configuration_file); + exit(util::EXIT_INVALID_INPUT); + } + + // check that no components of the path are '..' + if path.components().any(|c| c == std::path::Component::ParentDir) { + error!("Error: Include path must not contain '..': {}", include.configuration_file); + exit(util::EXIT_INVALID_INPUT); + } + + // use DSC_CONFIG_ROOT env var as current directory + let current_directory = match std::env::var("DSC_CONFIG_ROOT") { + Ok(current_directory) => current_directory, + Err(err) => { + error!("Error: Could not read DSC_CONFIG_ROOT env var: {err}"); + exit(util::EXIT_INVALID_INPUT); + } + }; + + // combine the current directory with the Include path + let include_path = Path::new(¤t_directory).join(&include.configuration_file); + + // read the file specified in the Include input + let mut buffer: Vec = Vec::new(); + match File::open(&include_path) { + Ok(mut file) => { + match file.read_to_end(&mut buffer) { + Ok(_) => (), + Err(err) => { + error!("Error: Failed to read file '{include_path:?}': {err}"); + exit(util::EXIT_INVALID_INPUT); + } + } + }, + Err(err) => { + error!("Error: Failed to included file '{include_path:?}': {err}"); + exit(util::EXIT_INVALID_INPUT); + } + } + // convert the buffer to a string + let include_content = match String::from_utf8(buffer) { + Ok(input) => input, + Err(err) => { + error!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}"); + exit(util::EXIT_INVALID_INPUT); + } + }; + + // try to deserialize the Include content as YAML first + let configuration: Configuration = match serde_yaml::from_str(&include_content) { + Ok(configuration) => configuration, + Err(_err) => { + // if that fails, try to deserialize it as JSON + match serde_json::from_str(&include_content) { + Ok(configuration) => configuration, + Err(err) => { + error!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}"); + exit(util::EXIT_INVALID_INPUT); + } + } + } + }; + + // serialize the Configuration as JSON + match serde_json::to_string(&configuration) { + Ok(json) => json, + Err(err) => { + error!("Error: JSON Error: {err}"); + exit(util::EXIT_JSON_ERROR); + } + } +} + fn ctrlc_handler() { warn!("Ctrl-C received"); diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index df75c403..d7eb2d28 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -198,22 +198,27 @@ pub fn config_export(configurator: &mut Configurator, format: &Option, stdin: &Option, as_group: &bool) { +pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: &Option, as_group: &bool, use_stdin: &bool) { let json_string = match subcommand { ConfigSubCommand::Get { document, path, .. } | ConfigSubCommand::Set { document, path, .. } | ConfigSubCommand::Test { document, path, .. } | ConfigSubCommand::Validate { document, path, .. } | ConfigSubCommand::Export { document, path, .. } => { - let mut new_path = path; - let opt_new_path; - if path.is_some() - { + let new_path = if path.is_some() { let config_path = path.clone().unwrap_or_default(); - opt_new_path = Some(set_dscconfigroot(&config_path)); - new_path = &opt_new_path; + Some(set_dscconfigroot(&config_path)) + } else { + // use current working directory + let current_directory = std::env::current_dir().unwrap_or_default(); + Some(current_directory.to_string_lossy().to_string()) + }; + + if *use_stdin { + stdin.clone().unwrap_or_default() + } else { + get_input(document, stdin, &new_path) } - get_input(document, stdin, new_path) } }; @@ -226,8 +231,12 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: }; let parameters: Option = match parameters { - None => None, + None => { + debug!("No parameters specified"); + None + }, Some(parameters) => { + debug!("Parameters specified"); match serde_json::from_str(parameters) { Ok(json) => Some(json), Err(_) => { diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 53cee18f..db764438 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -4,6 +4,7 @@ use crate::args::{DscType, OutputFormat, TraceFormat, TraceLevel}; use atty::Stream; +use crate::include::Include; use dsc_lib::{ configure::{ config_doc::Configuration, @@ -15,13 +16,11 @@ use dsc_lib::{ }, dscerror::DscError, dscresources::{ - dscresource::DscResource, - invoke_result::{ + dscresource::DscResource, invoke_result::{ GetResult, SetResult, TestResult, - }, - resource_manifest::ResourceManifest + }, resource_manifest::ResourceManifest } }; use jsonschema::JSONSchema; @@ -158,6 +157,9 @@ pub fn get_schema(dsc_type: DscType) -> RootSchema { DscType::ResourceManifest => { schema_for!(ResourceManifest) }, + DscType::Include => { + schema_for!(Include) + }, DscType::Configuration => { schema_for!(Configuration) }, diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index d4ee0d9c..d15bc858 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -509,7 +509,9 @@ impl Configurator { }; for (name, parameter) in parameters { + debug!("Processing parameter '{name}'"); if let Some(default_value) = ¶meter.default_value { + debug!("Set default parameter '{name}'"); // default values can be expressions let value = if default_value.is_string() { if let Some(value) = default_value.as_str() { @@ -526,15 +528,18 @@ impl Configurator { } let Some(parameters_input) = parameters_input else { + debug!("No parameters input"); return Ok(()); }; + trace!("parameters_input: {parameters_input}"); let parameters: HashMap = serde_json::from_value::(parameters_input.clone())?.parameters; let Some(parameters_constraints) = &config.parameters else { return Err(DscError::Validation("No parameters defined in configuration".to_string())); }; for (name, value) in parameters { if let Some(constraint) = parameters_constraints.get(&name) { + debug!("Validating parameter '{name}'"); check_length(&name, &value, constraint)?; check_allowed_values(&name, &value, constraint)?; check_number_limits(&name, &value, constraint)?; diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 5f9a5b4d..e0b77d6b 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -7,7 +7,7 @@ use std::{collections::HashMap, env, io::{Read, Write}, process::{Command, Stdio use crate::{dscerror::DscError, dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}}; use crate::configure::config_result::ResourceGetResult; use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, SetResult, TestResult, ValidateResult}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; -use tracing::{error, warn, info, debug, trace}; +use tracing::{debug, error, info, level_filters::LevelFilter, trace, warn}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; @@ -17,6 +17,7 @@ pub fn log_resource_traces(process_name: &str, stderr: &str) if !stderr.is_empty() { for trace_line in stderr.lines() { + // TODO: deserialize tracing JSON to have better presentation if let Result::Ok(json_obj) = serde_json::from_str::(trace_line) { if let Some(msg) = json_obj.get("Error") { error!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); @@ -460,7 +461,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> if let Some(input) = input { if !input.is_empty() { verify_json(resource, cwd, input)?; - + command_input = get_command_input(&export.input, input)?; } @@ -529,7 +530,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option let mut child = command.spawn()?; if let Some(input) = input { - trace!("Writing stdin to command: {input}"); + trace!("Writing to command STDIN: {input}"); // pipe to child stdin in a scope so that it is dropped before we wait // otherwise the pipe isn't closed and the child process waits forever let Some(mut child_stdin) = child.stdin.take() else { @@ -590,6 +591,18 @@ fn process_args(args: &Option>, value: &str) -> Option> processed_args.push(json_input_arg.clone()); processed_args.push(value.to_string()); }, + ArgKind::TraceLevel { trace_level_arg } => { + let trace_level = match tracing::level_filters::STATIC_MAX_LEVEL { + LevelFilter::ERROR => "error", + LevelFilter::WARN => "warning", + LevelFilter::INFO => "info", + LevelFilter::DEBUG => "debug", + LevelFilter::TRACE => "trace", + LevelFilter::OFF => continue, + }; + processed_args.push(trace_level_arg.clone()); + processed_args.push(trace_level.to_string()); + }, } } diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index e54b3944..04efe3fc 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -92,12 +92,17 @@ pub enum ArgKind { /// The argument is a string. String(String), /// The argument accepts the JSON input object. - Json{ + Json { /// The argument that accepts the JSON input object. #[serde(rename = "jsonInputArg")] json_input_arg: String, /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. mandatory: Option, + }, + TraceLevel { + /// The argument that accepts the current directory. + #[serde(rename = "traceLevelArg")] + trace_level_arg: String, } } From d9d3eeed526e2a0d1ad1f11715a302dab019ad54 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 14:27:18 -0700 Subject: [PATCH 03/15] add `Import` kind of resource used by the `Include` resource supporting a `resolve` operation --- dsc/examples/include.dsc.yaml | 1 + dsc/include.dsc.resource.json | 43 +---- dsc/src/args.rs | 19 +- dsc/src/include.rs | 15 -- dsc/src/main.rs | 114 +----------- dsc/src/resolve.rs | 152 +++++++++++++++ dsc/src/resource_command.rs | 4 +- dsc/src/subcommand.rs | 120 ++++++++---- dsc/src/util.rs | 103 +++++----- dsc/tests/dsc_include.tests.ps1 | 126 +++++++++++++ dsc/tests/dsc_tracing.tests.ps1 | 24 ++- dsc_lib/src/configure/mod.rs | 51 ++--- dsc_lib/src/discovery/command_discovery.rs | 28 ++- dsc_lib/src/dscerror.rs | 3 + dsc_lib/src/dscresources/command_resource.rs | 176 ++++++++++++------ dsc_lib/src/dscresources/dscresource.rs | 23 ++- dsc_lib/src/dscresources/include.rs | 13 ++ dsc_lib/src/dscresources/invoke_result.rs | 11 +- dsc_lib/src/dscresources/resource_manifest.rs | 31 ++- dsc_lib/src/lib.rs | 1 + dsc_lib/src/util.rs | 39 ++++ tools/dsctest/dsctrace.dsc.resource.json | 36 ++++ tools/dsctest/src/args.rs | 4 + tools/dsctest/src/main.rs | 16 ++ tools/dsctest/src/trace.rs | 11 ++ tools/test_group_resource/src/main.rs | 34 +--- 26 files changed, 820 insertions(+), 378 deletions(-) delete mode 100644 dsc/src/include.rs create mode 100644 dsc/src/resolve.rs create mode 100644 dsc/tests/dsc_include.tests.ps1 create mode 100644 dsc_lib/src/dscresources/include.rs create mode 100644 dsc_lib/src/util.rs create mode 100644 tools/dsctest/dsctrace.dsc.resource.json create mode 100644 tools/dsctest/src/trace.rs diff --git a/dsc/examples/include.dsc.yaml b/dsc/examples/include.dsc.yaml index 6138c48c..8a3898ac 100644 --- a/dsc/examples/include.dsc.yaml +++ b/dsc/examples/include.dsc.yaml @@ -6,3 +6,4 @@ resources: type: Microsoft.DSC/Include properties: configurationFile: osinfo_parameters.dsc.yaml + parametersFile: osinfo.parameters.yaml diff --git a/dsc/include.dsc.resource.json b/dsc/include.dsc.resource.json index a84fc003..b5c35d94 100644 --- a/dsc/include.dsc.resource.json +++ b/dsc/include.dsc.resource.json @@ -2,49 +2,16 @@ "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", "type": "Microsoft.DSC/Include", "version": "0.1.0", - "description": "Allows including a configuration file into current configuration.", - "kind": "Group", - "get": { + "description": "Allows including a configuration file contents into current configuration.", + "kind": "Import", + "resolve": { "executable": "dsc", "args": [ - { - "traceLevelArg": "--trace-level" - }, "config", - "--as-group", - "--as-include", - "get" - ], - "input": "stdin" - }, - "set": { - "executable": "dsc", - "args": [ - { - "traceLevelArg": "--trace-level" - }, - "config", - "--as-group", - "--as-include", - "set" - ], - "input": "stdin", - "implementsPretest": true, - "return": "state" - }, - "test": { - "executable": "dsc", - "args": [ - { - "traceLevelArg": "--trace-level" - }, - "config", - "--as-group", - "--as-include", - "test" + "resolve" ], "input": "stdin", - "return": "state" + "handlingResourceType": "Microsoft.DSC/Group" }, "exitCodes": { "0": "Success", diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 9208c2a5..a8b48fba 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -21,7 +21,7 @@ pub enum TraceFormat { #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum TraceLevel { Error, - Warning, + Warn, Info, Debug, Trace @@ -33,8 +33,8 @@ pub struct Args { /// The subcommand to run #[clap(subcommand)] pub subcommand: SubCommand, - #[clap(short = 'l', long, help = "Trace level to use", value_enum, default_value = "warning")] - pub trace_level: TraceLevel, + #[clap(short = 'l', long, help = "Trace level to use", value_enum)] + pub trace_level: Option, #[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "default")] pub trace_format: TraceFormat, } @@ -57,9 +57,6 @@ pub enum SubCommand { // Used to inform when DSC is used as a group resource to modify it's output #[clap(long, hide = true)] as_group: bool, - // Used for the Microsoft.DSC/Include resource - #[clap(long, hide = true)] - as_include: bool, }, #[clap(name = "resource", about = "Invoke a specific DSC resource")] Resource { @@ -123,6 +120,15 @@ pub enum ConfigSubCommand { path: Option, #[clap(short = 'f', long, help = "The output format to use")] format: Option, + }, + #[clap(name = "resolve", about = "Resolve the current configuration")] + Resolve { + #[clap(short = 'd', long, help = "The document to pass to the configuration or resource", conflicts_with = "path")] + document: Option, + #[clap(short = 'p', long, help = "The path to a file used as input to the configuration or resource", conflicts_with = "document")] + path: Option, + #[clap(short = 'f', long, help = "The output format to use")] + format: Option, } } @@ -207,6 +213,7 @@ pub enum DscType { GetResult, SetResult, TestResult, + ResolveResult, DscResource, ResourceManifest, Include, diff --git a/dsc/src/include.rs b/dsc/src/include.rs deleted file mode 100644 index bfb91452..00000000 --- a/dsc/src/include.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -pub struct Include { - /// The path to the file to include. Path is relative to the file containing the include - /// and not allowed to reference parent directories. If a configuration document is used - /// instead of a file, then the path is relative to the current working directory. - #[serde(rename = "configurationFile")] - pub configuration_file: String, - pub parameters_file: Option, -} diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 9ffa4288..3f33c63e 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -5,11 +5,7 @@ use args::{Args, SubCommand}; use atty::Stream; use clap::{CommandFactory, Parser}; use clap_complete::generate; -use dsc_lib::configure::config_doc::Configuration; -use crate::include::Include; use std::io::{self, Read}; -use std::fs::File; -use std::path::Path; use std::process::exit; use sysinfo::{Process, ProcessExt, RefreshKind, System, SystemExt, get_current_pid, ProcessRefreshKind}; use tracing::{error, info, warn, debug}; @@ -20,7 +16,7 @@ use crossterm::event; use std::env; pub mod args; -pub mod include; +pub mod resolve; pub mod resource_command; pub mod subcommand; pub mod tablewriter; @@ -40,7 +36,7 @@ fn main() { debug!("Running dsc {}", env!("CARGO_PKG_VERSION")); - let mut input = if atty::is(Stream::Stdin) { + let input = if atty::is(Stream::Stdin) { None } else { info!("Reading input from STDIN"); @@ -70,23 +66,19 @@ fn main() { let mut cmd = Args::command(); generate(shell, &mut cmd, "dsc", &mut io::stdout()); }, - SubCommand::Config { subcommand, parameters, parameters_file, as_group, as_include} => { - if as_include { - input = Some(read_include_file(&input)); - } - + SubCommand::Config { subcommand, parameters, parameters_file, as_group} => { if let Some(file_name) = parameters_file { - info!("Reading parameters from file {}", file_name); - match std::fs::read_to_string(file_name) { - Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group, &as_include), + info!("Reading parameters from file {file_name}"); + match std::fs::read_to_string(&file_name) { + Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group), Err(err) => { - error!("Error: Failed to read parameters file: {err}"); + error!("Error: Failed to read parameters file '{file_name}': {err}"); exit(util::EXIT_INVALID_INPUT); } } } else { - subcommand::config(&subcommand, ¶meters, &input, &as_group, &as_include); + subcommand::config(&subcommand, ¶meters, &input, &as_group); } }, SubCommand::Resource { subcommand } => { @@ -108,96 +100,6 @@ fn main() { exit(util::EXIT_SUCCESS); } -fn read_include_file(input: &Option) -> String { - let Some(include) = input else { - error!("Error: Include requires input from STDIN"); - exit(util::EXIT_INVALID_INPUT); - }; - - // deserialize the Include input - let include: Include = match serde_json::from_str(include) { - Ok(include) => include, - Err(err) => { - error!("Error: Failed to deserialize Include input: {err}"); - exit(util::EXIT_INVALID_INPUT); - } - }; - - let path = Path::new(&include.configuration_file); - if path.is_absolute() { - error!("Error: Include path must be relative: {}", include.configuration_file); - exit(util::EXIT_INVALID_INPUT); - } - - // check that no components of the path are '..' - if path.components().any(|c| c == std::path::Component::ParentDir) { - error!("Error: Include path must not contain '..': {}", include.configuration_file); - exit(util::EXIT_INVALID_INPUT); - } - - // use DSC_CONFIG_ROOT env var as current directory - let current_directory = match std::env::var("DSC_CONFIG_ROOT") { - Ok(current_directory) => current_directory, - Err(err) => { - error!("Error: Could not read DSC_CONFIG_ROOT env var: {err}"); - exit(util::EXIT_INVALID_INPUT); - } - }; - - // combine the current directory with the Include path - let include_path = Path::new(¤t_directory).join(&include.configuration_file); - - // read the file specified in the Include input - let mut buffer: Vec = Vec::new(); - match File::open(&include_path) { - Ok(mut file) => { - match file.read_to_end(&mut buffer) { - Ok(_) => (), - Err(err) => { - error!("Error: Failed to read file '{include_path:?}': {err}"); - exit(util::EXIT_INVALID_INPUT); - } - } - }, - Err(err) => { - error!("Error: Failed to included file '{include_path:?}': {err}"); - exit(util::EXIT_INVALID_INPUT); - } - } - // convert the buffer to a string - let include_content = match String::from_utf8(buffer) { - Ok(input) => input, - Err(err) => { - error!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}"); - exit(util::EXIT_INVALID_INPUT); - } - }; - - // try to deserialize the Include content as YAML first - let configuration: Configuration = match serde_yaml::from_str(&include_content) { - Ok(configuration) => configuration, - Err(_err) => { - // if that fails, try to deserialize it as JSON - match serde_json::from_str(&include_content) { - Ok(configuration) => configuration, - Err(err) => { - error!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}"); - exit(util::EXIT_INVALID_INPUT); - } - } - } - }; - - // serialize the Configuration as JSON - match serde_json::to_string(&configuration) { - Ok(json) => json, - Err(err) => { - error!("Error: JSON Error: {err}"); - exit(util::EXIT_JSON_ERROR); - } - } -} - fn ctrlc_handler() { warn!("Ctrl-C received"); diff --git a/dsc/src/resolve.rs b/dsc/src/resolve.rs new file mode 100644 index 00000000..da248ee2 --- /dev/null +++ b/dsc/src/resolve.rs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use dsc_lib::configure::config_doc::Configuration; +use dsc_lib::util::parse_input_to_json; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::io::Read; +use std::fs::File; +use std::path::{Path, PathBuf}; +use tracing::{debug, info}; + +use crate::util::DSC_CONFIG_ROOT; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct Include { + /// The path to the file to include. Path is relative to the file containing the include + /// and not allowed to reference parent directories. If a configuration document is used + /// instead of a file, then the path is relative to the current working directory. + #[serde(rename = "configurationFile")] + pub configuration_file: String, + #[serde(rename = "parametersFile")] + pub parameters_file: Option, +} + +/// Read the file specified in the Include input and return the content as a JSON string. +/// +/// # Arguments +/// +/// * `input` - The Include input as a JSON string. +/// +/// # Returns +/// +/// A tuple containing the path to the parameters file specified in the Include input and the content of +/// the file as a JSON string. +/// +/// # Errors +/// +/// This function will return an error if the Include input is not valid JSON, if the file +/// specified in the Include input cannot be read, or if the content of the file cannot be +/// deserialized as YAML or JSON. +pub fn get_config(input: &str) -> Result<(Option, String), String> { + debug!("Processing Include input"); + + // deserialize the Include input + let include = match serde_json::from_str::(input) { + Ok(include) => include, + Err(err) => { + return Err(format!("Error: Failed to deserialize Include input: {err}")); + } + }; + + let include_path = normalize_path(Path::new(&include.configuration_file))?; + + // read the file specified in the Include input + let mut buffer: Vec = Vec::new(); + match File::open(&include_path) { + Ok(mut file) => { + match file.read_to_end(&mut buffer) { + Ok(_) => (), + Err(err) => { + return Err(format!("Error: Failed to read file '{include_path:?}': {err}")); + } + } + }, + Err(err) => { + return Err(format!("Error: Failed to open included file '{include_path:?}': {err}")); + } + } + // convert the buffer to a string + let include_content = match String::from_utf8(buffer) { + Ok(input) => input, + Err(err) => { + return Err(format!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}")); + } + }; + + // try to deserialize the Include content as YAML first + let configuration: Configuration = match serde_yaml::from_str(&include_content) { + Ok(configuration) => configuration, + Err(_err) => { + // if that fails, try to deserialize it as JSON + match serde_json::from_str(&include_content) { + Ok(configuration) => configuration, + Err(err) => { + return Err(format!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}")); + } + } + } + }; + + // serialize the Configuration as JSON + let config_json = match serde_json::to_string(&configuration) { + Ok(json) => json, + Err(err) => { + return Err(format!("Error: JSON Error: {err}")); + } + }; + + let parameters = if let Some(parameters_file) = include.parameters_file { + // combine the path with DSC_CONFIG_ROOT + let parameters_file = normalize_path(Path::new(¶meters_file))?; + info!("Resolving parameters from file '{parameters_file:?}'"); + match std::fs::read_to_string(¶meters_file) { + Ok(parameters) => { + let parameters_json = match parse_input_to_json(¶meters) { + Ok(json) => json, + Err(err) => { + return Err(format!("Failed to parse parameters file '{parameters_file:?}' to JSON: {err}")); + } + }; + Some(parameters_json) + }, + Err(err) => { + return Err(format!("Failed to resolve parameters file '{parameters_file:?}': {err}")); + } + } + } else { + debug!("No parameters file found"); + None + }; + + Ok((parameters, config_json)) +} + +fn normalize_path(path: &Path) -> Result { + if path.is_absolute() { + Ok(path.to_path_buf()) + } else { + // check that no components of the path are '..' + if path.components().any(|c| c == std::path::Component::ParentDir) { + return Err(format!("Error: Include path must not contain '..': {path:?}")); + } + + // use DSC_CONFIG_ROOT env var as current directory + let current_directory = match std::env::var(DSC_CONFIG_ROOT) { + Ok(current_directory) => current_directory, + Err(_err) => { + // use current working directory + match std::env::current_dir() { + Ok(current_directory) => current_directory.to_string_lossy().into_owned(), + Err(err) => { + return Err(format!("Error: Failed to get current directory: {err}")); + } + } + } + }; + + // combine the current directory with the Include path + Ok(Path::new(¤t_directory).join(path)) + } +} diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 94d59bb3..a8787aee 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -261,6 +261,6 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, format: &Option(dsc: &'a DscManager, resource: &str) -> Option<&'a DscResource> { - //TODO: add dinamically generated resource to dsc - dsc.find_resource(String::from(resource).to_lowercase().as_str()) + //TODO: add dynamically generated resource to dsc + dsc.find_resource(resource) } diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index d7eb2d28..146bdb8b 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -2,14 +2,15 @@ // Licensed under the MIT License. use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; +use crate::resolve::get_config; use crate::resource_command::{get_resource, self}; use crate::Stream; use crate::tablewriter::Table; -use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; -use dsc_lib::configure::{Configurator, ErrorAction, config_result::ResourceGetResult}; +use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; +use dsc_lib::configure::{Configurator, config_result::ResourceGetResult}; use dsc_lib::dscerror::DscError; use dsc_lib::dscresources::invoke_result::{ - GroupResourceSetResponse, GroupResourceTestResponse, TestResult + GroupResourceSetResponse, GroupResourceTestResponse, ResolveResult, TestResult }; use dsc_lib::{ DscManager, @@ -17,13 +18,13 @@ use dsc_lib::{ dscresources::dscresource::{Capability, ImplementedAs, Invoke}, dscresources::resource_manifest::{import_manifest, ResourceManifest}, }; -use serde_yaml::Value; +use std::collections::HashMap; use std::process::exit; use tracing::{debug, error, trace}; pub fn config_get(configurator: &mut Configurator, format: &Option, as_group: &bool) { - match configurator.invoke_get(ErrorAction::Continue, || { /* code */ }) { + match configurator.invoke_get() { Ok(result) => { if *as_group { let mut group_result = Vec::::new(); @@ -62,7 +63,7 @@ pub fn config_get(configurator: &mut Configurator, format: &Option pub fn config_set(configurator: &mut Configurator, format: &Option, as_group: &bool) { - match configurator.invoke_set(false, ErrorAction::Continue, || { /* code */ }) { + match configurator.invoke_set(false) { Ok(result) => { if *as_group { let group_result = GroupResourceSetResponse { @@ -100,7 +101,7 @@ pub fn config_set(configurator: &mut Configurator, format: &Option pub fn config_test(configurator: &mut Configurator, format: &Option, as_group: &bool, as_get: &bool) { - match configurator.invoke_test(ErrorAction::Continue, || { /* code */ }) { + match configurator.invoke_test() { Ok(result) => { if *as_group { let mut in_desired_state = true; @@ -171,7 +172,7 @@ pub fn config_test(configurator: &mut Configurator, format: &Option) { - match configurator.invoke_export(ErrorAction::Continue, || { /* code */ }) { + match configurator.invoke_export() { Ok(result) => { let json = match serde_json::to_string(&result.result) { Ok(json) => json, @@ -198,27 +199,44 @@ pub fn config_export(configurator: &mut Configurator, format: &Option, stdin: &Option, as_group: &bool, use_stdin: &bool) { - let json_string = match subcommand { +fn initialize_config_root(path: &Option) -> Option { + if path.is_some() { + let config_path = path.clone().unwrap_or_default(); + Some(set_dscconfigroot(&config_path)) + } else if std::env::var(DSC_CONFIG_ROOT).is_ok() { + let config_root = std::env::var(DSC_CONFIG_ROOT).unwrap_or_default(); + debug!("Using {config_root} for {DSC_CONFIG_ROOT}"); + None + } else { + let current_directory = std::env::current_dir().unwrap_or_default(); + debug!("Using current directory '{current_directory:?}' for {DSC_CONFIG_ROOT}"); + set_dscconfigroot(¤t_directory.to_string_lossy()); + None + } +} + +#[allow(clippy::too_many_lines)] +pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: &Option, as_group: &bool) { + let (new_parameters, json_string) = match subcommand { ConfigSubCommand::Get { document, path, .. } | ConfigSubCommand::Set { document, path, .. } | ConfigSubCommand::Test { document, path, .. } | ConfigSubCommand::Validate { document, path, .. } | ConfigSubCommand::Export { document, path, .. } => { - let new_path = if path.is_some() { - let config_path = path.clone().unwrap_or_default(); - Some(set_dscconfigroot(&config_path)) - } else { - // use current working directory - let current_directory = std::env::current_dir().unwrap_or_default(); - Some(current_directory.to_string_lossy().to_string()) + let new_path = initialize_config_root(path); + (None, get_input(document, stdin, &new_path)) + }, + ConfigSubCommand::Resolve { document, path, .. } => { + let new_path = initialize_config_root(path); + let input = get_input(document, stdin, &new_path); + let (new_parameters, config_json) = match get_config(&input) { + Ok((parameters, config_json)) => (parameters, config_json), + Err(err) => { + error!("{err}"); + exit(EXIT_DSC_ERROR); + } }; - - if *use_stdin { - stdin.clone().unwrap_or_default() - } else { - get_input(document, stdin, &new_path) - } + (new_parameters, config_json) } }; @@ -230,7 +248,11 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: } }; - let parameters: Option = match parameters { + let parameters: Option = match if new_parameters.is_some() { + &new_parameters + } else { + parameters + } { None => { debug!("No parameters specified"); None @@ -240,7 +262,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: match serde_json::from_str(parameters) { Ok(json) => Some(json), Err(_) => { - match serde_yaml::from_str::(parameters) { + match serde_yaml::from_str::(parameters) { Ok(yaml) => { match serde_json::to_value(yaml) { Ok(json) => Some(json), @@ -303,7 +325,38 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: }, ConfigSubCommand::Export { format, .. } => { config_export(&mut configurator, format); - } + }, + ConfigSubCommand::Resolve { format, .. } => { + let configuration = match serde_json::from_str(&json_string) { + Ok(json) => json, + Err(err) => { + error!("Error: Failed to deserialize configuration: {err}"); + exit(EXIT_DSC_ERROR); + } + }; + // get the parameters out of the configurator + let parameters_hashmap = if configurator.context.parameters.is_empty() { + None + } else { + let mut parameters: HashMap = HashMap::new(); + for (key, value) in &configurator.context.parameters { + parameters.insert(key.clone(), value.0.clone()); + } + Some(parameters) + }; + let resolve_result = ResolveResult { + configuration, + parameters: parameters_hashmap, + }; + let json_string = match serde_json::to_string(&resolve_result) { + Ok(json) => json, + Err(err) => { + error!("Error: Failed to serialize resolve result: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json_string, format); + }, } } @@ -411,15 +464,15 @@ pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { list_resources(&mut dsc, resource_name, adapter_name, description, tags, format); }, ResourceSubCommand::Schema { resource , format } => { - dsc.find_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_string()]); resource_command::schema(&dsc, resource, format); }, ResourceSubCommand::Export { resource, format } => { - dsc.find_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_string()]); resource_command::export(&mut dsc, resource, format); }, ResourceSubCommand::Get { resource, input, path, all, format } => { - dsc.find_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_string()]); if *all { resource_command::get_all(&dsc, resource, format); } else { let parsed_input = get_input(input, stdin, path); @@ -427,17 +480,17 @@ pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { } }, ResourceSubCommand::Set { resource, input, path, format } => { - dsc.find_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_string()]); let parsed_input = get_input(input, stdin, path); resource_command::set(&dsc, resource, parsed_input, format); }, ResourceSubCommand::Test { resource, input, path, format } => { - dsc.find_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_string()]); let parsed_input = get_input(input, stdin, path); resource_command::test(&dsc, resource, parsed_input, format); }, ResourceSubCommand::Delete { resource, input, path } => { - dsc.find_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_string()]); let parsed_input = get_input(input, stdin, path); resource_command::delete(&dsc, resource, parsed_input); }, @@ -452,7 +505,7 @@ fn list_resources(dsc: &mut DscManager, resource_name: &Option, adapter_ write_table = true; } for resource in dsc.list_available_resources(&resource_name.clone().unwrap_or("*".to_string()), &adapter_name.clone().unwrap_or_default()) { - let mut capabilities = "------".to_string(); + let mut capabilities = "-------".to_string(); let capability_types = [ (Capability::Get, "g"), (Capability::Set, "s"), @@ -460,6 +513,7 @@ fn list_resources(dsc: &mut DscManager, resource_name: &Option, adapter_ (Capability::Test, "t"), (Capability::Delete, "d"), (Capability::Export, "e"), + (Capability::Resolve, "r"), ]; for (i, (capability, letter)) in capability_types.iter().enumerate() { diff --git a/dsc/src/util.rs b/dsc/src/util.rs index db764438..2689afb2 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -4,7 +4,7 @@ use crate::args::{DscType, OutputFormat, TraceFormat, TraceLevel}; use atty::Stream; -use crate::include::Include; +use crate::resolve::Include; use dsc_lib::{ configure::{ config_doc::Configuration, @@ -20,8 +20,10 @@ use dsc_lib::{ GetResult, SetResult, TestResult, + ResolveResult, }, resource_manifest::ResourceManifest - } + }, + util::parse_input_to_json, }; use jsonschema::JSONSchema; use path_absolutize::Absolutize; @@ -49,6 +51,9 @@ pub const EXIT_INVALID_INPUT: i32 = 4; pub const EXIT_VALIDATION_FAILED: i32 = 5; pub const EXIT_CTRL_C: i32 = 6; +pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT"; +pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL"; + /// Get string representation of JSON value. /// /// # Arguments @@ -151,6 +156,9 @@ pub fn get_schema(dsc_type: DscType) -> RootSchema { DscType::TestResult => { schema_for!(TestResult) }, + DscType::ResolveResult => { + schema_for!(ResolveResult) + } DscType::DscResource => { schema_for!(DscResource) }, @@ -259,10 +267,33 @@ pub fn write_output(json: &str, format: &Option) { } } -pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { +pub fn enable_tracing(trace_level: &Option, trace_format: &TraceFormat) { let tracing_level = match trace_level { + Some(level) => level, + None => { + // use DSC_TRACE_LEVEL env var if set + match env::var(DSC_TRACE_LEVEL) { + Ok(level) => { + match level.to_ascii_uppercase().as_str() { + "ERROR" => &TraceLevel::Error, + "WARN" => &TraceLevel::Warn, + "INFO" => &TraceLevel::Info, + "DEBUG" => &TraceLevel::Debug, + "TRACE" => &TraceLevel::Trace, + _ => { + warn!("Invalid DSC_TRACE_LEVEL value '{level}', defaulting to 'warn'"); + &TraceLevel::Warn + }, + } + }, + Err(_) => &TraceLevel::Warn, + } + } + }; + + let tracing_level = match tracing_level { TraceLevel::Error => Level::ERROR, - TraceLevel::Warning => Level::WARN, + TraceLevel::Warn => Level::WARN, TraceLevel::Info => Level::INFO, TraceLevel::Debug => Level::DEBUG, TraceLevel::Trace => Level::TRACE, @@ -308,6 +339,9 @@ pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { if tracing::subscriber::set_global_default(subscriber).is_err() { eprintln!("Unable to set global default tracing subscriber. Tracing is diabled."); } + + // set DSC_TRACE_LEVEL for child processes + env::set_var(DSC_TRACE_LEVEL, tracing_level.to_string().to_ascii_lowercase()); } /// Validate the JSON against the schema. @@ -347,29 +381,6 @@ pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), D Ok(()) } -pub fn parse_input_to_json(value: &str) -> String { - match serde_json::from_str(value) { - Ok(json) => json, - Err(_) => { - match serde_yaml::from_str::(value) { - Ok(yaml) => { - match serde_json::to_value(yaml) { - Ok(json) => json.to_string(), - Err(err) => { - error!("Error: Failed to convert YAML to JSON: {err}"); - exit(EXIT_DSC_ERROR); - } - } - }, - Err(err) => { - error!("Error: Input is not valid JSON or YAML: {err}"); - exit(EXIT_INVALID_INPUT); - } - } - } - } -} - pub fn get_input(input: &Option, stdin: &Option, path: &Option) -> String { let value = match (input, stdin, path) { (Some(_), Some(_), None) | (None, Some(_), Some(_)) => { @@ -418,18 +429,25 @@ pub fn get_input(input: &Option, stdin: &Option, path: &Option json, + Err(err) => { + error!("Error: Invalid JSON or YAML: {err}"); + exit(EXIT_INVALID_INPUT); + } + } } /// Sets `DSC_CONFIG_ROOT` env var and makes path absolute. /// /// # Arguments /// -/// * `config_path` - Full path to the config file +/// * `config_path` - Full path to the config file or directory. /// /// # Returns /// /// Absolute full path to the config file. +/// If a directory is provided, the path returned is the directory path. pub fn set_dscconfigroot(config_path: &str) -> String { let path = Path::new(config_path); @@ -440,24 +458,25 @@ pub fn set_dscconfigroot(config_path: &str) -> String exit(EXIT_DSC_ERROR); }; - let Some(config_root_path) = full_path.parent() else { - // this should never happen because path was absolutized - error!("Error reading config path parent"); - exit(EXIT_DSC_ERROR); + let config_root_path = if full_path.is_file() { + let Some(config_root_path) = full_path.parent() else { + // this should never happen because path was made absolute + error!("Error reading config path parent"); + exit(EXIT_DSC_ERROR); + }; + config_root_path.to_string_lossy().into_owned() + } else { + config_path.to_string() }; - let env_var = "DSC_CONFIG_ROOT"; - // warn if env var is already set/used - if env::var(env_var).is_ok() { - warn!("The current value of '{env_var}' env var will be overridden"); + if env::var(DSC_CONFIG_ROOT).is_ok() { + warn!("The current value of '{DSC_CONFIG_ROOT}' env var will be overridden"); } // Set env var so child processes (of resources) can use it - let config_root = config_root_path.to_str().unwrap_or_default(); - debug!("Setting '{env_var}' env var as '{}'", config_root); - env::set_var(env_var, config_root); + debug!("Setting '{DSC_CONFIG_ROOT}' env var as '{config_root_path}'"); + env::set_var(DSC_CONFIG_ROOT, config_root_path); - // return absolutized path - full_path.to_str().unwrap_or_default().to_string() + full_path.to_string_lossy().into_owned() } diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 new file mode 100644 index 00000000..90a02fd5 --- /dev/null +++ b/dsc/tests/dsc_include.tests.ps1 @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Include tests' { + BeforeAll { + $includePath = New-Item -ItemType Directory -Path (Join-Path $TestDrive 'include') + Copy-Item (Join-Path $PSScriptRoot '../examples/osinfo_parameters.dsc.yaml') -Destination $includePath + $osinfoConfigPath = Get-Item (Join-Path $includePath 'osinfo_parameters.dsc.yaml') + Copy-Item (Join-Path $PSScriptRoot '../examples/osinfo.parameters.yaml') -Destination $includePath + $osinfoParametersConfigPath = Get-Item (Join-Path $includePath 'osinfo.parameters.yaml') + + $logPath = Join-Path $TestDrive 'stderr.log' + + $includeConfig = @' + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: Echo + type: Test/Echo + properties: + output: Hello World +'@ + } + + It 'Include config with default parameters' { + $config = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: include/osinfo_parameters.dsc.yaml +"@ + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $config | Set-Content -Path $configPath + $out = dsc config get -p $configPath | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($IsWindows) { + $expectedOS = 'Windows' + } elseif ($IsLinux) { + $expectedOS = 'Linux' + } else { + $expectedOS = 'macOS' + } + $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS + } + + It 'Include config with parameters file' { + $config = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: include/osinfo_parameters.dsc.yaml + parametersFile: include/osinfo.parameters.yaml +"@ + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $config | Set-Content -Path $configPath + $out = dsc config get -p $configPath | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($IsWindows) { + $expectedOS = 'Windows' + } elseif ($IsLinux) { + $expectedOS = 'Linux' + } else { + $expectedOS = 'macOS' + } + $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS + } + + It 'Invalid file path: ' -TestCases @( + @{ test = 'non-existing configuration'; config = 'include/non-existing.dsc.yaml'; parameters = $null } + @{ test = 'non-existing parameters'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = 'include/non-existing.parameters.yaml' } + @{ test = 'configuration referencing parent directory'; config = '../include/osinfo_parameters.dsc.yaml'; parameters = $null } + @{ test = 'parameters referencing parent directory'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = '../include/non-existing.parameters.yaml' } + ) { + param($config, $parameters) + + $configYaml = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: $config + parametersFile: $parameters +"@ + + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $configYaml | Set-Content -Path $configPath + $out = dsc config get -p $configPath 2> $logPath + $LASTEXITCODE | Should -Be 2 + $log = Get-Content -Path $logPath -Raw + $log | Should -BeLike "*ERROR*" + } + + It 'Valid file path: ' -TestCases @( + @{ test = 'absolute configuration'; config = (Join-Path $TestDrive 'include/osinfo_parameters.dsc.yaml'); parameters = $null } + @{ test = 'absolute parameters'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = (Join-Path $TestDrive 'include/osinfo.parameters.yaml') } + ) { + param($config, $parameters) + + $configYaml = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: $config + parametersFile: $parameters +"@ + + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $configYaml | Set-Content -Path $configPath + $out = dsc config test -p $configPath | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($IsWindows) { + $inDesiredState = $false + } elseif ($IsLinux) { + $inDesiredState = $false + } else { + $inDesiredState = $true + } + $out.results[0].result.inDesiredState | Should -Be $inDesiredState + } +} diff --git a/dsc/tests/dsc_tracing.tests.ps1 b/dsc/tests/dsc_tracing.tests.ps1 index 24ae2edb..75e40ca4 100644 --- a/dsc/tests/dsc_tracing.tests.ps1 +++ b/dsc/tests/dsc_tracing.tests.ps1 @@ -47,7 +47,7 @@ Describe 'tracing tests' { It 'trace level emits source info: ' -TestCases @( @{ level = 'error'; sourceExpected = $false } - @{ level = 'warning'; sourceExpected = $false } + @{ level = 'warn'; sourceExpected = $false } @{ level = 'info'; sourceExpected = $false } @{ level = 'debug'; sourceExpected = $true } @{ level = 'trace'; sourceExpected = $true } @@ -63,4 +63,26 @@ Describe 'tracing tests' { $log | Should -Not -BeLike "*dsc_lib*: *" } } + + It 'trace level is passed to resource' -TestCases @( + @{ level = 'error'; expectedLevel = 'ERROR' } + @{ level = 'warn'; expectedLevel = 'WARN' } + @{ level = 'info'; expectedLevel = 'INFO' } + @{ level = 'debug'; expectedLevel = 'DEBUG'} + @{ level = 'trace'; expectedLevel = 'TRACE'} + ) { + param($level, $expectedLevel) + + $configYaml = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: trace + type: Test/Trace + properties: + level: trace +"@ + + $out = (dsc -l $level config get -d $configYaml 2> $null) | ConvertFrom-Json + $out.results[0].result.actualState.level | Should -BeExactly $expectedLevel + } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index d15bc858..a5c84636 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -31,17 +31,11 @@ pub mod parameters; pub struct Configurator { config: String, - context: Context, + pub context: Context, discovery: Discovery, statement_parser: Statement, } -#[derive(Debug, Clone, Copy)] -pub enum ErrorAction { - Continue, - Stop, -} - /// Add the results of an export operation to a configuration. /// /// # Arguments @@ -217,15 +211,14 @@ impl Configurator { /// Invoke the get operation on a resource. /// - /// # Arguments + /// # Returns /// - /// * `error_action` - The error action to use. - /// * `progress_callback` - A callback to call when progress is made. + /// * `ConfigurationGetResult` - The result of the get operation. /// /// # Errors /// /// This function will return an error if the underlying resource fails. - pub fn invoke_get(&mut self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { + pub fn invoke_get(&mut self) -> Result { let config = self.validate_config()?; let mut result = ConfigurationGetResult::new(); let resources = get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)?; @@ -235,7 +228,7 @@ impl Configurator { Span::current().pb_inc(1); pb_span.pb_set_message(format!("Get '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(&resource.properties)?; - let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { + let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); @@ -275,14 +268,16 @@ impl Configurator { /// /// # Arguments /// - /// * `error_action` - The error action to use. - /// * `progress_callback` - A callback to call when progress is made. + /// * `skip_test` - Whether to skip the test operation. + /// + /// # Returns + /// + /// * `ConfigurationSetResult` - The result of the set operation. /// /// # Errors /// /// This function will return an error if the underlying resource fails. - #[allow(clippy::too_many_lines)] - pub fn invoke_set(&mut self, skip_test: bool, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { + pub fn invoke_set(&mut self, skip_test: bool) -> Result { let config = self.validate_config()?; let mut result = ConfigurationSetResult::new(); let resources = get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)?; @@ -292,7 +287,7 @@ impl Configurator { Span::current().pb_inc(1); pb_span.pb_set_message(format!("Set '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(&resource.properties)?; - let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { + let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); @@ -393,15 +388,14 @@ impl Configurator { /// Invoke the test operation on a resource. /// - /// # Arguments + /// # Returns /// - /// * `error_action` - The error action to use. - /// * `progress_callback` - A callback to call when progress is made. + /// * `ConfigurationTestResult` - The result of the test operation. /// /// # Errors /// /// This function will return an error if the underlying resource fails. - pub fn invoke_test(&mut self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { + pub fn invoke_test(&mut self) -> Result { let config = self.validate_config()?; let mut result = ConfigurationTestResult::new(); let resources = get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)?; @@ -411,7 +405,7 @@ impl Configurator { Span::current().pb_inc(1); pb_span.pb_set_message(format!("Test '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(&resource.properties)?; - let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { + let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; debug!("resource_type {}", &resource.resource_type); @@ -449,11 +443,6 @@ impl Configurator { /// Invoke the export operation on a configuration. /// - /// # Arguments - /// - /// * `error_action` - The error action to use. - /// * `progress_callback` - A callback to call when progress is made. - /// /// # Returns /// /// * `ConfigurationExportResult` - The result of the export operation. @@ -461,7 +450,7 @@ impl Configurator { /// # Errors /// /// This function will return an error if the underlying resource fails. - pub fn invoke_export(&mut self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { + pub fn invoke_export(&mut self) -> Result { let config = self.validate_config()?; let mut result = ConfigurationExportResult::new(); @@ -473,7 +462,7 @@ impl Configurator { Span::current().pb_inc(1); pb_span.pb_set_message(format!("Export '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(&resource.properties)?; - let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { + let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; let input = add_metadata(&dsc_resource.kind, properties)?; @@ -616,9 +605,7 @@ impl Configurator { check_security_context(&config.metadata)?; // Perform discovery of resources used in config - let mut required_resources = config.resources.iter().map(|p| p.resource_type.to_lowercase()).collect::>(); - required_resources.sort_unstable(); - required_resources.dedup(); + let required_resources = config.resources.iter().map(|p| p.resource_type.clone()).collect::>(); self.discovery.find_resources(&required_resources); Ok(config) } diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index fb536db2..8662b103 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -106,7 +106,6 @@ impl ResourceDiscovery for CommandDiscovery { if let Ok(paths) = CommandDiscovery::get_resource_paths() { for path in paths { - trace!("Searching in {:?}", path); if path.exists() && path.is_dir() { for entry in path.read_dir().unwrap() { let entry = entry.unwrap(); @@ -142,10 +141,10 @@ impl ResourceDiscovery for CommandDiscovery { if let Some(ref manifest) = resource.manifest { let manifest = import_manifest(manifest.clone())?; if manifest.kind == Some(Kind::Adapter) { - trace!("Resource adapter {} found", resource.type_name); + trace!("Resource adapter '{}' found", resource.type_name); insert_resource(&mut adapters, &resource, true); } else { - trace!("Resource {} found", resource.type_name); + trace!("Resource '{}' found", resource.type_name); insert_resource(&mut resources, &resource, true); } } @@ -292,8 +291,12 @@ impl ResourceDiscovery for CommandDiscovery { debug!("Searching for resources: {:?}", required_resource_types); self.discover_resources("*")?; + // convert required_resource_types to lowercase to handle case-insentiive search + let mut remaining_required_resource_types = required_resource_types.iter().map(|x| x.to_lowercase()).collect::>(); + remaining_required_resource_types.sort_unstable(); + remaining_required_resource_types.dedup(); + let mut found_resources = BTreeMap::::new(); - let mut remaining_required_resource_types = required_resource_types.to_owned(); for (resource_name, resources) in &self.resources { // TODO: handle version requirements @@ -306,7 +309,7 @@ impl ResourceDiscovery for CommandDiscovery { { // remove the resource from the list of required resources remaining_required_resource_types.retain(|x| *x != resource_name.to_lowercase()); - found_resources.insert(resource_name.to_lowercase(), resource.clone()); + found_resources.insert(resource_name.clone(), resource.clone()); if remaining_required_resource_types.is_empty() { return Ok(found_resources); @@ -327,7 +330,7 @@ impl ResourceDiscovery for CommandDiscovery { { // remove the adapter from the list of required resources remaining_required_resource_types.retain(|x| *x != adapter_name.to_lowercase()); - found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); + found_resources.insert(adapter_name.clone(), adapter.clone()); if remaining_required_resource_types.is_empty() { return Ok(found_resources); @@ -346,10 +349,10 @@ impl ResourceDiscovery for CommandDiscovery { if remaining_required_resource_types.contains(&adapted_name.to_lowercase()) { remaining_required_resource_types.retain(|x| *x != adapted_name.to_lowercase()); - found_resources.insert(adapted_name.to_lowercase(), adapted_resource.clone()); + found_resources.insert(adapted_name.clone(), adapted_resource.clone()); // also insert the adapter - found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); + found_resources.insert(adapter_name.clone(), adapter.clone()); if remaining_required_resource_types.is_empty() { return Ok(found_resources); @@ -436,7 +439,11 @@ fn load_manifest(path: &Path) -> Result { }; // all command based resources are required to support `get` - let mut capabilities = vec![Capability::Get]; + let mut capabilities = if manifest.get.is_some() { + vec![Capability::Get] + } else { + vec![] + }; if let Some(set) = &manifest.set { capabilities.push(Capability::Set); if set.handles_exist == Some(true) { @@ -452,6 +459,9 @@ fn load_manifest(path: &Path) -> Result { if manifest.export.is_some() { capabilities.push(Capability::Export); } + if manifest.resolve.is_some() { + capabilities.push(Capability::Resolve); + } let resource = DscResource { type_name: manifest.resource_type.clone(), diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index fd401eb7..12ab5c1f 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -112,4 +112,7 @@ pub enum DscError { #[error("Validation: {0}")] Validation(String), + + #[error("YAML: {0}")] + Yaml(#[from] serde_yaml::Error), } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index e0b77d6b..cbf17a77 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -4,35 +4,28 @@ use jsonschema::JSONSchema; use serde_json::Value; use std::{collections::HashMap, env, io::{Read, Write}, process::{Command, Stdio}}; +use crate::{configure::{config_result::ResourceGetResult, parameters, Configurator}, util::parse_input_to_json}; use crate::{dscerror::DscError, dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}}; -use crate::configure::config_result::ResourceGetResult; -use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, SetResult, TestResult, ValidateResult}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; -use tracing::{debug, error, info, level_filters::LevelFilter, trace, warn}; +use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; +use tracing::{error, warn, info, debug, trace}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; - -pub fn log_resource_traces(process_name: &str, stderr: &str) -{ - if !stderr.is_empty() - { - for trace_line in stderr.lines() { - // TODO: deserialize tracing JSON to have better presentation - if let Result::Ok(json_obj) = serde_json::from_str::(trace_line) { - if let Some(msg) = json_obj.get("Error") { - error!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); - } else if let Some(msg) = json_obj.get("Warning") { - warn!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); - } else if let Some(msg) = json_obj.get("Info") { - info!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); - } else if let Some(msg) = json_obj.get("Debug") { - debug!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); - } else if let Some(msg) = json_obj.get("Trace") { - trace!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); - }; - }; - } - } +fn get_configurator(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result { + let resolve_result = invoke_resolve(resource, cwd, filter)?; + let configuration = serde_json::to_string(&resolve_result.configuration)?; + let configuration_json = parse_input_to_json(&configuration)?; + let mut configurator = Configurator::new(&configuration_json)?; + let parameters = if let Some(parameters) = resolve_result.parameters { + let parameters_input = parameters::Input { + parameters, + }; + Some(serde_json::to_value(parameters_input)?) + } else { + None + }; + configurator.set_parameters(¶meters)?; + Ok(configurator) } /// Invoke the get operation on a resource @@ -46,18 +39,27 @@ pub fn log_resource_traces(process_name: &str, stderr: &str) /// /// Error returned if the resource does not successfully get the current state pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result { + debug!("Invoking get for '{}'", &resource.resource_type); + if resource.kind == Some(Kind::Import) { + let mut configurator = get_configurator(resource, cwd, filter)?; + let config_result = configurator.invoke_get()?; + return Ok(GetResult::Group(config_result.results)); + } + let mut command_input = CommandInput { env: None, stdin: None }; - let args = process_args(&resource.get.args, filter); + let Some(get) = &resource.get else { + return Err(DscError::NotImplemented("get".to_string())); + }; + let args = process_args(&get.args, filter); if !filter.is_empty() { verify_json(resource, cwd, filter)?; - - command_input = get_command_input(&resource.get.input, filter)?; + command_input = get_command_input(&get.input, filter)?; } - info!("Invoking get '{}' using '{}'", &resource.resource_type, &resource.get.executable); - let (_exit_code, stdout, stderr) = invoke_command(&resource.get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; + info!("Invoking get '{}' using '{}'", &resource.resource_type, &get.executable); + let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; if resource.kind == Some(Kind::Resource) { - debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &resource.get.executable); + debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &get.executable); verify_json(resource, cwd, &stdout)?; } @@ -68,7 +70,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul let result: Value = match serde_json::from_str(&stdout) { Ok(r) => {r}, Err(err) => { - return Err(DscError::Operation(format!("Failed to parse JSON from get {}|{}|{} -> {err}", &resource.get.executable, stdout, stderr))) + return Err(DscError::Operation(format!("Failed to parse JSON from get {}|{}|{} -> {err}", &get.executable, stdout, stderr))) } }; GetResult::Resource(ResourceGetResponse{ @@ -92,6 +94,8 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul /// Error returned if the resource does not successfully set the desired state #[allow(clippy::too_many_lines)] pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool) -> Result { + // TODO: support import resources + let Some(set) = &resource.set else { return Err(DscError::NotImplemented("set".to_string())); }; @@ -122,14 +126,17 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te } } - let args = process_args(&resource.get.args, desired); - let command_input = get_command_input(&resource.get.input, desired)?; + let Some(get) = &resource.get else { + return Err(DscError::NotImplemented("get".to_string())); + }; + let args = process_args(&get.args, desired); + let command_input = get_command_input(&get.input, desired)?; - info!("Getting current state for set by invoking get {} using {}", &resource.resource_type, &resource.get.executable); - let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; + info!("Getting current state for set by invoking get {} using {}", &resource.resource_type, &get.executable); + let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; if resource.kind == Some(Kind::Resource) { - debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &resource.get.executable); + debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &get.executable); verify_json(resource, cwd, &stdout)?; } @@ -236,6 +243,8 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te /// /// Error is returned if the underlying command returns a non-zero exit code. pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result { + // TODO: support import resources + let Some(test) = &resource.test else { info!("Resource '{}' does not implement test, performing synthetic test", &resource.resource_type); return invoke_synthetic_test(resource, cwd, expected); @@ -450,12 +459,10 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result) -> Result { - let Some(export) = resource.export.as_ref() else { return Err(DscError::Operation(format!("Export is not supported by resource {}", &resource.resource_type))) }; - let mut command_input: CommandInput = CommandInput { env: None, stdin: None }; let args: Option>; if let Some(input) = input { @@ -481,7 +488,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> } }; if resource.kind == Some(Kind::Resource) { - debug!("Verifying output of export '{}' using '{}'", &resource.resource_type, &resource.get.executable); + debug!("Verifying output of export '{}' using '{}'", &resource.resource_type, &export.executable); verify_json(resource, cwd, line)?; } instances.push(instance); @@ -492,6 +499,35 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> }) } +/// Invoke the resolve operation on a resource +/// +/// # Arguments +/// +/// * `resource` - The resource manifest +/// * `cwd` - The current working directory +/// * `input` - Input to the command +/// +/// # Returns +/// +/// * `ResolveResult` - The result of the resolve operation +/// +/// # Errors +/// +/// Error returned if the resource does not successfully resolve the input +pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Result { + let Some(resolve) = &resource.resolve else { + return Err(DscError::Operation(format!("Resolve is not supported by resource {}", &resource.resource_type))); + }; + + let args = process_args(&resolve.args, input); + let command_input = get_command_input(&resolve.input, input)?; + + info!("Invoking resolve '{}' using '{}'", &resource.resource_type, &resolve.executable); + let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; + let result: ResolveResult = serde_json::from_str(&stdout)?; + Ok(result) +} + /// Invoke a command and return the exit code, stdout, and stderr. /// /// # Arguments @@ -559,16 +595,20 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option if !stdout.is_empty() { trace!("STDOUT returned: {}", &stdout); } - if !stderr.is_empty() { - trace!("STDERR returned: {}", &stderr); - log_resource_traces(executable, &stderr); - } + let cleaned_stderr = if stderr.is_empty() { + stderr + } else { + trace!("STDERR returned data to be traced"); + log_resource_traces(executable, &child.id(), &stderr); + // TODO: remove logged traces from STDERR + String::new() + }; if exit_code != 0 { - return Err(DscError::Command(executable.to_string(), exit_code, stderr)); + return Err(DscError::Command(executable.to_string(), exit_code, cleaned_stderr)); } - Ok((exit_code, stdout, stderr)) + Ok((exit_code, stdout, cleaned_stderr)) } fn process_args(args: &Option>, value: &str) -> Option> { @@ -591,18 +631,6 @@ fn process_args(args: &Option>, value: &str) -> Option> processed_args.push(json_input_arg.clone()); processed_args.push(value.to_string()); }, - ArgKind::TraceLevel { trace_level_arg } => { - let trace_level = match tracing::level_filters::STATIC_MAX_LEVEL { - LevelFilter::ERROR => "error", - LevelFilter::WARN => "warning", - LevelFilter::INFO => "info", - LevelFilter::DEBUG => "debug", - LevelFilter::TRACE => "trace", - LevelFilter::OFF => continue, - }; - processed_args.push(trace_level_arg.clone()); - processed_args.push(trace_level.to_string()); - }, } } @@ -719,3 +747,35 @@ fn json_to_hashmap(json: &str) -> Result, DscError> { } Ok(map) } + +/// Log output from a process as traces. +/// +/// # Arguments +/// +/// * `process_name` - The name of the process +/// * `process_id` - The ID of the process +/// * `stderr` - The stderr output from the process +pub fn log_resource_traces(process_name: &str, process_id: &u32, stderr: &str) +{ + if !stderr.is_empty() + { + for trace_line in stderr.lines() { + if let Result::Ok(json_obj) = serde_json::from_str::(trace_line) { + if let Some(msg) = json_obj.get("Error") { + error!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); + } else if let Some(msg) = json_obj.get("Warning") { + warn!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); + } else if let Some(msg) = json_obj.get("Info") { + info!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); + } else if let Some(msg) = json_obj.get("Debug") { + debug!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); + } else if let Some(msg) = json_obj.get("Trace") { + trace!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); + }; + } else { + // TODO: deserialize tracing JSON to have better presentation + trace!("Process '{process_name}' id {process_id} : {trace_line}"); + } + } + } +} diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 37fa8317..25f5ea69 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest}; +use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest}; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] @@ -56,6 +56,8 @@ pub enum Capability { Delete, /// The resource supports exporting configuration. Export, + /// The resource supports resolving imported configuration. + Resolve, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -168,6 +170,17 @@ pub trait Invoke { /// /// This function will return an error if the underlying resource fails. fn export(&self, input: &str) -> Result; + + /// Invoke the resolve operation on the resource. + /// + /// # Arguments + /// + /// * `input` - The input to the operation to be resolved. + /// + /// # Errors + /// + /// This function will return an error if the underlying resource fails. + fn resolve(&self, input: &str) -> Result; } impl Invoke for DscResource { @@ -296,6 +309,14 @@ impl Invoke for DscResource { let resource_manifest = import_manifest(manifest.clone())?; command_resource::invoke_export(&resource_manifest, &self.directory, Some(input)) } + + fn resolve(&self, input: &str) -> Result { + let Some(manifest) = &self.manifest else { + return Err(DscError::MissingManifest(self.type_name.clone())); + }; + let resource_manifest = import_manifest(manifest.clone())?; + command_resource::invoke_resolve(&resource_manifest, &self.directory, input) + } } #[must_use] diff --git a/dsc_lib/src/dscresources/include.rs b/dsc_lib/src/dscresources/include.rs new file mode 100644 index 00000000..125d7235 --- /dev/null +++ b/dsc_lib/src/dscresources/include.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct Include { + /// The path to the file to include. Relative paths are relative to the file containing the include + /// and not allowed to reference parent directories. + #[serde(rename = "configurationFile")] + pub configuration_file: String, +} diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index 5df73e37..78525fd6 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; - +use std::collections::HashMap; use crate::configure::config_result::{ResourceGetResult, ResourceSetResult, ResourceTestResult}; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -147,3 +147,12 @@ pub struct ExportResult { #[serde(rename = "actualState")] pub actual_state: Vec, } + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResolveResult { + /// The resolved configuration. + pub configuration: Value, + /// The optional resolved parameters. + pub parameters: Option>, +} diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index 04efe3fc..016af1b1 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -13,10 +13,11 @@ use crate::dscerror::DscError; pub enum Kind { Adapter, Group, + Import, Resource, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ResourceManifest { /// The version of the resource manifest schema. @@ -35,7 +36,7 @@ pub struct ResourceManifest { /// Tags for the resource. pub tags: Option>, /// Details how to call the Get method of the resource. - pub get: GetMethod, + pub get: Option, /// Details how to call the Set method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub set: Option, @@ -48,6 +49,9 @@ pub struct ResourceManifest { /// Details how to call the Export method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub export: Option, + /// Details how to call the Resolve method of the resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolve: Option, /// Details how to call the Validate method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub validate: Option, @@ -99,11 +103,6 @@ pub enum ArgKind { /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. mandatory: Option, }, - TraceLevel { - /// The argument that accepts the current directory. - #[serde(rename = "traceLevelArg")] - trace_level_arg: String, - } } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -147,7 +146,7 @@ pub enum ReturnKind { StateAndDiff, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct GetMethod { /// The command to run to get the state of the resource. pub executable: String, @@ -220,6 +219,22 @@ pub struct ExportMethod { pub input: Option, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct ResolveMethod { + /// The command to run to enumerate instances of the resource. + pub executable: String, + /// The resource type to pass execution after resolving. + #[serde(rename = "handlingResourceType")] + pub handling_resource_type: String, + /// The resource version to pass execution after resolving. + #[serde(rename = "handlingResourceVersion")] + pub handling_resource_version: Option, + /// The arguments to pass to the command to perform a Export. + pub args: Option>, + /// How to pass input for a Export. + pub input: Option, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct Adapter { /// The way to list adapter supported resources. diff --git a/dsc_lib/src/lib.rs b/dsc_lib/src/lib.rs index 743e6b1c..482b9e9b 100644 --- a/dsc_lib/src/lib.rs +++ b/dsc_lib/src/lib.rs @@ -10,6 +10,7 @@ pub mod dscerror; pub mod dscresources; pub mod functions; pub mod parser; +pub mod util; pub struct DscManager { discovery: discovery::Discovery, diff --git a/dsc_lib/src/util.rs b/dsc_lib/src/util.rs new file mode 100644 index 00000000..a5bbcccf --- /dev/null +++ b/dsc_lib/src/util.rs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dscerror::DscError; +use serde_json::Value; + +/// Return JSON string whether the input is JSON or YAML +/// +/// # Arguments +/// +/// * `value` - A string slice that holds the input value +/// +/// # Returns +/// +/// A string that holds the JSON value +/// +/// # Errors +/// +/// This function will return an error if the input value is not valid JSON or YAML +pub fn parse_input_to_json(value: &str) -> Result { + match serde_json::from_str(value) { + Ok(json) => Ok(json), + Err(_) => { + match serde_yaml::from_str::(value) { + Ok(yaml) => { + match serde_json::to_value(yaml) { + Ok(json) => Ok(json.to_string()), + Err(err) => { + Err(DscError::Json(err)) + } + } + }, + Err(err) => { + Err(DscError::Yaml(err)) + } + } + } + } +} diff --git a/tools/dsctest/dsctrace.dsc.resource.json b/tools/dsctest/dsctrace.dsc.resource.json new file mode 100644 index 00000000..b5b93c04 --- /dev/null +++ b/tools/dsctest/dsctrace.dsc.resource.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Test/Trace", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "trace" + ], + "input": "stdin" + }, + "set": { + "executable": "dsctest", + "args": [ + "trace" + ], + "input": "stdin" + }, + "test": { + "executable": "dsctest", + "args": [ + "trace" + ], + "input": "stdin" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "trace" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 562ce8b9..f74c508b 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -9,6 +9,7 @@ pub enum Schemas { Echo, Exist, Sleep, + Trace, } #[derive(Debug, Parser)] @@ -50,4 +51,7 @@ pub enum SubCommand { #[clap(name = "input", short, long, help = "The input to the sleep command as JSON")] input: String, }, + + #[clap(name = "trace", about = "The trace level")] + Trace, } diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 931ad334..79684cb3 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -6,6 +6,7 @@ mod delete; mod echo; mod exist; mod sleep; +mod trace; use args::{Args, Schemas, SubCommand}; use clap::Parser; @@ -14,6 +15,7 @@ use crate::delete::Delete; use crate::echo::Echo; use crate::exist::{Exist, State}; use crate::sleep::Sleep; +use crate::trace::Trace; use std::{thread, time::Duration}; fn main() { @@ -70,6 +72,9 @@ fn main() { Schemas::Sleep => { schema_for!(Sleep) }, + Schemas::Trace => { + schema_for!(Trace) + }, }; serde_json::to_string(&schema).unwrap() }, @@ -84,6 +89,17 @@ fn main() { thread::sleep(Duration::from_secs(sleep.seconds)); serde_json::to_string(&sleep).unwrap() }, + SubCommand::Trace => { + // get level from DSC_TRACE_LEVEL env var + let level = match std::env::var("DSC_TRACE_LEVEL") { + Ok(level) => level, + Err(_) => "warn".to_string(), + }; + let trace = trace::Trace { + level, + }; + serde_json::to_string(&trace).unwrap() + }, }; println!("{json}"); diff --git a/tools/dsctest/src/trace.rs b/tools/dsctest/src/trace.rs new file mode 100644 index 00000000..f9d9ff50 --- /dev/null +++ b/tools/dsctest/src/trace.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Trace { + pub level: String, +} diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index cf2e742a..9bff0beb 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -30,20 +30,11 @@ fn main() { resource_type: "Test/TestResource1".to_string(), kind: Some(Kind::Resource), version: "1.0.0".to_string(), - tags: None, - get: GetMethod { + get: Some(GetMethod { executable: String::new(), - args: None, - input: None, - }, - set: None, - test: None, - delete: None, - export: None, - validate: None, - adapter: None, - exit_codes: None, - schema: None, + ..Default::default() + }), + ..Default::default() }).unwrap()), }; let resource2 = DscResource { @@ -64,20 +55,11 @@ fn main() { resource_type: "Test/TestResource2".to_string(), kind: Some(Kind::Resource), version: "1.0.1".to_string(), - tags: None, - get: GetMethod { + get: Some(GetMethod { executable: String::new(), - args: None, - input: None, - }, - set: None, - test: None, - delete: None, - export: None, - validate: None, - adapter: None, - exit_codes: None, - schema: None, + ..Default::default() + }), + ..Default::default() }).unwrap()), }; println!("{}", serde_json::to_string(&resource1).unwrap()); From c82e022d96132364e8ea452ff1ead755608dd884 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 14:38:15 -0700 Subject: [PATCH 04/15] fix test for get --- dsc/src/main.rs | 2 +- dsc/tests/dsc_include.tests.ps1 | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 3f33c63e..0567c77a 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -66,7 +66,7 @@ fn main() { let mut cmd = Args::command(); generate(shell, &mut cmd, "dsc", &mut io::stdout()); }, - SubCommand::Config { subcommand, parameters, parameters_file, as_group} => { + SubCommand::Config { subcommand, parameters, parameters_file, as_group } => { if let Some(file_name) = parameters_file { info!("Reading parameters from file {file_name}"); match std::fs::read_to_string(&file_name) { diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index 90a02fd5..47c7875b 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -112,15 +112,15 @@ Describe 'Include tests' { $configPath = Join-Path $TestDrive 'config.dsc.yaml' $configYaml | Set-Content -Path $configPath - $out = dsc config test -p $configPath | ConvertFrom-Json + $out = dsc config get -p $configPath | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 if ($IsWindows) { - $inDesiredState = $false + $expectedOS = 'Windows' } elseif ($IsLinux) { - $inDesiredState = $false + $expectedOS = 'Linux' } else { - $inDesiredState = $true + $expectedOS = 'macOS' } - $out.results[0].result.inDesiredState | Should -Be $inDesiredState + $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS } } From 5e06410edec06d0af2aa21ca202e4452c6a1ee87 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 14:42:32 -0700 Subject: [PATCH 05/15] remove unneeded handling resource --- dsc/include.dsc.resource.json | 3 +-- dsc_lib/src/dscresources/resource_manifest.rs | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/dsc/include.dsc.resource.json b/dsc/include.dsc.resource.json index b5c35d94..2ee13406 100644 --- a/dsc/include.dsc.resource.json +++ b/dsc/include.dsc.resource.json @@ -10,8 +10,7 @@ "config", "resolve" ], - "input": "stdin", - "handlingResourceType": "Microsoft.DSC/Group" + "input": "stdin" }, "exitCodes": { "0": "Success", diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index 016af1b1..0060659c 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -223,12 +223,6 @@ pub struct ExportMethod { pub struct ResolveMethod { /// The command to run to enumerate instances of the resource. pub executable: String, - /// The resource type to pass execution after resolving. - #[serde(rename = "handlingResourceType")] - pub handling_resource_type: String, - /// The resource version to pass execution after resolving. - #[serde(rename = "handlingResourceVersion")] - pub handling_resource_version: Option, /// The arguments to pass to the command to perform a Export. pub args: Option>, /// How to pass input for a Export. From 5726e95024582702d1aeacace6b2641f2968a58e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 17:30:25 -0700 Subject: [PATCH 06/15] fix tests, add runcommandonset to vscode project --- .vscode/settings.json | 1 + dsc/tests/dsc_include.tests.ps1 | 16 +++++++--- dsc/tests/dsc_tracing.tests.ps1 | 14 ++++----- dsc_lib/src/dscresources/command_resource.rs | 4 ++- osinfo/tests/osinfo.tests.ps1 | 6 ++-- runcommandonset/src/args.rs | 4 +-- runcommandonset/src/main.rs | 30 ++++++++++++++++--- runcommandonset/src/utils.rs | 2 +- .../tests/runcommandonset.set.tests.ps1 | 18 ++--------- 9 files changed, 58 insertions(+), 37 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 09a06fd6..95742eed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "./dsc_lib/Cargo.toml", "./osinfo/Cargo.toml", "./registry/Cargo.toml", + "./runcommandonset/Cargo.toml", "./tools/test_group_resource/Cargo.toml", "./tools/dsctest/Cargo.toml", "./tree-sitter-dscexpression/Cargo.toml", diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index 47c7875b..ee833062 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -94,11 +94,19 @@ Describe 'Include tests' { $log | Should -BeLike "*ERROR*" } - It 'Valid file path: ' -TestCases @( - @{ test = 'absolute configuration'; config = (Join-Path $TestDrive 'include/osinfo_parameters.dsc.yaml'); parameters = $null } - @{ test = 'absolute parameters'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = (Join-Path $TestDrive 'include/osinfo.parameters.yaml') } + It 'Valid absolute file path: ' -TestCases @( + @{ test = 'configuration'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = $null } + @{ test = 'parameters'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = 'include/osinfo.parameters.yaml' } ) { - param($config, $parameters) + param($test, $config, $parameters) + + if ($test -eq 'configuration') { + $config = Join-Path $TestDrive $config + } elseif ($test -eq 'parameters') { + $parameters = Join-Path $TestDrive $parameters + } else { + throw "Invalid test case: $test" + } $configYaml = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json diff --git a/dsc/tests/dsc_tracing.tests.ps1 b/dsc/tests/dsc_tracing.tests.ps1 index 75e40ca4..56698ac0 100644 --- a/dsc/tests/dsc_tracing.tests.ps1 +++ b/dsc/tests/dsc_tracing.tests.ps1 @@ -65,13 +65,13 @@ Describe 'tracing tests' { } It 'trace level is passed to resource' -TestCases @( - @{ level = 'error'; expectedLevel = 'ERROR' } - @{ level = 'warn'; expectedLevel = 'WARN' } - @{ level = 'info'; expectedLevel = 'INFO' } - @{ level = 'debug'; expectedLevel = 'DEBUG'} - @{ level = 'trace'; expectedLevel = 'TRACE'} + @{ level = 'error' } + @{ level = 'warn' } + @{ level = 'info' } + @{ level = 'debug' } + @{ level = 'trace' } ) { - param($level, $expectedLevel) + param($level) $configYaml = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json @@ -83,6 +83,6 @@ Describe 'tracing tests' { "@ $out = (dsc -l $level config get -d $configYaml 2> $null) | ConvertFrom-Json - $out.results[0].result.actualState.level | Should -BeExactly $expectedLevel + $out.results[0].result.actualState.level | Should -BeExactly $level } } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index cbf17a77..1524e227 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -771,9 +771,11 @@ pub fn log_resource_traces(process_name: &str, process_id: &u32, stderr: &str) debug!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); } else if let Some(msg) = json_obj.get("Trace") { trace!("Process '{process_name}' id {process_id} : {}", msg.as_str().unwrap_or_default()); + } else { + // TODO: deserialize tracing JSON to have better presentation + trace!("Process '{process_name}' id {process_id} : {trace_line}"); }; } else { - // TODO: deserialize tracing JSON to have better presentation trace!("Process '{process_name}' id {process_id} : {trace_line}"); } } diff --git a/osinfo/tests/osinfo.tests.ps1 b/osinfo/tests/osinfo.tests.ps1 index 8d75cc98..c42efb51 100644 --- a/osinfo/tests/osinfo.tests.ps1 +++ b/osinfo/tests/osinfo.tests.ps1 @@ -3,7 +3,7 @@ Describe 'osinfo resource tests' { It 'should get osinfo' { - $out = dsc resource get -r Microsoft/osinfo | ConvertFrom-Json + $out = dsc resource get -r Microsoft/OSInfo | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 if ($IsWindows) { $out.actualState.family | Should -BeExactly 'Windows' @@ -31,7 +31,7 @@ Describe 'osinfo resource tests' { else { $invalid = 'Windows' } - $out = "{`"family`": `"$invalid`"}" | dsc resource test -r 'Microsoft/osinfo' | ConvertFrom-Json + $out = "{`"family`": `"$invalid`"}" | dsc resource test -r 'Microsoft/OSInfo' | ConvertFrom-Json $actual = dsc resource get -r Microsoft/OSInfo | ConvertFrom-Json $out.actualState.family | Should -BeExactly $actual.actualState.family $out.actualState.version | Should -BeExactly $actual.actualState.version @@ -41,7 +41,7 @@ Describe 'osinfo resource tests' { } It 'should support export' { - $out = dsc resource export -r Microsoft/osinfo | ConvertFrom-Json + $out = dsc resource export -r Microsoft/OSInfo | ConvertFrom-Json $out.'$schema' | Should -BeExactly 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json' if ($IsWindows) { $out.resources[0].properties.family | Should -BeExactly 'Windows' diff --git a/runcommandonset/src/args.rs b/runcommandonset/src/args.rs index e4816a55..ab546d2d 100644 --- a/runcommandonset/src/args.rs +++ b/runcommandonset/src/args.rs @@ -13,7 +13,7 @@ pub enum TraceFormat { #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum TraceLevel { Error, - Warning, + Warn, Info, Debug, Trace @@ -26,7 +26,7 @@ pub struct Arguments { #[clap(subcommand)] pub subcommand: SubCommand, #[clap(short = 'l', long, help = "Trace level to use", value_enum, default_value = "info")] - pub trace_level: TraceLevel, + pub trace_level: Option, #[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "json")] pub trace_format: TraceFormat, } diff --git a/runcommandonset/src/main.rs b/runcommandonset/src/main.rs index 16cd3543..f5e2a6b5 100644 --- a/runcommandonset/src/main.rs +++ b/runcommandonset/src/main.rs @@ -2,12 +2,12 @@ // Licensed under the MIT License. use atty::Stream; -use clap::{Parser}; +use clap::Parser; use std::{io::{self, Read}, process::exit}; use tracing::{error, warn, debug}; -use args::{Arguments, SubCommand}; -use runcommand::{RunCommand}; +use args::{Arguments, SubCommand, TraceLevel}; +use runcommand::RunCommand; use utils::{enable_tracing, invoke_command, parse_input, EXIT_INVALID_ARGS}; pub mod args; @@ -16,7 +16,29 @@ pub mod utils; fn main() { let args = Arguments::parse(); - enable_tracing(&args.trace_level, &args.trace_format); + let trace_level = match args.trace_level { + Some(trace_level) => trace_level, + None => { + // get from DSC_TRACE_LEVEL env var + if let Ok(trace_level) = std::env::var("DSC_TRACE_LEVEL") { + match trace_level.to_lowercase().as_str() { + "error" => TraceLevel::Error, + "warn" => TraceLevel::Warn, + "info" => TraceLevel::Info, + "debug" => TraceLevel::Debug, + "trace" => TraceLevel::Trace, + _ => { + warn!("Invalid trace level: {trace_level}"); + TraceLevel::Info + } + } + } else { + // default to info + TraceLevel::Info + } + } + }; + enable_tracing(&trace_level, &args.trace_format); warn!("This resource is not idempotent"); let stdin = if atty::is(Stream::Stdin) { diff --git a/runcommandonset/src/utils.rs b/runcommandonset/src/utils.rs index 831b4fc3..62937499 100644 --- a/runcommandonset/src/utils.rs +++ b/runcommandonset/src/utils.rs @@ -65,7 +65,7 @@ pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { // originally implemented in dsc/src/util.rs let tracing_level = match trace_level { TraceLevel::Error => Level::ERROR, - TraceLevel::Warning => Level::WARN, + TraceLevel::Warn => Level::WARN, TraceLevel::Info => Level::INFO, TraceLevel::Debug => Level::DEBUG, TraceLevel::Trace => Level::TRACE, diff --git a/runcommandonset/tests/runcommandonset.set.tests.ps1 b/runcommandonset/tests/runcommandonset.set.tests.ps1 index 94dd30e7..e3188d22 100644 --- a/runcommandonset/tests/runcommandonset.set.tests.ps1 +++ b/runcommandonset/tests/runcommandonset.set.tests.ps1 @@ -76,25 +76,13 @@ Describe 'tests for runcommandonset set' { } It 'Executable does not exist' { - '{ "executable": "foo" }' | dsc resource set -r Microsoft.DSC.Transitional/RunCommandOnSet 2> $TestDrive/output.txt - $actual = Get-Content -Path $TestDrive/output.txt + '{ "executable": "foo" }' | dsc -l trace resource set -r Microsoft.DSC.Transitional/RunCommandOnSet 2> $TestDrive/output.txt + $actual = Get-Content -Path $TestDrive/output.txt -Raw $expected_logging = 'Failed to execute foo: No such file or directory (os error 2)' if ($IsWindows) { $expected_logging = 'Failed to execute foo: program not found' } - $found_logging = $false - ForEach ($line in $actual) { - try { - $log = $line | ConvertFrom-Json - if ($log.fields.message -eq $expected_logging) { - $found_logging = $true - break - } - } catch { - # skip lines that aren't JSON - } - } - $found_logging | Should -Be $true + $actual | Should -BeLike "*$expected_logging*" $LASTEXITCODE | Should -Be 2 } } From b947abaff6023eb1ef28741ad5ddfbfcc456c5ab Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 17:52:09 -0700 Subject: [PATCH 07/15] fix registry resource case --- dsc/tests/dsc_get.tests.ps1 | 4 ++-- dsc/tests/dsc_schema.tests.ps1 | 2 +- dsc/tests/dsc_set.tests.ps1 | 6 +++--- dsc/tests/dsc_test.tests.ps1 | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dsc/tests/dsc_get.tests.ps1 b/dsc/tests/dsc_get.tests.ps1 index a5586e91..f1cf7b65 100644 --- a/dsc/tests/dsc_get.tests.ps1 +++ b/dsc/tests/dsc_get.tests.ps1 @@ -9,7 +9,7 @@ Describe 'resource get tests' { switch ($type) { 'string' { - $resource = 'Microsoft.Windows/registry' + $resource = 'Microsoft.Windows/Registry' } 'json' { $resource = dsc resource list *registry @@ -44,7 +44,7 @@ Describe 'resource get tests' { "Name": "ProductName" } '@ - $testError = & {$json | dsc resource get -r Microsoft.Windows/registry get 2>&1} + $testError = & {$json | dsc resource get -r Microsoft.Windows/Registry get 2>&1} $testError[0] | SHould -match 'error:' $LASTEXITCODE | Should -Be 2 } diff --git a/dsc/tests/dsc_schema.tests.ps1 b/dsc/tests/dsc_schema.tests.ps1 index 48c308bc..1bcef876 100644 --- a/dsc/tests/dsc_schema.tests.ps1 +++ b/dsc/tests/dsc_schema.tests.ps1 @@ -3,7 +3,7 @@ Describe 'config schema tests' { It 'return resource schema' -Skip:(!$IsWindows) { - $schema = dsc resource schema -r Microsoft.Windows/registry + $schema = dsc resource schema -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $schema | Should -Not -BeNullOrEmpty $schema = $schema | ConvertFrom-Json diff --git a/dsc/tests/dsc_set.tests.ps1 b/dsc/tests/dsc_set.tests.ps1 index 0323cf4b..3175c57d 100644 --- a/dsc/tests/dsc_set.tests.ps1 +++ b/dsc/tests/dsc_set.tests.ps1 @@ -88,7 +88,7 @@ Describe 'resource set tests' { } } '@ - $out = $json | dsc resource set -r Microsoft.Windows/registry + $out = $json | dsc resource set -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $result = $out | ConvertFrom-Json $result.afterState.keyPath | Should -Be 'HKCU\1\2\3' @@ -97,7 +97,7 @@ Describe 'resource set tests' { $result.changedProperties | Should -Be @('valueName', 'valueData', '_exist') ($result.psobject.properties | Measure-Object).Count | Should -Be 3 - $out = $json | dsc resource get -r Microsoft.Windows/registry + $out = $json | dsc resource get -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $result = $out | ConvertFrom-Json $result.actualState.keyPath | Should -Be 'HKCU\1\2\3' @@ -111,7 +111,7 @@ Describe 'resource set tests' { "_exist": false } '@ - $out = $json | dsc resource set -r Microsoft.Windows/registry + $out = $json | dsc resource set -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $result = $out | ConvertFrom-Json $result.afterState.keyPath | Should -BeExactly 'HKCU\1' diff --git a/dsc/tests/dsc_test.tests.ps1 b/dsc/tests/dsc_test.tests.ps1 index a108e60f..35999f48 100644 --- a/dsc/tests/dsc_test.tests.ps1 +++ b/dsc/tests/dsc_test.tests.ps1 @@ -10,7 +10,7 @@ Describe 'resource test tests' { } '@ $current = registry config get --input $json - $out = $current | dsc resource test -r Microsoft.Windows/registry + $out = $current | dsc resource test -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $out = $out | ConvertFrom-Json $out.inDesiredState | Should -BeTrue @@ -27,7 +27,7 @@ Describe 'resource test tests' { } } '@ - $out = $json | dsc resource test -r Microsoft.Windows/registry + $out = $json | dsc resource test -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $out = $out | ConvertFrom-Json $out.inDesiredState | Should -BeFalse @@ -45,7 +45,7 @@ Describe 'resource test tests' { } } '@ - $out = $json | dsc resource test -r Microsoft.Windows/registry + $out = $json | dsc resource test -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 $out = $out | ConvertFrom-Json $out.inDesiredState | Should -BeFalse From 1f8e4c69e1c97a9da68f90921ea11a2cbe2deb6a Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 19:15:09 -0700 Subject: [PATCH 08/15] fix case-insensitivity of resource type names --- dsc/tests/dsc_config_get.tests.ps1 | 4 ++-- dsc_lib/src/discovery/command_discovery.rs | 8 ++++---- dsc_lib/src/discovery/mod.rs | 14 +++++++------- osinfo/tests/osinfo.tests.ps1 | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index ea9f25ba..6016835c 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -37,7 +37,7 @@ Describe 'dsc config get tests' { `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json resources: - name: Echo - type: Test/Echo + type: test/echo properties: output: hello "@ @@ -45,7 +45,7 @@ Describe 'dsc config get tests' { $result.hadErrors | Should -BeFalse $result.results.Count | Should -Be 1 $result.results[0].Name | Should -Be 'Echo' - $result.results[0].type | Should -BeExactly 'Test/Echo' + $result.results[0].type | Should -BeExactly 'test/echo' $result.results[0].result.actualState.output | Should -Be 'hello' $result.metadata.'Microsoft.DSC'.version | Should -BeLike '3.*' $result.metadata.'Microsoft.DSC'.operation | Should -BeExactly 'Get' diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 8662b103..ea1ae1c0 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -309,7 +309,7 @@ impl ResourceDiscovery for CommandDiscovery { { // remove the resource from the list of required resources remaining_required_resource_types.retain(|x| *x != resource_name.to_lowercase()); - found_resources.insert(resource_name.clone(), resource.clone()); + found_resources.insert(resource_name.to_lowercase(), resource.clone()); if remaining_required_resource_types.is_empty() { return Ok(found_resources); @@ -330,7 +330,7 @@ impl ResourceDiscovery for CommandDiscovery { { // remove the adapter from the list of required resources remaining_required_resource_types.retain(|x| *x != adapter_name.to_lowercase()); - found_resources.insert(adapter_name.clone(), adapter.clone()); + found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); if remaining_required_resource_types.is_empty() { return Ok(found_resources); @@ -349,10 +349,10 @@ impl ResourceDiscovery for CommandDiscovery { if remaining_required_resource_types.contains(&adapted_name.to_lowercase()) { remaining_required_resource_types.retain(|x| *x != adapted_name.to_lowercase()); - found_resources.insert(adapted_name.clone(), adapted_resource.clone()); + found_resources.insert(adapted_name.to_lowercase(), adapted_resource.clone()); // also insert the adapter - found_resources.insert(adapter_name.clone(), adapter.clone()); + found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); if remaining_required_resource_types.is_empty() { return Ok(found_resources); diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index 4a45664a..20921f7a 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -27,14 +27,14 @@ impl Discovery { } /// List operation for getting available resources based on the filters. - /// + /// /// # Arguments - /// + /// /// * `type_name_filter` - The filter for the resource type name. /// * `adapter_name_filter` - The filter for the adapter name. - /// + /// /// # Returns - /// + /// /// A vector of `DscResource` instances. pub fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str) -> Vec { let discovery_types: Vec> = vec![ @@ -65,13 +65,13 @@ impl Discovery { #[must_use] pub fn find_resource(&self, type_name: &str) -> Option<&DscResource> { - self.resources.get(type_name) + self.resources.get(&type_name.to_lowercase()) } /// Find resources based on the required resource types. - /// + /// /// # Arguments - /// + /// /// * `required_resource_types` - The required resource types. pub fn find_resources(&mut self, required_resource_types: &[String]) { let discovery_types: Vec> = vec![ diff --git a/osinfo/tests/osinfo.tests.ps1 b/osinfo/tests/osinfo.tests.ps1 index c42efb51..5120c3be 100644 --- a/osinfo/tests/osinfo.tests.ps1 +++ b/osinfo/tests/osinfo.tests.ps1 @@ -3,7 +3,7 @@ Describe 'osinfo resource tests' { It 'should get osinfo' { - $out = dsc resource get -r Microsoft/OSInfo | ConvertFrom-Json + $out = dsc resource get -r Microsoft/osInfo | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 if ($IsWindows) { $out.actualState.family | Should -BeExactly 'Windows' From a3082688cb54b52695e2da80c75b984aa8644a5d Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 3 May 2024 19:55:30 -0700 Subject: [PATCH 09/15] update ps-adapter test due to change in behavior --- powershell-adapter/Tests/powershellgroup.config.tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index a562cefb..b36ff5ed 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -135,7 +135,7 @@ Describe 'PowerShell adapter resource tests' { $res.results.result.actualState.result.properties.Prop1 | Should -Be $TestDrive } - It 'DSC_CONFIG_ROOT env var does not exist when config is piped from stdin' -Skip:(!$IsWindows){ + It 'DSC_CONFIG_ROOT env var is cwd when config is piped from stdin' -Skip:(!$IsWindows){ $yaml = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json @@ -149,8 +149,8 @@ Describe 'PowerShell adapter resource tests' { properties: Name: "[envvar('DSC_CONFIG_ROOT')]" "@ - $testError = & {$yaml | dsc config get 2>&1} - $testError | Select-String 'Environment variable not found' -Quiet | Should -BeTrue - $LASTEXITCODE | Should -Be 2 + $out = $yaml | dsc config get | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.result[0].properties.Name | Should -BeExactly (Get-Location).Path } } From c31eef39ad204f0f738715a6d55b00170210d51b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 20:19:44 -0700 Subject: [PATCH 10/15] add nested test --- dsc/tests/dsc_include.tests.ps1 | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index ee833062..9f056b38 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -131,4 +131,52 @@ Describe 'Include tests' { } $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS } + + It 'Multiple includes' { + $echoConfig = @' + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: one + type: Test/Echo + properties: + output: one +'@ + + $echoConfigPath = Join-Path $TestDrive 'echo.dsc.yaml' + $echoConfig | Set-Content -Path $echoConfigPath + + $nestedIncludeConfig = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: nested + type: Microsoft.DSC/Include + properties: + configurationFile: $echoConfigPath +"@ + + $nestedIncludeConfigPath = Join-Path $TestDrive 'nested_include.dsc.yaml' + $nestedIncludeConfig | Set-Content -Path $nestedIncludeConfigPath + + $includeConfig = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: include + type: Microsoft.DSC/Include + properties: + configurationFile: $echoConfigPath + - name: include nested + type: Microsoft.DSC/Include + properties: + configurationFile: $nestedIncludeConfigPath +"@ + + $out = $includeConfig | dsc -l trace config get | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result[0].result.actualState.output | Should -Be 'one' + $out.results[1].result[0].name | Should -Be 'nested' + $out.results[1].result[0].type | Should -Be 'Microsoft.DSC/Include' + $out.results[1].result[0].result[0].name | Should -Be 'one' + $out.results[1].result[0].result[0].type | Should -Be 'Test/Echo' + $out.results[1].result[0].result[0].result[0].actualState.output | Should -Be 'one' + } } From 138ff9e31fc61a86fff1fe80bd532e3b1d95d791 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 20:38:46 -0700 Subject: [PATCH 11/15] update test to include expression --- dsc/tests/dsc_include.tests.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index 9f056b38..e68cb305 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -144,6 +144,9 @@ Describe 'Include tests' { $echoConfigPath = Join-Path $TestDrive 'echo.dsc.yaml' $echoConfig | Set-Content -Path $echoConfigPath + $echoConfigPathParent = Split-Path $echoConfigPath -Parent + $echoConfigPathLeaf = Split-Path $echoConfigPath -Leaf + $directorySeparator = [System.IO.Path]::DirectorySeparatorChar $nestedIncludeConfig = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json @@ -151,7 +154,7 @@ Describe 'Include tests' { - name: nested type: Microsoft.DSC/Include properties: - configurationFile: $echoConfigPath + configurationFile: "[concat('$echoConfigPathParent', '$directorySeparator', '$echoConfigPathLeaf')]" "@ $nestedIncludeConfigPath = Join-Path $TestDrive 'nested_include.dsc.yaml' From 339765fc4d41d3b94e9a614fb1c15ca62f384a6d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 21:00:49 -0700 Subject: [PATCH 12/15] remove trace during test --- dsc/tests/dsc_include.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index e68cb305..9d49dd1b 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -173,7 +173,7 @@ Describe 'Include tests' { configurationFile: $nestedIncludeConfigPath "@ - $out = $includeConfig | dsc -l trace config get | ConvertFrom-Json + $out = $includeConfig | dsc config get | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.results[0].result[0].result.actualState.output | Should -Be 'one' $out.results[1].result[0].name | Should -Be 'nested' From 041d71e285c19c7637701dd50ba4f80015d182c0 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 3 May 2024 21:36:07 -0700 Subject: [PATCH 13/15] fix test by escaping backslashes --- dsc/tests/dsc_include.tests.ps1 | 55 +++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index 9d49dd1b..fab0690a 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -134,43 +134,44 @@ Describe 'Include tests' { It 'Multiple includes' { $echoConfig = @' - $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - - name: one - type: Test/Echo - properties: - output: one +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +resources: +- name: one + type: Test/Echo + properties: + output: one '@ $echoConfigPath = Join-Path $TestDrive 'echo.dsc.yaml' - $echoConfig | Set-Content -Path $echoConfigPath - $echoConfigPathParent = Split-Path $echoConfigPath -Parent - $echoConfigPathLeaf = Split-Path $echoConfigPath -Leaf - $directorySeparator = [System.IO.Path]::DirectorySeparatorChar + $echoConfig | Set-Content -Path $echoConfigPath -Encoding utf8 + # need to escape backslashes for YAML + $echoConfigPathParent = (Split-Path $echoConfigPath -Parent).Replace('\', '\\') + $echoConfigPathLeaf = (Split-Path $echoConfigPath -Leaf).Replace('\', '\\') + $directorySeparator = [System.IO.Path]::DirectorySeparatorChar.ToString().Replace('\', '\\') $nestedIncludeConfig = @" - `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - - name: nested - type: Microsoft.DSC/Include - properties: - configurationFile: "[concat('$echoConfigPathParent', '$directorySeparator', '$echoConfigPathLeaf')]" +`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +resources: +- name: nested + type: Microsoft.DSC/Include + properties: + configurationFile: "[concat('$echoConfigPathParent', '$directorySeparator', '$echoConfigPathLeaf')]" "@ $nestedIncludeConfigPath = Join-Path $TestDrive 'nested_include.dsc.yaml' - $nestedIncludeConfig | Set-Content -Path $nestedIncludeConfigPath + $nestedIncludeConfig | Set-Content -Path $nestedIncludeConfigPath -Encoding utf8 $includeConfig = @" - `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - - name: include - type: Microsoft.DSC/Include - properties: - configurationFile: $echoConfigPath - - name: include nested - type: Microsoft.DSC/Include - properties: - configurationFile: $nestedIncludeConfigPath +`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +resources: +- name: include + type: Microsoft.DSC/Include + properties: + configurationFile: $echoConfigPath +- name: include nested + type: Microsoft.DSC/Include + properties: + configurationFile: $nestedIncludeConfigPath "@ $out = $includeConfig | dsc config get | ConvertFrom-Json From 3c0291a7062ec0f671f2905d2b15a4849879b9a5 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 3 May 2024 21:47:12 -0700 Subject: [PATCH 14/15] hide the resolve subcommand since it's not intended to be used by end users yet --- dsc/src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/src/args.rs b/dsc/src/args.rs index a8b48fba..e973d7c2 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -121,7 +121,7 @@ pub enum ConfigSubCommand { #[clap(short = 'f', long, help = "The output format to use")] format: Option, }, - #[clap(name = "resolve", about = "Resolve the current configuration")] + #[clap(name = "resolve", about = "Resolve the current configuration", hide = true)] Resolve { #[clap(short = 'd', long, help = "The document to pass to the configuration or resource", conflicts_with = "path")] document: Option, From a5239977df37b696a89ecfbacc5524e30ddb4926 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 4 May 2024 08:44:11 -0700 Subject: [PATCH 15/15] fix comment and rename function --- dsc/src/resolve.rs | 6 +++--- dsc/src/subcommand.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dsc/src/resolve.rs b/dsc/src/resolve.rs index da248ee2..5e261466 100644 --- a/dsc/src/resolve.rs +++ b/dsc/src/resolve.rs @@ -31,15 +31,15 @@ pub struct Include { /// /// # Returns /// -/// A tuple containing the path to the parameters file specified in the Include input and the content of -/// the file as a JSON string. +/// A tuple containing the contents of the parameters file as JSON and the configuration content +/// as a JSON string. /// /// # Errors /// /// This function will return an error if the Include input is not valid JSON, if the file /// specified in the Include input cannot be read, or if the content of the file cannot be /// deserialized as YAML or JSON. -pub fn get_config(input: &str) -> Result<(Option, String), String> { +pub fn get_contents(input: &str) -> Result<(Option, String), String> { debug!("Processing Include input"); // deserialize the Include input diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 146bdb8b..0a873e10 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; -use crate::resolve::get_config; +use crate::resolve::get_contents; use crate::resource_command::{get_resource, self}; use crate::Stream; use crate::tablewriter::Table; @@ -229,7 +229,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: ConfigSubCommand::Resolve { document, path, .. } => { let new_path = initialize_config_root(path); let input = get_input(document, stdin, &new_path); - let (new_parameters, config_json) = match get_config(&input) { + let (new_parameters, config_json) = match get_contents(&input) { Ok((parameters, config_json)) => (parameters, config_json), Err(err) => { error!("{err}");