From e9ba9b1c32f60aeef5dd10086456419075e754e8 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 26 Jan 2024 07:42:37 -0800 Subject: [PATCH 01/14] change config output to have own schema and remove wrapper --- dsc/assertion.dsc.resource.json | 3 + dsc/group.dsc.resource.json | 3 + dsc/src/args.rs | 5 + dsc/src/main.rs | 6 +- dsc/src/subcommand.rs | 121 +++++++++++++++++------- dsc/src/util.rs | 18 +++- dsc_lib/src/configure/config_result.rs | 63 ++++++++++++ dsc_lib/src/dscresources/dscresource.rs | 7 +- 8 files changed, 186 insertions(+), 40 deletions(-) diff --git a/dsc/assertion.dsc.resource.json b/dsc/assertion.dsc.resource.json index f95c1780..7bf691d6 100644 --- a/dsc/assertion.dsc.resource.json +++ b/dsc/assertion.dsc.resource.json @@ -7,6 +7,7 @@ "executable": "dsc", "args": [ "config", + "--as_group", "test" ], "input": "stdin" @@ -15,6 +16,7 @@ "executable": "dsc", "args": [ "config", + "--as_group", "test" ], "input": "stdin", @@ -25,6 +27,7 @@ "executable": "dsc", "args": [ "config", + "--as_group", "test" ], "input": "stdin", diff --git a/dsc/group.dsc.resource.json b/dsc/group.dsc.resource.json index 5ecf8d7c..d5ba944d 100644 --- a/dsc/group.dsc.resource.json +++ b/dsc/group.dsc.resource.json @@ -7,6 +7,7 @@ "executable": "dsc", "args": [ "config", + "--as_group", "get" ], "input": "stdin" @@ -15,6 +16,7 @@ "executable": "dsc", "args": [ "config", + "--as_group", "set" ], "input": "stdin", @@ -25,6 +27,7 @@ "executable": "dsc", "args": [ "config", + "--as_group", "test" ], "input": "stdin", diff --git a/dsc/src/args.rs b/dsc/src/args.rs index ca74549b..c03ea4df 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -54,6 +54,8 @@ 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, + #[clap(long, hide = true)] + as_group: bool, }, #[clap(name = "resource", about = "Invoke a specific DSC resource")] Resource { @@ -185,6 +187,9 @@ pub enum DscType { GetResult, SetResult, TestResult, + GroupResourceGetResult, + GroupResourceSetResult, + GroupResourceTestResult, DscResource, ResourceManifest, Configuration, diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 4e3a5e01..bb9520f1 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -65,11 +65,11 @@ fn main() { let mut cmd = Args::command(); generate(shell, &mut cmd, "dsc", &mut io::stdout()); }, - SubCommand::Config { subcommand, parameters, parameters_file } => { + 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), + Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, as_group), Err(err) => { error!("Error: Failed to read parameters file: {err}"); exit(util::EXIT_INVALID_INPUT); @@ -77,7 +77,7 @@ fn main() { } } else { - subcommand::config(&subcommand, ¶meters, &input); + subcommand::config(&subcommand, ¶meters, &input, as_group); } }, SubCommand::Resource { subcommand } => { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 9145384f..19e200fd 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -10,6 +10,11 @@ use tracing::error; use atty::Stream; use dsc_lib::{ configure::{Configurator, ErrorAction}, + configure::config_result::{ + GroupResourceGetResult, + GroupResourceSetResult, + GroupResourceTestResult, + }, DscManager, dscresources::dscresource::{ImplementedAs, Invoke}, dscresources::resource_manifest::{import_manifest, ResourceManifest}, @@ -18,20 +23,36 @@ use jsonschema::JSONSchema; use serde_yaml::Value; use std::process::exit; -pub fn config_get(configurator: &mut Configurator, format: &Option) +pub fn config_get(configurator: &mut Configurator, format: &Option, as_group: bool) { match configurator.invoke_get(ErrorAction::Continue, || { /* code */ }) { Ok(result) => { - let json = match serde_json::to_string(&result) { - Ok(json) => json, - Err(err) => { - error!("JSON Error: {err}"); - exit(EXIT_JSON_ERROR); + if as_group { + let mut group_result = GroupResourceGetResult::new(); + for resource_result in result.results { + group_result.results.push(resource_result); + } + let json = match serde_json::to_string(&group_result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + } + else { + let json = match serde_json::to_string(&result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + if result.had_errors { + exit(EXIT_DSC_ERROR); } - }; - write_output(&json, format); - if result.had_errors { - exit(EXIT_DSC_ERROR); } }, Err(err) => { @@ -41,20 +62,36 @@ pub fn config_get(configurator: &mut Configurator, format: &Option } } -pub fn config_set(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 */ }) { Ok(result) => { - let json = match serde_json::to_string(&result) { - Ok(json) => json, - Err(err) => { - error!("JSON Error: {err}"); - exit(EXIT_JSON_ERROR); + if as_group { + let mut group_result = GroupResourceSetResult::new(); + for resource_result in result.results { + group_result.results.push(resource_result); + } + let json = match serde_json::to_string(&group_result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + } + else { + let json = match serde_json::to_string(&result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + if result.had_errors { + exit(EXIT_DSC_ERROR); } - }; - write_output(&json, format); - if result.had_errors { - exit(EXIT_DSC_ERROR); } }, Err(err) => { @@ -64,20 +101,36 @@ pub fn config_set(configurator: &mut Configurator, format: &Option } } -pub fn config_test(configurator: &mut Configurator, format: &Option) +pub fn config_test(configurator: &mut Configurator, format: &Option, as_group: bool) { match configurator.invoke_test(ErrorAction::Continue, || { /* code */ }) { Ok(result) => { - let json = match serde_json::to_string(&result) { - Ok(json) => json, - Err(err) => { - error!("JSON Error: {err}"); - exit(EXIT_JSON_ERROR); + if as_group { + let mut group_result = GroupResourceTestResult::new(); + for resource_result in result.results { + group_result.results.push(resource_result); + } + let json = match serde_json::to_string(&group_result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + } + else { + let json = match serde_json::to_string(&result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + if result.had_errors { + exit(EXIT_DSC_ERROR); } - }; - write_output(&json, format); - if result.had_errors { - exit(EXIT_DSC_ERROR); } }, Err(err) => { @@ -116,7 +169,7 @@ pub fn config_export(configurator: &mut Configurator, format: &Option, stdin: &Option) { +pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: &Option, as_group: bool) { let json_string = match subcommand { ConfigSubCommand::Get { document, path, .. } | ConfigSubCommand::Set { document, path, .. } | @@ -170,13 +223,13 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: match subcommand { ConfigSubCommand::Get { format, .. } => { - config_get(&mut configurator, format); + config_get(&mut configurator, format, as_group); }, ConfigSubCommand::Set { format, .. } => { - config_set(&mut configurator, format); + config_set(&mut configurator, format, as_group); }, ConfigSubCommand::Test { format, .. } => { - config_test(&mut configurator, format); + config_test(&mut configurator, format, as_group); }, ConfigSubCommand::Validate { .. } => { validate_config(&json_string); diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 2ca73de9..c2c1699b 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -7,7 +7,14 @@ use atty::Stream; use dsc_lib::{ configure::{ config_doc::Configuration, - config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult} + config_result::{ + GroupResourceGetResult, + GroupResourceSetResult, + GroupResourceTestResult, + ConfigurationGetResult, + ConfigurationSetResult, + ConfigurationTestResult + } }, dscerror::DscError, dscresources::{ @@ -141,6 +148,15 @@ pub fn get_schema(dsc_type: DscType) -> RootSchema { DscType::TestResult => { schema_for!(TestResult) }, + DscType::GroupResourceGetResult => { + schema_for!(GroupResourceGetResult) + }, + DscType::GroupResourceSetResult => { + schema_for!(GroupResourceSetResult) + }, + DscType::GroupResourceTestResult => { + schema_for!(GroupResourceTestResult) + }, DscType::DscResource => { schema_for!(DscResource) }, diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index 96ab11ea..9fc5d03c 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -32,6 +32,27 @@ pub struct ResourceGetResult { pub result: GetResult, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct GroupResourceGetResult { + pub results: Vec, +} + +impl GroupResourceGetResult { + #[must_use] + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } +} + +impl Default for GroupResourceGetResult { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ConfigurationGetResult { @@ -67,6 +88,27 @@ pub struct ResourceSetResult { pub result: SetResult, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct GroupResourceSetResult { + pub results: Vec, +} + +impl GroupResourceSetResult { + #[must_use] + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } +} + +impl Default for GroupResourceSetResult { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ConfigurationSetResult { @@ -102,6 +144,27 @@ pub struct ResourceTestResult { pub result: TestResult, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct GroupResourceTestResult { + pub results: Vec, +} + +impl GroupResourceTestResult { + #[must_use] + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } +} + +impl Default for GroupResourceTestResult { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ConfigurationTestResult { diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 7bdab211..53e23a16 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -71,8 +71,11 @@ impl DscResource { let resource_manifest = import_manifest(manifest.clone())?; if resource_manifest.validate.is_some() { - let Ok(validation_result) = self.validate(input) else { - return Err(DscError::Validation("Validation invocation failed".to_string())); + let validation_result = match self.validate(input) { + Ok(validation_result) => validation_result, + Err(err) => { + return Err(DscError::Validation(format!("Validation failed: {err}"))); + }, }; if !validation_result.valid { return Err(DscError::Validation("Validation failed".to_string())); From d6a1da7f7094422b51bba28e9f036a7cd66a44cc Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 31 Jan 2024 13:15:49 -0800 Subject: [PATCH 02/14] fixes --- dsc/assertion.dsc.resource.json | 6 +++--- dsc/group.dsc.resource.json | 6 +++--- dsc/parallel.dsc.resource.json | 3 +++ dsc/src/main.rs | 8 +++++++- dsc/src/subcommand.rs | 9 +++++++-- dsc_lib/src/dscresources/command_resource.rs | 7 ++++++- osinfo/README.md | 12 ++++++------ 7 files changed, 35 insertions(+), 16 deletions(-) diff --git a/dsc/assertion.dsc.resource.json b/dsc/assertion.dsc.resource.json index 7bf691d6..72345ea7 100644 --- a/dsc/assertion.dsc.resource.json +++ b/dsc/assertion.dsc.resource.json @@ -7,7 +7,7 @@ "executable": "dsc", "args": [ "config", - "--as_group", + "--as-group", "test" ], "input": "stdin" @@ -16,7 +16,7 @@ "executable": "dsc", "args": [ "config", - "--as_group", + "--as-group", "test" ], "input": "stdin", @@ -27,7 +27,7 @@ "executable": "dsc", "args": [ "config", - "--as_group", + "--as-group", "test" ], "input": "stdin", diff --git a/dsc/group.dsc.resource.json b/dsc/group.dsc.resource.json index d5ba944d..6e3fcfca 100644 --- a/dsc/group.dsc.resource.json +++ b/dsc/group.dsc.resource.json @@ -7,7 +7,7 @@ "executable": "dsc", "args": [ "config", - "--as_group", + "--as-group", "get" ], "input": "stdin" @@ -16,7 +16,7 @@ "executable": "dsc", "args": [ "config", - "--as_group", + "--as-group", "set" ], "input": "stdin", @@ -27,7 +27,7 @@ "executable": "dsc", "args": [ "config", - "--as_group", + "--as-group", "test" ], "input": "stdin", diff --git a/dsc/parallel.dsc.resource.json b/dsc/parallel.dsc.resource.json index a74ac490..6f644eba 100644 --- a/dsc/parallel.dsc.resource.json +++ b/dsc/parallel.dsc.resource.json @@ -8,6 +8,7 @@ "args": [ "config", "--parallel", + "--as-group", "get" ], "input": "stdin" @@ -17,6 +18,7 @@ "args": [ "config", "--parallel", + "--as-group", "set" ], "input": "stdin", @@ -28,6 +30,7 @@ "args": [ "config", "--parallel", + "--as-group", "test" ], "input": "stdin", diff --git a/dsc/src/main.rs b/dsc/src/main.rs index bb9520f1..79309fe6 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -136,7 +136,13 @@ fn check_debug() { if env::var("DEBUG_DSC").is_ok() { eprintln!("attach debugger to pid {} and press a key to continue", std::process::id()); loop { - let event = event::read().unwrap(); + let event = match event::read() { + Ok(event) => event, + Err(err) => { + eprintln!("Error: Failed to read event: {err}"); + break; + } + }; if let event::Event::Key(key) = event { // workaround bug in 0.26+ https://github.com/crossterm-rs/crossterm/issues/752#issuecomment-1414909095 if key.kind == event::KeyEventKind::Press { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 19e200fd..47a0c2a4 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -274,7 +274,7 @@ pub fn validate_config(config: &str) { exit(EXIT_INVALID_INPUT); }; - let dsc = match DscManager::new() { + let mut dsc = match DscManager::new() { Ok(dsc) => dsc, Err(err) => { error!("Error: {err}"); @@ -287,14 +287,19 @@ pub fn validate_config(config: &str) { error!("Error: Resources not specified"); exit(EXIT_INVALID_INPUT); }; + for resource_block in resources { let type_name = resource_block["type"].as_str().unwrap_or_else(|| { error!("Error: Resource type not specified"); exit(EXIT_INVALID_INPUT); }); + + // discover the resource + dsc.discover_resources(&[type_name.to_lowercase().to_string()]); + // get the actual resource let Some(resource) = get_resource(&dsc, type_name) else { - error!("Error: Resource type not found"); + error!("Error: Resource type '{type_name}' not found"); exit(EXIT_DSC_ERROR); }; // see if the resource is command based diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 766d82d9..4d9c23b5 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -3,7 +3,7 @@ use jsonschema::JSONSchema; use serde_json::Value; -use std::{collections::HashMap, process::Command, io::{Write, Read}, process::Stdio}; +use std::{collections::HashMap, env, process::Command, io::{Write, Read}, process::Stdio}; use crate::dscerror::DscError; use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, InputKind, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; use tracing::{debug, info}; @@ -427,6 +427,11 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option command.envs(env); } + if executable == "dsc" && env::var("DEBUG_DSC").is_ok() { + // remove this env var from child process as it will fail reading from keyboard to allow attaching + command.env_remove("DEBUG_DSC"); + } + let mut child = command.spawn()?; if input.is_some() { // pipe to child stdin in a scope so that it is dropped before we wait diff --git a/osinfo/README.md b/osinfo/README.md index 96123678..7219ab35 100644 --- a/osinfo/README.md +++ b/osinfo/README.md @@ -19,10 +19,10 @@ Example output: ```json { "$id": "https://developer.microsoft.com/json-schemas/dsc/os_info/20230303/Microsoft.Dsc.OS_Info.schema.json", - "type": "Windows", + "family": "Windows", "version": "10.0.25309", "edition": "Windows 11 Professional", - "bitness": "X64" + "bitness": "64" } ``` @@ -43,10 +43,10 @@ Example output as YAML: ```yaml actual_state: $id: https://developer.microsoft.com/json-schemas/dsc/os_info/20230303/Microsoft.Dsc.OS_Info.schema.json - type: Windows + family: Windows version: 10.0.25309 edition: Windows 11 Professional - bitness: X64 + bitness: 64 ``` ## Performing a `test` @@ -65,10 +65,10 @@ expected_state: type: unknown actual_state: $id: https://developer.microsoft.com/json-schemas/dsc/os_info/20230303/Microsoft.Dsc.OS_Info.schema.json - type: Windows + family: Windows version: 10.0.25309 edition: Windows 11 Professional - bitness: X64 + bitness: 64 diff_properties: - type ``` From 50028eb903797338520f61f6f32624b1fa2ed9b8 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 1 Feb 2024 13:54:19 -0800 Subject: [PATCH 03/14] factor out json schema helper, add more debug tracing --- dsc/src/subcommand.rs | 41 ++++++------------------- dsc/src/util.rs | 21 +++++++++++++ dsc_lib/src/dscresources/dscresource.rs | 5 +++ 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 47a0c2a4..9f59c981 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -4,7 +4,7 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; -use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_SUCCESS, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot}; +use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_SUCCESS, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; use tracing::error; use atty::Stream; @@ -19,7 +19,6 @@ use dsc_lib::{ dscresources::dscresource::{ImplementedAs, Invoke}, dscresources::resource_manifest::{import_manifest, ResourceManifest}, }; -use jsonschema::JSONSchema; use serde_yaml::Value; use std::process::exit; @@ -251,13 +250,6 @@ pub fn validate_config(config: &str) { exit(EXIT_DSC_ERROR); }, }; - let compiled_schema = match JSONSchema::compile(&schema) { - Ok(schema) => schema, - Err(e) => { - error!("Error: Failed to compile schema: {e}"); - exit(EXIT_DSC_ERROR); - }, - }; let config_value = match serde_json::from_str(config) { Ok(config) => config, Err(e) => { @@ -265,13 +257,10 @@ pub fn validate_config(config: &str) { exit(EXIT_INVALID_INPUT); }, }; - if let Err(err) = compiled_schema.validate(&config_value) { - let mut error = "Configuration failed validation: ".to_string(); - for e in err { - error.push_str(&format!("\n{e} ")); - } - error!("{error}"); - exit(EXIT_INVALID_INPUT); + + if let Err(err) = validate_json("Configuration", &schema, &config_value) { + error!("{err}"); + exit(EXIT_VALIDATION_FAILED); }; let mut dsc = match DscManager::new() { @@ -342,23 +331,11 @@ pub fn validate_config(config: &str) { exit(EXIT_DSC_ERROR); }, }; - let compiled_schema = match JSONSchema::compile(&schema) { - Ok(schema) => schema, - Err(e) => { - error!("Error: Failed to compile schema: {e}"); - exit(EXIT_DSC_ERROR); - }, - }; - let properties = resource_block["properties"].clone(); - let validation = compiled_schema.validate(&properties); - if let Err(err) = validation { - let mut error = String::new(); - for e in err { - error.push_str(&format!("{e} ")); - } - error!("Error: Resource {type_name} failed validation: {error}"); + + if let Err(err) = validate_json(&resource.type_name, &schema, &resource_block["properties"]) { + error!("{err}"); exit(EXIT_VALIDATION_FAILED); - }; + } } } } diff --git a/dsc/src/util.rs b/dsc/src/util.rs index c2c1699b..c8f681d6 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -2,6 +2,8 @@ // Licensed under the MIT License. use crate::args::{DscType, OutputFormat, TraceFormat, TraceLevel}; +use jsonschema::JSONSchema; +use serde_json::Value; use atty::Stream; use dsc_lib::{ @@ -389,3 +391,22 @@ pub fn set_dscconfigroot(config_path: &str) debug!("Setting 'DSCConfigRoot' env var as '{}'", config_root); env::set_var("DSCConfigRoot", config_root.clone()); } + +pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { + let compiled_schema = match JSONSchema::compile(&schema) { + Ok(compiled_schema) => compiled_schema, + Err(err) => { + return Err(DscError::Validation(format!("JSON Schema Compilation Error: {err}"))); + } + }; + + if let Err(err) = compiled_schema.validate(&json) { + let mut error = format!("'{source}' failed validation: "); + for e in err { + error.push_str(&format!("\n{e} ")); + } + return Err(DscError::Validation(error)); + }; + + Ok(()) +} diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 53e23a16..b7d7b249 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -6,6 +6,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use tracing::debug; + use super::{command_resource, dscerror, resource_manifest::import_manifest, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] @@ -62,6 +64,7 @@ impl DscResource { } fn validate_input(&self, input: &str) -> Result<(), DscError> { + debug!("Validating input for resource: {}", &self.type_name); if input.is_empty() { return Ok(()); } @@ -71,6 +74,7 @@ impl DscResource { let resource_manifest = import_manifest(manifest.clone())?; if resource_manifest.validate.is_some() { + debug!("Using custom validation"); let validation_result = match self.validate(input) { Ok(validation_result) => validation_result, Err(err) => { @@ -82,6 +86,7 @@ impl DscResource { } } else { + debug!("Using JSON schema validation"); let Ok(schema) = self.schema() else { return Err(DscError::Validation("Schema not available".to_string())); }; From d965a9ac9e0e09883df3d8a4de56d99d84ed5c89 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 5 Feb 2024 20:27:45 -0800 Subject: [PATCH 04/14] separate resource results and group resource results --- dsc/examples/brew.dsc.yaml | 2 +- dsc/src/args.rs | 3 - dsc/src/resource_command.rs | 6 +- dsc/src/subcommand.rs | 173 ++++++++++--------- dsc/src/util.rs | 21 +-- dsc_lib/src/configure/config_result.rs | 21 --- dsc_lib/src/dscresources/command_resource.rs | 110 ++++++++---- dsc_lib/src/dscresources/dscresource.rs | 22 ++- dsc_lib/src/dscresources/invoke_result.rs | 88 +++++++++- 9 files changed, 285 insertions(+), 161 deletions(-) diff --git a/dsc/examples/brew.dsc.yaml b/dsc/examples/brew.dsc.yaml index bf1a94f7..4c9860db 100644 --- a/dsc/examples/brew.dsc.yaml +++ b/dsc/examples/brew.dsc.yaml @@ -9,7 +9,7 @@ resources: - name: os_check type: Microsoft/OSInfo properties: - family: MacOS + family: macOS - name: brew type: DSC.PackageManagement/Brew properties: diff --git a/dsc/src/args.rs b/dsc/src/args.rs index c03ea4df..0ef91242 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -187,9 +187,6 @@ pub enum DscType { GetResult, SetResult, TestResult, - GroupResourceGetResult, - GroupResourceSetResult, - GroupResourceTestResult, DscResource, ResourceManifest, Configuration, diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 2b75a09e..427a796d 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -5,7 +5,7 @@ use crate::args::OutputFormat; use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, add_type_name_to_json, write_output}; use dsc_lib::configure::config_doc::Configuration; use dsc_lib::configure::add_resource_export_results_to_configuration; -use dsc_lib::dscresources::invoke_result::GetResult; +use dsc_lib::dscresources::invoke_result::{GetResult, ResourceGetResponse}; use dsc_lib::dscerror::DscError; use tracing::{error, debug}; @@ -79,9 +79,9 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: &Option json, diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 9f59c981..7630a0e4 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -5,17 +5,22 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_SUCCESS, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; +use dsc_lib::dscerror::DscError; +use dsc_lib::dscresources::invoke_result::{ + GetResult, + SetResult, + TestResult, + GroupResourceGetResponse, + GroupResourceSetResponse, + GroupResourceTestResponse +}; use tracing::error; use atty::Stream; use dsc_lib::{ configure::{Configurator, ErrorAction}, - configure::config_result::{ - GroupResourceGetResult, - GroupResourceSetResult, - GroupResourceTestResult, - }, DscManager, + dscresources::invoke_result::ValidateResult, dscresources::dscresource::{ImplementedAs, Invoke}, dscresources::resource_manifest::{import_manifest, ResourceManifest}, }; @@ -27,9 +32,16 @@ pub fn config_get(configurator: &mut Configurator, format: &Option match configurator.invoke_get(ErrorAction::Continue, || { /* code */ }) { Ok(result) => { if as_group { - let mut group_result = GroupResourceGetResult::new(); + let mut group_result = GroupResourceGetResponse::new(); for resource_result in result.results { - group_result.results.push(resource_result); + match resource_result.result { + GetResult::Group(mut group_response) => { + group_result.results.append(&mut group_response.results); + }, + GetResult::Resource(response) => { + group_result.results.push(response); + } + } } let json = match serde_json::to_string(&group_result) { Ok(json) => json, @@ -66,9 +78,16 @@ pub fn config_set(configurator: &mut Configurator, format: &Option match configurator.invoke_set(false, ErrorAction::Continue, || { /* code */ }) { Ok(result) => { if as_group { - let mut group_result = GroupResourceSetResult::new(); + let mut group_result = GroupResourceSetResponse::new(); for resource_result in result.results { - group_result.results.push(resource_result); + match resource_result.result { + SetResult::Group(mut group_response) => { + group_result.results.append(&mut group_response.results); + }, + SetResult::Resource(response) => { + group_result.results.push(response); + } + } } let json = match serde_json::to_string(&group_result) { Ok(json) => json, @@ -105,9 +124,16 @@ pub fn config_test(configurator: &mut Configurator, format: &Option { if as_group { - let mut group_result = GroupResourceTestResult::new(); + let mut group_result = GroupResourceTestResponse::new(); for resource_result in result.results { - group_result.results.push(resource_result); + match resource_result.result { + TestResult::Group(mut group_response) => { + group_result.results.append(&mut group_response.results); + }, + TestResult::Resource(response) => { + group_result.results.push(response); + } + } } let json = match serde_json::to_string(&group_result) { Ok(json) => json, @@ -230,8 +256,27 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: ConfigSubCommand::Test { format, .. } => { config_test(&mut configurator, format, as_group); }, - ConfigSubCommand::Validate { .. } => { - validate_config(&json_string); + ConfigSubCommand::Validate => { + let mut result = ValidateResult { + valid: true, + reason: None, + }; + let valid = match validate_config(&json_string) { + Ok(_) => { + true + }, + Err(err) => { + error!("{err}"); + result.valid = false; + false + } + }; + + let json = serde_json::to_string(&result).unwrap(); + write_output(&json, format); + if !valid { + exit(EXIT_VALIDATION_FAILED); + } }, ConfigSubCommand::Export { format, .. } => { config_export(&mut configurator, format); @@ -241,107 +286,71 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: /// Validate configuration. #[allow(clippy::too_many_lines)] -pub fn validate_config(config: &str) { +pub fn validate_config(config: &str) -> Result<(), DscError> { // first validate against the config schema - let schema = match serde_json::to_value(get_schema(DscType::Configuration)) { - Ok(schema) => schema, - Err(e) => { - error!("Error: Failed to convert schema to JSON: {e}"); - exit(EXIT_DSC_ERROR); - }, - }; - let config_value = match serde_json::from_str(config) { - Ok(config) => config, - Err(e) => { - error!("Error: Failed to parse configuration: {e}"); - exit(EXIT_INVALID_INPUT); - }, - }; - - if let Err(err) = validate_json("Configuration", &schema, &config_value) { - error!("{err}"); - exit(EXIT_VALIDATION_FAILED); - }; - - let mut dsc = match DscManager::new() { - Ok(dsc) => dsc, - Err(err) => { - error!("Error: {err}"); - exit(EXIT_DSC_ERROR); - } - }; + let schema = serde_json::to_value(get_schema(DscType::Configuration))?; + let config_value = serde_json::from_str(config)?; + validate_json("Configuration", &schema, &config_value)?; + let mut dsc = DscManager::new()?; // then validate each resource let Some(resources) = config_value["resources"].as_array() else { - error!("Error: Resources not specified"); - exit(EXIT_INVALID_INPUT); + return Err(DscError::Validation("Error: Resources not specified".to_string())); }; + // discover the resources + let mut resource_types = Vec::new(); for resource_block in resources { - let type_name = resource_block["type"].as_str().unwrap_or_else(|| { - error!("Error: Resource type not specified"); - exit(EXIT_INVALID_INPUT); - }); + let Some(type_name) = resource_block["type"].as_str() else { + return Err(DscError::Validation("Error: Resource type not specified".to_string())); + }; + + if resource_types.contains(&type_name.to_lowercase()) { + continue; + } - // discover the resource - dsc.discover_resources(&[type_name.to_lowercase().to_string()]); + resource_types.push(type_name.to_lowercase().to_string()); + } + dsc.discover_resources(&resource_types); + + for resource_block in resources { + let Some(type_name) = resource_block["type"].as_str() else { + return Err(DscError::Validation("Error: Resource type not specified".to_string())); + }; // get the actual resource let Some(resource) = get_resource(&dsc, type_name) else { - error!("Error: Resource type '{type_name}' not found"); - exit(EXIT_DSC_ERROR); + return Err(DscError::Validation(format!("Error: Resource type '{type_name}' not found"))); }; + // see if the resource is command based if resource.implemented_as == ImplementedAs::Command { // if so, see if it implements validate via the resource manifest if let Some(manifest) = resource.manifest.clone() { // convert to resource_manifest - let manifest: ResourceManifest = match serde_json::from_value(manifest) { - Ok(manifest) => manifest, - Err(e) => { - error!("Error: Failed to parse resource manifest: {e}"); - exit(EXIT_INVALID_INPUT); - }, - }; + let manifest: ResourceManifest = serde_json::from_value(manifest)?; if manifest.validate.is_some() { - let result = match resource.validate(config) { - Ok(result) => result, - Err(e) => { - error!("Error: Failed to validate resource: {e}"); - exit(EXIT_VALIDATION_FAILED); - }, - }; + let result = resource.validate(config)?; if !result.valid { let reason = result.reason.unwrap_or("No reason provided".to_string()); let type_name = resource.type_name.clone(); - error!("Resource {type_name} failed validation: {reason}"); - exit(EXIT_VALIDATION_FAILED); + return Err(DscError::Validation(format!("Resource {type_name} failed validation: {reason}"))); } } else { // use schema validation let Ok(schema) = resource.schema() else { - error!("Error: Resource {type_name} does not have a schema nor supports validation"); - exit(EXIT_VALIDATION_FAILED); - }; - let schema = match serde_json::to_value(&schema) { - Ok(schema) => schema, - Err(e) => { - error!("Error: Failed to convert schema to JSON: {e}"); - exit(EXIT_DSC_ERROR); - }, + return Err(DscError::Validation(format!("Error: Resource {type_name} does not have a schema nor supports validation"))); }; + let schema = serde_json::from_str(&schema)?; - if let Err(err) = validate_json(&resource.type_name, &schema, &resource_block["properties"]) { - error!("{err}"); - exit(EXIT_VALIDATION_FAILED); - } + validate_json(&resource.type_name, &schema, &resource_block["properties"])?; } } } - } - exit(EXIT_SUCCESS); + + Ok(()) } pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { diff --git a/dsc/src/util.rs b/dsc/src/util.rs index c8f681d6..844961ed 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -10,9 +10,6 @@ use dsc_lib::{ configure::{ config_doc::Configuration, config_result::{ - GroupResourceGetResult, - GroupResourceSetResult, - GroupResourceTestResult, ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult @@ -21,7 +18,11 @@ use dsc_lib::{ dscerror::DscError, dscresources::{ dscresource::DscResource, - invoke_result::{GetResult, SetResult, TestResult}, + invoke_result::{ + GetResult, + SetResult, + TestResult, + }, resource_manifest::ResourceManifest } }; @@ -150,15 +151,6 @@ pub fn get_schema(dsc_type: DscType) -> RootSchema { DscType::TestResult => { schema_for!(TestResult) }, - DscType::GroupResourceGetResult => { - schema_for!(GroupResourceGetResult) - }, - DscType::GroupResourceSetResult => { - schema_for!(GroupResourceSetResult) - }, - DscType::GroupResourceTestResult => { - schema_for!(GroupResourceTestResult) - }, DscType::DscResource => { schema_for!(DscResource) }, @@ -393,6 +385,9 @@ pub fn set_dscconfigroot(config_path: &str) } pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { + debug!("Validating {source} against schema"); + debug!("JSON: {json}"); + debug!("Schema: {schema}"); let compiled_schema = match JSONSchema::compile(&schema) { Ok(compiled_schema) => compiled_schema, Err(err) => { diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index 9fc5d03c..c8d6a776 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -32,27 +32,6 @@ pub struct ResourceGetResult { pub result: GetResult, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct GroupResourceGetResult { - pub results: Vec, -} - -impl GroupResourceGetResult { - #[must_use] - pub fn new() -> Self { - Self { - results: Vec::new(), - } - } -} - -impl Default for GroupResourceGetResult { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ConfigurationGetResult { diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 4d9c23b5..5c60eb81 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -4,7 +4,7 @@ use jsonschema::JSONSchema; use serde_json::Value; use std::{collections::HashMap, env, process::Command, io::{Write, Read}, process::Stdio}; -use crate::dscerror::DscError; +use crate::{dscerror::DscError, dscresources::invoke_result::{GroupResourceGetResponse, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}}; use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, InputKind, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; use tracing::{debug, info}; @@ -53,16 +53,24 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); } - let result: Value = match serde_json::from_str(&stdout){ - Result::Ok(r) => {r}, - Result::Err(err) => { - return Err(DscError::Operation(format!("Failed to parse json from get {}|{}|{} -> {err}", &resource.get.executable, stdout, stderr))) + let result: GetResult = match serde_json::from_str::(&stdout) { + Ok(group_response) => { + GetResult::Group(group_response) + }, + Err(_) => { + match serde_json::from_str::(&stdout) { + Ok(response) => { + GetResult::Resource(response) + }, + Err(_) => { + debug!("Invalid get response: {}", &stdout); + return Err(DscError::Validation(format!("Resource {} did not return a valid response", &resource.resource_type))) + } + } } }; - Ok(GetResult { - actual_state: result, - }) + Ok(result) } /// Invoke the set operation on a resource @@ -101,13 +109,31 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && !set.pre_test.unwrap_or_default() { info!("No pretest, invoking test {}", &resource.resource_type); - let test_result = invoke_test(resource, cwd, desired)?; - if test_result.in_desired_state { - return Ok(SetResult { - before_state: test_result.desired_state, - after_state: test_result.actual_state, + let (in_desired_state, desired_state, actual_state) = match invoke_test(resource, cwd, desired)? { + TestResult::Group(group_response) => { + let mut in_desired_state = true; + let mut result_array: Vec = Vec::new(); + let mut desired_state_array: Vec = Vec::new(); + for response in group_response.results { + if response.in_desired_state == false { + in_desired_state = false; + } + result_array.push(response.actual_state); + desired_state_array.push(response.desired_state); + } + (in_desired_state, Value::from(desired_state_array), Value::from(result_array)) + }, + TestResult::Resource(response) => { + (response.in_desired_state, response.desired_state, response.actual_state) + } + }; + + if in_desired_state { + return Ok(SetResult::Resource(ResourceSetResponse{ + before_state: desired_state, + after_state: actual_state, changed_properties: None, - }); + })); } } @@ -159,11 +185,11 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te // for changed_properties, we compare post state to pre state let diff_properties = get_diff( &actual_value, &pre_state); - Ok(SetResult { + Ok(SetResult::Resource(ResourceSetResponse{ before_state: pre_state, after_state: actual_value, changed_properties: Some(diff_properties), - }) + })) }, Some(ReturnKind::StateAndDiff) => { // command should be returning actual state as a JSON line and a list of properties that differ as separate JSON line @@ -177,22 +203,34 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te return Err(DscError::Command(resource.resource_type.clone(), exit_code, "Command did not return expected diff output".to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_line)?; - Ok(SetResult { + Ok(SetResult::Resource(ResourceSetResponse { before_state: pre_state, after_state: actual_value, changed_properties: Some(diff_properties), - }) + })) }, None => { // perform a get and compare the result to the expected state let get_result = invoke_get(resource, cwd, desired)?; // for changed_properties, we compare post state to pre state - let diff_properties = get_diff( &get_result.actual_state, &pre_state); - Ok(SetResult { + let actual_state = match get_result { + GetResult::Group(group_response) => { + let mut result_array: Vec = Vec::new(); + for result in group_response.results { + result_array.push(result.actual_state); + } + Value::from(result_array) + }, + GetResult::Resource(response) => { + response.actual_state + } + }; + let diff_properties = get_diff( &actual_state, &pre_state); + Ok(SetResult::Resource(ResourceSetResponse { before_state: pre_state, - after_state: get_result.actual_state, + after_state: actual_state, changed_properties: Some(diff_properties), - }) + })) }, } } @@ -245,12 +283,12 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re } }; let diff_properties = get_diff(&expected_value, &actual_value); - Ok(TestResult { + Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state: actual_value, in_desired_state: diff_properties.is_empty(), diff_properties, - }) + })) }, Some(ReturnKind::StateAndDiff) => { // command should be returning actual state as a JSON line and a list of properties that differ as separate JSON line @@ -263,23 +301,35 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re return Err(DscError::Command(resource.resource_type.clone(), exit_code, "No diff properties returned".to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_properties)?; - Ok(TestResult { + Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state: actual_value, in_desired_state: diff_properties.is_empty(), diff_properties, - }) + })) }, None => { // perform a get and compare the result to the expected state let get_result = invoke_get(resource, cwd, expected)?; - let diff_properties = get_diff(&expected_value, &get_result.actual_state); - Ok(TestResult { + let actual_state = match get_result { + GetResult::Group(group_response) => { + let mut result_array: Vec = Vec::new(); + for result in group_response.results { + result_array.push(result.actual_state); + } + Value::from(result_array) + }, + GetResult::Resource(response) => { + response.actual_state + } + }; + let diff_properties = get_diff( &expected_value, &actual_state); + Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, - actual_state: get_result.actual_state, + actual_state: actual_state, in_desired_state: diff_properties.is_empty(), diff_properties, - }) + })) }, } } diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index b7d7b249..adc708ec 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -8,7 +8,7 @@ use serde_json::Value; use std::collections::HashMap; use tracing::debug; -use super::{command_resource, dscerror, resource_manifest::import_manifest, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; +use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest}; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] @@ -232,13 +232,25 @@ impl Invoke for DscResource { if resource_manifest.test.is_none() { let get_result = self.get(expected)?; let desired_state = serde_json::from_str(expected)?; - let diff_properties = get_diff(&desired_state, &get_result.actual_state); - let test_result = TestResult { + let actual_state = match get_result { + GetResult::Group(group_response) => { + let mut result_array: Vec = Vec::new(); + for result in group_response.results { + result_array.push(result.actual_state); + } + Value::from(result_array) + }, + GetResult::Resource(response) => { + response.actual_state + } + }; + let diff_properties = get_diff( &desired_state, &actual_state); + let test_result = TestResult::Resource(ResourceTestResponse { desired_state: serde_json::from_str(expected)?, - actual_state: get_result.actual_state, + actual_state: actual_state, in_desired_state: diff_properties.is_empty(), diff_properties, - }; + }); Ok(test_result) } else { diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index 63dd3b7d..61013152 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -5,9 +5,37 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct GetResult { +pub struct GroupResourceGetResponse { + pub results: Vec, +} + +impl GroupResourceGetResponse { + #[must_use] + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } +} + +impl Default for GroupResourceGetResponse { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub enum GetResult { + Resource(ResourceGetResponse), + Group(GroupResourceGetResponse), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResourceGetResponse { /// The state of the resource as it was returned by the Get method. #[serde(rename = "actualState")] pub actual_state: Value, @@ -15,7 +43,34 @@ pub struct GetResult { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct SetResult { +pub struct GroupResourceSetResponse { + pub results: Vec, +} + +impl GroupResourceSetResponse { + #[must_use] + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } +} + +impl Default for GroupResourceSetResponse { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub enum SetResult { + Resource(ResourceSetResponse), + Group(GroupResourceSetResponse), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResourceSetResponse { /// The state of the resource as it was before the Set method was called. #[serde(rename = "beforeState")] pub before_state: Value, @@ -29,7 +84,34 @@ pub struct SetResult { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct TestResult { +pub struct GroupResourceTestResponse { + pub results: Vec, +} + +impl GroupResourceTestResponse { + #[must_use] + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } +} + +impl Default for GroupResourceTestResponse { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub enum TestResult { + Resource(ResourceTestResponse), + Group(GroupResourceTestResponse), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResourceTestResponse { /// The state of the resource as it was expected to be. #[serde(rename = "desiredState")] pub desired_state: Value, From 6bacd4e7d05ce0b1d0cedf9e8927dfb48d1f591b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 5 Feb 2024 21:34:53 -0800 Subject: [PATCH 05/14] fix returning get result --- dsc_lib/src/dscresources/command_resource.rs | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 5c60eb81..53c52755 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -58,15 +58,15 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul GetResult::Group(group_response) }, Err(_) => { - match serde_json::from_str::(&stdout) { - Ok(response) => { - GetResult::Resource(response) - }, - Err(_) => { - debug!("Invalid get response: {}", &stdout); - return Err(DscError::Validation(format!("Resource {} did not return a valid response", &resource.resource_type))) + 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))) } - } + }; + GetResult::Resource(ResourceGetResponse{ + actual_state: result, + }) } }; @@ -509,6 +509,12 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option let exit_code = exit_status.code().unwrap_or(EXIT_PROCESS_TERMINATED); let stdout = String::from_utf8_lossy(&stdout_buf).to_string(); let stderr = String::from_utf8_lossy(&stderr_buf).to_string(); + if !stdout.is_empty() { + debug!("STDOUT returned: {}", &stdout); + } + if !stderr.is_empty() { + debug!("STDERR returned: {}", &stderr); + } Ok((exit_code, stdout, stderr)) } From 1e7128365789802c5978cb4c9eb958be2d6a0cb9 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Feb 2024 16:44:43 -0800 Subject: [PATCH 06/14] support converting test result to a get result --- dsc/assertion.dsc.resource.json | 6 +- dsc/src/args.rs | 2 + dsc/src/main.rs | 4 +- dsc/src/subcommand.rs | 131 +++++++++++-------- dsc/src/util.rs | 19 ++- dsc_lib/src/configure/config_result.rs | 24 ++++ dsc_lib/src/dscresources/command_resource.rs | 60 ++++----- dsc_lib/src/dscresources/dscresource.rs | 4 +- dsc_lib/src/dscresources/invoke_result.rs | 37 +++++- 9 files changed, 189 insertions(+), 98 deletions(-) diff --git a/dsc/assertion.dsc.resource.json b/dsc/assertion.dsc.resource.json index 72345ea7..1449585b 100644 --- a/dsc/assertion.dsc.resource.json +++ b/dsc/assertion.dsc.resource.json @@ -8,7 +8,8 @@ "args": [ "config", "--as-group", - "test" + "test", + "--as-get" ], "input": "stdin" }, @@ -28,7 +29,8 @@ "args": [ "config", "--as-group", - "test" + "test", + "--as-get" ], "input": "stdin", "return": "state" diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 0ef91242..fa0a7d43 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -99,6 +99,8 @@ pub enum ConfigSubCommand { path: Option, #[clap(short = 'f', long, help = "The output format to use")] format: Option, + #[clap(long, hide = true)] + as_get: bool, }, #[clap(name = "validate", about = "Validate the current configuration", hide = true)] Validate { diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 79309fe6..cce6c0da 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -69,7 +69,7 @@ fn main() { 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), Err(err) => { error!("Error: Failed to read parameters file: {err}"); exit(util::EXIT_INVALID_INPUT); @@ -77,7 +77,7 @@ fn main() { } } else { - subcommand::config(&subcommand, ¶meters, &input, as_group); + subcommand::config(&subcommand, ¶meters, &input, &as_group); } }, SubCommand::Resource { subcommand } => { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 7630a0e4..e8746421 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -4,15 +4,15 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; +<<<<<<< HEAD use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_SUCCESS, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; +======= +use crate::util::{EXIT_DSC_ERROR, EXIT_VALIDATION_FAILED, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, get_schema, serde_json_value_to_string, write_output, validate_json}; +use dsc_lib::configure::config_result::ResourceGetResult; +>>>>>>> 9a2f668 (support converting test result to a get result) use dsc_lib::dscerror::DscError; use dsc_lib::dscresources::invoke_result::{ - GetResult, - SetResult, - TestResult, - GroupResourceGetResponse, - GroupResourceSetResponse, - GroupResourceTestResponse + GroupResourceGetResponse, GroupResourceSetResponse, GroupResourceTestResponse, TestResult }; use tracing::error; @@ -27,22 +27,14 @@ use dsc_lib::{ use serde_yaml::Value; use std::process::exit; -pub fn config_get(configurator: &mut Configurator, format: &Option, as_group: bool) +pub fn config_get(configurator: &mut Configurator, format: &Option, as_group: &bool) { match configurator.invoke_get(ErrorAction::Continue, || { /* code */ }) { Ok(result) => { - if as_group { - let mut group_result = GroupResourceGetResponse::new(); - for resource_result in result.results { - match resource_result.result { - GetResult::Group(mut group_response) => { - group_result.results.append(&mut group_response.results); - }, - GetResult::Resource(response) => { - group_result.results.push(response); - } - } - } + if *as_group { + let group_result = GroupResourceGetResponse { + results: result.results + }; let json = match serde_json::to_string(&group_result) { Ok(json) => json, Err(err) => { @@ -73,22 +65,14 @@ pub fn config_get(configurator: &mut Configurator, format: &Option } } -pub fn config_set(configurator: &mut Configurator, format: &Option, as_group: bool) +pub fn config_set(configurator: &mut Configurator, format: &Option, as_group: &bool) { match configurator.invoke_set(false, ErrorAction::Continue, || { /* code */ }) { Ok(result) => { - if as_group { - let mut group_result = GroupResourceSetResponse::new(); - for resource_result in result.results { - match resource_result.result { - SetResult::Group(mut group_response) => { - group_result.results.append(&mut group_response.results); - }, - SetResult::Resource(response) => { - group_result.results.push(response); - } - } - } + if *as_group { + let group_result = GroupResourceSetResponse { + results: result.results + }; let json = match serde_json::to_string(&group_result) { Ok(json) => json, Err(err) => { @@ -119,27 +103,55 @@ pub fn config_set(configurator: &mut Configurator, format: &Option } } -pub fn config_test(configurator: &mut Configurator, format: &Option, as_group: bool) +pub fn config_test(configurator: &mut Configurator, format: &Option, as_group: &bool, as_get: &bool) { match configurator.invoke_test(ErrorAction::Continue, || { /* code */ }) { Ok(result) => { - if as_group { - let mut group_result = GroupResourceTestResponse::new(); - for resource_result in result.results { - match resource_result.result { - TestResult::Group(mut group_response) => { - group_result.results.append(&mut group_response.results); + if *as_group { + let mut in_desired_state = true; + for test_result in &result.results { + match &test_result.result { + TestResult::Resource(resource_test_result) => { + if !resource_test_result.in_desired_state { + in_desired_state = false; + break; + } }, - TestResult::Resource(response) => { - group_result.results.push(response); + TestResult::Group(group_resource_test_result) => { + if !group_resource_test_result.in_desired_state { + in_desired_state = false; + break; + } } } } - let json = match serde_json::to_string(&group_result) { - Ok(json) => json, - Err(err) => { - error!("JSON Error: {err}"); - exit(EXIT_JSON_ERROR); + let json = if *as_get { + let mut results = Vec::::new(); + for test_result in result.results { + results.push(test_result.into()); + } + let group_result = GroupResourceGetResponse { + results + }; + match serde_json::to_string(&group_result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + } + } + else { + let group_result = GroupResourceTestResponse { + results: result.results, + in_desired_state + }; + match serde_json::to_string(&group_result) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } } }; write_output(&json, format); @@ -194,7 +206,7 @@ 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) { let json_string = match subcommand { ConfigSubCommand::Get { document, path, .. } | ConfigSubCommand::Set { document, path, .. } | @@ -253,8 +265,8 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: ConfigSubCommand::Set { format, .. } => { config_set(&mut configurator, format, as_group); }, - ConfigSubCommand::Test { format, .. } => { - config_test(&mut configurator, format, as_group); + ConfigSubCommand::Test { format, as_get } => { + config_test(&mut configurator, format, as_group, as_get); }, ConfigSubCommand::Validate => { let mut result = ValidateResult { @@ -262,7 +274,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: reason: None, }; let valid = match validate_config(&json_string) { - Ok(_) => { + Ok(()) => { true }, Err(err) => { @@ -272,7 +284,11 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: } }; - let json = serde_json::to_string(&result).unwrap(); + let Ok(json) = serde_json::to_string(&result) else { + error!("Failed to convert validation result to JSON"); + exit(EXIT_JSON_ERROR); + }; + write_output(&json, format); if !valid { exit(EXIT_VALIDATION_FAILED); @@ -285,7 +301,18 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: } /// Validate configuration. -#[allow(clippy::too_many_lines)] +/// +/// # Arguments +/// +/// * `config` - The configuration to validate. +/// +/// # Returns +/// +/// Nothing on success. +/// +/// # Errors +/// +/// * `DscError` - The error that occurred. pub fn validate_config(config: &str) -> Result<(), DscError> { // first validate against the config schema let schema = serde_json::to_value(get_schema(DscType::Configuration))?; diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 844961ed..b8eb3f1c 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -384,18 +384,33 @@ pub fn set_dscconfigroot(config_path: &str) env::set_var("DSCConfigRoot", config_root.clone()); } +/// Validate the JSON against the schema. +/// +/// # Arguments +/// +/// * `source` - The source of the JSON +/// * `schema` - The schema to validate against +/// * `json` - The JSON to validate +/// +/// # Returns +/// +/// Nothing on success. +/// +/// # Errors +/// +/// * `DscError` - The JSON is invalid pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { debug!("Validating {source} against schema"); debug!("JSON: {json}"); debug!("Schema: {schema}"); - let compiled_schema = match JSONSchema::compile(&schema) { + let compiled_schema = match JSONSchema::compile(schema) { Ok(compiled_schema) => compiled_schema, Err(err) => { return Err(DscError::Validation(format!("JSON Schema Compilation Error: {err}"))); } }; - if let Err(err) = compiled_schema.validate(&json) { + if let Err(err) = compiled_schema.validate(json) { let mut error = format!("'{source}' failed validation: "); for e in err { error.push_str(&format!("\n{e} ")); diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index c8d6a776..493a102c 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -32,6 +32,16 @@ pub struct ResourceGetResult { pub result: GetResult, } +impl From for ResourceGetResult { + fn from(test_result: ResourceTestResult) -> Self { + Self { + name: test_result.name, + resource_type: test_result.resource_type, + result: test_result.result.into(), + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ConfigurationGetResult { @@ -58,6 +68,20 @@ impl Default for ConfigurationGetResult { } } +impl From for ConfigurationGetResult { + fn from(test_result: ConfigurationTestResult) -> Self { + let mut results = Vec::::new(); + for result in test_result.results { + results.push(result.into()); + } + Self { + results, + messages: test_result.messages, + had_errors: test_result.had_errors, + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ResourceSetResult { diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 53c52755..bf61ac09 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -6,7 +6,7 @@ use serde_json::Value; use std::{collections::HashMap, env, process::Command, io::{Write, Read}, process::Stdio}; use crate::{dscerror::DscError, dscresources::invoke_result::{GroupResourceGetResponse, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}}; use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, InputKind, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; -use tracing::{debug, info}; +use tracing::{debug, info, trace}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; @@ -53,21 +53,19 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); } - let result: GetResult = match serde_json::from_str::(&stdout) { - Ok(group_response) => { - GetResult::Group(group_response) - }, - Err(_) => { - 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))) - } - }; - GetResult::Resource(ResourceGetResponse{ - actual_state: result, - }) - } + let result: GetResult = if let Ok(group_response) = serde_json::from_str::(&stdout) { + trace!("Group get response: {:?}", &group_response); + GetResult::Group(group_response) + } else { + 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))) + } + }; + GetResult::Resource(ResourceGetResponse{ + actual_state: result, + }) }; Ok(result) @@ -109,28 +107,22 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && !set.pre_test.unwrap_or_default() { info!("No pretest, invoking test {}", &resource.resource_type); - let (in_desired_state, desired_state, actual_state) = match invoke_test(resource, cwd, desired)? { + let (in_desired_state, actual_state) = match invoke_test(resource, cwd, desired)? { TestResult::Group(group_response) => { - let mut in_desired_state = true; let mut result_array: Vec = Vec::new(); - let mut desired_state_array: Vec = Vec::new(); - for response in group_response.results { - if response.in_desired_state == false { - in_desired_state = false; - } - result_array.push(response.actual_state); - desired_state_array.push(response.desired_state); + for result in group_response.results { + result_array.push(serde_json::to_value(result)?); } - (in_desired_state, Value::from(desired_state_array), Value::from(result_array)) + (group_response.in_desired_state, Value::from(result_array)) }, TestResult::Resource(response) => { - (response.in_desired_state, response.desired_state, response.actual_state) + (response.in_desired_state, response.actual_state) } }; if in_desired_state { return Ok(SetResult::Resource(ResourceSetResponse{ - before_state: desired_state, + before_state: serde_json::from_str(desired)?, after_state: actual_state, changed_properties: None, })); @@ -217,7 +209,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te GetResult::Group(group_response) => { let mut result_array: Vec = Vec::new(); for result in group_response.results { - result_array.push(result.actual_state); + result_array.push(serde_json::to_value(result)?); } Value::from(result_array) }, @@ -315,7 +307,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re GetResult::Group(group_response) => { let mut result_array: Vec = Vec::new(); for result in group_response.results { - result_array.push(result.actual_state); + result_array.push(serde_json::to_value(&result)?); } Value::from(result_array) }, @@ -326,7 +318,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re let diff_properties = get_diff( &expected_value, &actual_state); Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, - actual_state: actual_state, + actual_state, in_desired_state: diff_properties.is_empty(), diff_properties, })) @@ -460,7 +452,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> /// Error is returned if the command fails to execute or stdin/stdout/stderr cannot be opened. #[allow(clippy::implicit_hasher)] pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>) -> Result<(i32, String, String), DscError> { - debug!("Invoking command {} with args {:?}", executable, args); + debug!("Invoking command '{}' with args {:?}", executable, args); let mut command = Command::new(executable); if input.is_some() { command.stdin(Stdio::piped()); @@ -510,10 +502,10 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option let stdout = String::from_utf8_lossy(&stdout_buf).to_string(); let stderr = String::from_utf8_lossy(&stderr_buf).to_string(); if !stdout.is_empty() { - debug!("STDOUT returned: {}", &stdout); + trace!("STDOUT returned: {}", &stdout); } if !stderr.is_empty() { - debug!("STDERR returned: {}", &stderr); + trace!("STDERR returned: {}", &stderr); } Ok((exit_code, stdout, stderr)) } diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index adc708ec..bff31512 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -236,7 +236,7 @@ impl Invoke for DscResource { GetResult::Group(group_response) => { let mut result_array: Vec = Vec::new(); for result in group_response.results { - result_array.push(result.actual_state); + result_array.push(serde_json::to_value(result)?); } Value::from(result_array) }, @@ -247,7 +247,7 @@ impl Invoke for DscResource { let diff_properties = get_diff( &desired_state, &actual_state); let test_result = TestResult::Resource(ResourceTestResponse { desired_state: serde_json::from_str(expected)?, - actual_state: actual_state, + actual_state, in_desired_state: diff_properties.is_empty(), diff_properties, }); diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index 61013152..1fb62ea7 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -5,18 +5,20 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::configure::config_result::{ResourceGetResult, ResourceSetResult, ResourceTestResult}; + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GroupResourceGetResponse { - pub results: Vec, + pub results: Vec, } impl GroupResourceGetResponse { #[must_use] pub fn new() -> Self { Self { - results: Vec::new(), + results: Vec::new() } } } @@ -28,11 +30,33 @@ impl Default for GroupResourceGetResponse { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] pub enum GetResult { Resource(ResourceGetResponse), Group(GroupResourceGetResponse), } +impl From for GetResult { + fn from(value: TestResult) -> Self { + match value { + TestResult::Group(group) => { + let mut results = Vec::::new(); + for result in group.results { + results.push(result.into()); + } + GetResult::Group(GroupResourceGetResponse { + results + }) + }, + TestResult::Resource(resource) => { + GetResult::Resource(ResourceGetResponse { + actual_state: resource.actual_state + }) + } + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ResourceGetResponse { @@ -44,7 +68,7 @@ pub struct ResourceGetResponse { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GroupResourceSetResponse { - pub results: Vec, + pub results: Vec, } impl GroupResourceSetResponse { @@ -63,6 +87,7 @@ impl Default for GroupResourceSetResponse { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] pub enum SetResult { Resource(ResourceSetResponse), Group(GroupResourceSetResponse), @@ -85,7 +110,9 @@ pub struct ResourceSetResponse { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GroupResourceTestResponse { - pub results: Vec, + pub results: Vec, + #[serde(rename = "inDesiredState")] + pub in_desired_state: bool, } impl GroupResourceTestResponse { @@ -93,6 +120,7 @@ impl GroupResourceTestResponse { pub fn new() -> Self { Self { results: Vec::new(), + in_desired_state: false, } } } @@ -104,6 +132,7 @@ impl Default for GroupResourceTestResponse { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] pub enum TestResult { Resource(ResourceTestResponse), Group(GroupResourceTestResponse), From 41948c01c216c615b513b10d0899ee092d9768cb Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Feb 2024 21:20:29 -0800 Subject: [PATCH 07/14] add --format to validate --- dsc/src/args.rs | 2 ++ dsc/src/subcommand.rs | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dsc/src/args.rs b/dsc/src/args.rs index fa0a7d43..0f470b1e 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -108,6 +108,8 @@ pub enum ConfigSubCommand { 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, }, #[clap(name = "export", about = "Export the current configuration")] Export { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index e8746421..0264d254 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -4,12 +4,8 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; -<<<<<<< HEAD use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_SUCCESS, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; -======= -use crate::util::{EXIT_DSC_ERROR, EXIT_VALIDATION_FAILED, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, get_schema, serde_json_value_to_string, write_output, validate_json}; use dsc_lib::configure::config_result::ResourceGetResult; ->>>>>>> 9a2f668 (support converting test result to a get result) use dsc_lib::dscerror::DscError; use dsc_lib::dscresources::invoke_result::{ GroupResourceGetResponse, GroupResourceSetResponse, GroupResourceTestResponse, TestResult @@ -265,10 +261,10 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: ConfigSubCommand::Set { format, .. } => { config_set(&mut configurator, format, as_group); }, - ConfigSubCommand::Test { format, as_get } => { + ConfigSubCommand::Test { format, as_get, .. } => { config_test(&mut configurator, format, as_group, as_get); }, - ConfigSubCommand::Validate => { + ConfigSubCommand::Validate { format, .. } => { let mut result = ValidateResult { valid: true, reason: None, From d691e5a594fd19180f955b76d79dda072c4ecdd4 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 13 Feb 2024 13:35:45 -0800 Subject: [PATCH 08/14] remove GroupResourceGetResponse to eliminate unwanted extra property in JSON --- dsc/src/subcommand.rs | 14 +++++----- dsc_lib/src/dscresources/command_resource.rs | 13 ++++----- dsc_lib/src/dscresources/dscresource.rs | 6 ++--- dsc_lib/src/dscresources/invoke_result.rs | 28 ++------------------ 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 0264d254..ba0f80e3 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -8,7 +8,7 @@ use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_SUCC use dsc_lib::configure::config_result::ResourceGetResult; use dsc_lib::dscerror::DscError; use dsc_lib::dscresources::invoke_result::{ - GroupResourceGetResponse, GroupResourceSetResponse, GroupResourceTestResponse, TestResult + GroupResourceSetResponse, GroupResourceTestResponse, TestResult }; use tracing::error; @@ -28,8 +28,9 @@ pub fn config_get(configurator: &mut Configurator, format: &Option match configurator.invoke_get(ErrorAction::Continue, || { /* code */ }) { Ok(result) => { if *as_group { - let group_result = GroupResourceGetResponse { - results: result.results + let mut group_result = Vec::::new(); + for result in result.results { + group_result.push(result); }; let json = match serde_json::to_string(&group_result) { Ok(json) => json, @@ -122,13 +123,10 @@ pub fn config_test(configurator: &mut Configurator, format: &Option::new(); + let mut group_result = Vec::::new(); for test_result in result.results { - results.push(test_result.into()); + group_result.push(test_result.into()); } - let group_result = GroupResourceGetResponse { - results - }; match serde_json::to_string(&group_result) { Ok(json) => json, Err(err) => { diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index bf61ac09..9c0d1353 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -4,7 +4,8 @@ use jsonschema::JSONSchema; use serde_json::Value; use std::{collections::HashMap, env, process::Command, io::{Write, Read}, process::Stdio}; -use crate::{dscerror::DscError, dscresources::invoke_result::{GroupResourceGetResponse, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}}; +use crate::{dscerror::DscError, dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}}; +use crate::configure::config_result::ResourceGetResult; use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, InputKind, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; use tracing::{debug, info, trace}; @@ -53,7 +54,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); } - let result: GetResult = if let Ok(group_response) = serde_json::from_str::(&stdout) { + let result: GetResult = if let Ok(group_response) = serde_json::from_str::>(&stdout) { trace!("Group get response: {:?}", &group_response); GetResult::Group(group_response) } else { @@ -206,9 +207,9 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te let get_result = invoke_get(resource, cwd, desired)?; // for changed_properties, we compare post state to pre state let actual_state = match get_result { - GetResult::Group(group_response) => { + GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); - for result in group_response.results { + for result in results { result_array.push(serde_json::to_value(result)?); } Value::from(result_array) @@ -304,9 +305,9 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re // perform a get and compare the result to the expected state let get_result = invoke_get(resource, cwd, expected)?; let actual_state = match get_result { - GetResult::Group(group_response) => { + GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); - for result in group_response.results { + for result in results { result_array.push(serde_json::to_value(&result)?); } Value::from(result_array) diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index bff31512..4c656104 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -172,7 +172,7 @@ pub trait Invoke { fn schema(&self) -> Result; /// Invoke the export operation on the resource. - /// + /// /// # Arguments /// /// * `input` - Input for export operation. @@ -233,9 +233,9 @@ impl Invoke for DscResource { let get_result = self.get(expected)?; let desired_state = serde_json::from_str(expected)?; let actual_state = match get_result { - GetResult::Group(group_response) => { + GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); - for result in group_response.results { + for result in results { result_array.push(serde_json::to_value(result)?); } Value::from(result_array) diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index 1fb62ea7..5df73e37 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -7,33 +7,11 @@ use serde_json::Value; use crate::configure::config_result::{ResourceGetResult, ResourceSetResult, ResourceTestResult}; - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct GroupResourceGetResponse { - pub results: Vec, -} - -impl GroupResourceGetResponse { - #[must_use] - pub fn new() -> Self { - Self { - results: Vec::new() - } - } -} - -impl Default for GroupResourceGetResponse { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(untagged)] pub enum GetResult { Resource(ResourceGetResponse), - Group(GroupResourceGetResponse), + Group(Vec), } impl From for GetResult { @@ -44,9 +22,7 @@ impl From for GetResult { for result in group.results { results.push(result.into()); } - GetResult::Group(GroupResourceGetResponse { - results - }) + GetResult::Group(results) }, TestResult::Resource(resource) => { GetResult::Resource(ResourceGetResponse { From 5cc6214eb316aa82134dabf7394dda031e382b9b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 17 Feb 2024 17:38:32 -0800 Subject: [PATCH 09/14] add group test and fix config resource validation --- dsc/examples/groups.dsc.yaml | 38 +++++++++++++++++ dsc/src/subcommand.rs | 16 ++++--- dsc/tests/dsc_brew.tests.ps1 | 23 ++++++++++ dsc/tests/dsc_group.tests.ps1 | 44 ++++++++++++++++++++ dsc_lib/src/configure/depends_on.rs | 4 ++ dsc_lib/src/configure/mod.rs | 3 +- dsc_lib/src/dscresources/command_resource.rs | 17 ++++++-- dsc_lib/src/dscresources/dscresource.rs | 7 ++-- 8 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 dsc/examples/groups.dsc.yaml create mode 100644 dsc/tests/dsc_brew.tests.ps1 create mode 100644 dsc/tests/dsc_group.tests.ps1 diff --git a/dsc/examples/groups.dsc.yaml b/dsc/examples/groups.dsc.yaml new file mode 100644 index 00000000..81f909c1 --- /dev/null +++ b/dsc/examples/groups.dsc.yaml @@ -0,0 +1,38 @@ +# Example for grouping and groups in groups +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +resources: +- name: Last Group + type: DSC/Group + properties: + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json + resources: + - name: Last + type: Test/Echo + properties: + text: Last + dependsOn: + - "[resourceId('DSC/Group','First Group')]" +- name: First Group + type: DSC/Group + properties: + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json + resources: + - name: First + type: Test/Echo + properties: + text: First + - name: Nested Group + type: DSC/Group + properties: + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json + resources: + - name: Nested Second + type: Test/Echo + properties: + text: Nested Second + dependsOn: + - "[resourceId('Test/Echo','Nested First')]" + - name: Nested First + type: Test/Echo + properties: + text: Nested First diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index ba0f80e3..e6c847c4 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -10,11 +10,7 @@ use dsc_lib::dscerror::DscError; use dsc_lib::dscresources::invoke_result::{ GroupResourceSetResponse, GroupResourceTestResponse, TestResult }; -use tracing::error; - -use atty::Stream; use dsc_lib::{ - configure::{Configurator, ErrorAction}, DscManager, dscresources::invoke_result::ValidateResult, dscresources::dscresource::{ImplementedAs, Invoke}, @@ -22,6 +18,7 @@ use dsc_lib::{ }; use serde_yaml::Value; use std::process::exit; +use tracing::{debug, error, trace}; pub fn config_get(configurator: &mut Configurator, format: &Option, as_group: &bool) { @@ -309,6 +306,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: /// * `DscError` - The error that occurred. pub fn validate_config(config: &str) -> Result<(), DscError> { // first validate against the config schema + debug!("Validating configuration against schema"); let schema = serde_json::to_value(get_schema(DscType::Configuration))?; let config_value = serde_json::from_str(config)?; validate_json("Configuration", &schema, &config_value)?; @@ -339,6 +337,8 @@ pub fn validate_config(config: &str) -> Result<(), DscError> { return Err(DscError::Validation("Error: Resource type not specified".to_string())); }; + trace!("Validating resource named '{}'", resource_block["name"].as_str().unwrap_or_default()); + // get the actual resource let Some(resource) = get_resource(&dsc, type_name) else { return Err(DscError::Validation(format!("Error: Resource type '{type_name}' not found"))); @@ -351,7 +351,10 @@ pub fn validate_config(config: &str) -> Result<(), DscError> { // convert to resource_manifest let manifest: ResourceManifest = serde_json::from_value(manifest)?; if manifest.validate.is_some() { - let result = resource.validate(config)?; + debug!("Resource {type_name} implements validation"); + // get the resource's part of the config + let resource_config = resource_block["properties"].to_string(); + let result = resource.validate(&resource_config)?; if !result.valid { let reason = result.reason.unwrap_or("No reason provided".to_string()); let type_name = resource.type_name.clone(); @@ -360,6 +363,7 @@ pub fn validate_config(config: &str) -> Result<(), DscError> { } else { // use schema validation + trace!("Resource {type_name} does not implement validation, using schema"); let Ok(schema) = resource.schema() else { return Err(DscError::Validation(format!("Error: Resource {type_name} does not have a schema nor supports validation"))); }; @@ -367,6 +371,8 @@ pub fn validate_config(config: &str) -> Result<(), DscError> { validate_json(&resource.type_name, &schema, &resource_block["properties"])?; } + } else { + return Err(DscError::Validation(format!("Error: Resource {type_name} does not have a manifest"))); } } } diff --git a/dsc/tests/dsc_brew.tests.ps1 b/dsc/tests/dsc_brew.tests.ps1 new file mode 100644 index 00000000..082f2c24 --- /dev/null +++ b/dsc/tests/dsc_brew.tests.ps1 @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Brew resource tests' { + BeforeAll { + # skip if brew is not installed + $PSDefaultParameterValues = @{ 'It:Skip' = ($null -eq (Get-Command brew -CommandType Application -ErrorAction Ignore)) } + } + + It 'Config get works' { + $out = dsc config get -p $PSScriptRoot/../examples/brew.dsc.yaml | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 + $exists = $null -ne (Get-Command gitui -CommandType Application -ErrorAction Ignore) + $out.results[1].result.actualState._exist | Should -Be $exists + } + + It 'Config test works' { + $out = dsc config test -p $PSScriptRoot/../examples/brew.dsc.yaml | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 + $exists = $null -ne (Get-Command gitui -CommandType Application -ErrorAction Ignore) + $out.results[1].result.inDesiredState | Should -Be $exists + } +} diff --git a/dsc/tests/dsc_group.tests.ps1 b/dsc/tests/dsc_group.tests.ps1 new file mode 100644 index 00000000..df73d18b --- /dev/null +++ b/dsc/tests/dsc_group.tests.ps1 @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Group resource tests' { + It 'Nested groups should work for get' { + $out = (dsc config get -p $PSScriptRoot/../examples/groups.dsc.yaml -f yaml | Out-String).Trim() + $LASTEXITCODE | Should -Be 0 + $out | Should -BeExactly @' +results: +- name: First Group + type: DSC/Group + result: + - name: First + type: Test/Echo + result: + actualState: + text: First + - name: Nested Group + type: DSC/Group + result: + - name: Nested First + type: Test/Echo + result: + actualState: + text: Nested First + - name: Nested Second + type: Test/Echo + result: + actualState: + text: Nested Second +- name: Last Group + type: DSC/Group + result: + - name: Last + type: Test/Echo + result: + actualState: + text: Last +messages: [] +hadErrors: false +'@ + } + +} diff --git a/dsc_lib/src/configure/depends_on.rs b/dsc_lib/src/configure/depends_on.rs index 0d7e123e..ff34375d 100644 --- a/dsc_lib/src/configure/depends_on.rs +++ b/dsc_lib/src/configure/depends_on.rs @@ -8,6 +8,8 @@ use crate::parser::Statement; use super::context::Context; +use tracing::{debug, trace}; + /// Gets the invocation order of resources based on their dependencies /// /// # Arguments @@ -22,6 +24,7 @@ use super::context::Context; /// /// * `DscError::Validation` - The configuration is invalid pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statement, context: &Context) -> Result, DscError> { + debug!("Getting resource invocation order"); let mut order: Vec = Vec::new(); for resource in &config.resources { // validate that the resource isn't specified more than once in the config @@ -84,6 +87,7 @@ pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statem order.push(resource.clone()); } + trace!("Resource invocation order: {0:?}", order); Ok(order) } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index cb763684..e4290eda 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -165,6 +165,7 @@ impl Configurator { let config = self.validate_config()?; let mut result = ConfigurationGetResult::new(); for resource in get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)? { + trace!("Get resource '{}' named: {}", resource.resource_type, resource.name); let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -274,7 +275,7 @@ impl Configurator { let mut result = ConfigurationExportResult::new(); let mut conf = config_doc::Configuration::new(); - for resource in &config.resources { + for resource in get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)? { let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 9c0d1353..83edadf8 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -343,6 +343,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re /// /// Error is returned if the underlying command returns a non-zero exit code. pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) -> Result { + trace!("Invoking validate '{}' using: {}", &resource.resource_type, &config); // TODO: use schema to validate config if validate is not implemented let Some(validate) = resource.validate.as_ref() else { return Err(DscError::NotImplemented("validate".to_string())); @@ -533,12 +534,20 @@ fn replace_token(args: &mut Option>, token: &str, value: &str) -> Re fn verify_json(resource: &ResourceManifest, cwd: &str, json: &str) -> Result<(), DscError> { - debug!("resource_type - {}", resource.resource_type); + debug!("Verify JSON for '{}'", resource.resource_type); - //TODO: remove this after schema validation for classic PS resources is implemented - if (resource.resource_type == "DSC/PowerShellGroup") - || (resource.resource_type == "DSC/WMIGroup") {return Ok(());} + // see if resource implements validate + if resource.validate.is_some() { + trace!("Validating against JSON: {json}"); + let result = invoke_validate(resource, cwd, json)?; + if result.valid { + return Ok(()); + } else { + return Err(DscError::Validation("Resource reported input JSON is not valid".to_string())); + } + } + // otherwise, use schema validation let schema = get_schema(resource, cwd)?; let schema: Value = serde_json::from_str(&schema)?; let compiled_schema = match JSONSchema::compile(&schema) { diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 4c656104..aeaf0320 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use tracing::debug; +use tracing::{debug, trace}; use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest}; @@ -74,19 +74,20 @@ impl DscResource { let resource_manifest = import_manifest(manifest.clone())?; if resource_manifest.validate.is_some() { - debug!("Using custom validation"); + trace!("Using custom validation"); let validation_result = match self.validate(input) { Ok(validation_result) => validation_result, Err(err) => { return Err(DscError::Validation(format!("Validation failed: {err}"))); }, }; + trace!("Validation result is valid: {}", validation_result.valid); if !validation_result.valid { return Err(DscError::Validation("Validation failed".to_string())); } } else { - debug!("Using JSON schema validation"); + trace!("Using JSON schema validation"); let Ok(schema) = self.schema() else { return Err(DscError::Validation("Schema not available".to_string())); }; From 0cecb71f9b8ce238be69d0666090847b7b193672 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 17 Feb 2024 22:25:12 -0800 Subject: [PATCH 10/14] fix clippy --- dsc_lib/src/dscresources/command_resource.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 83edadf8..af864f22 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -542,9 +542,9 @@ fn verify_json(resource: &ResourceManifest, cwd: &str, json: &str) -> Result<(), let result = invoke_validate(resource, cwd, json)?; if result.valid { return Ok(()); - } else { - return Err(DscError::Validation("Resource reported input JSON is not valid".to_string())); } + + return Err(DscError::Validation("Resource reported input JSON is not valid".to_string())); } // otherwise, use schema validation From 9445e727db4fd0e8add8d6ce40680b67e4a77c6f Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 18 Feb 2024 08:19:22 -0800 Subject: [PATCH 11/14] fix how test is skipped --- dsc/tests/dsc_brew.tests.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dsc/tests/dsc_brew.tests.ps1 b/dsc/tests/dsc_brew.tests.ps1 index 082f2c24..6739cc09 100644 --- a/dsc/tests/dsc_brew.tests.ps1 +++ b/dsc/tests/dsc_brew.tests.ps1 @@ -3,8 +3,12 @@ Describe 'Brew resource tests' { BeforeAll { - # skip if brew is not installed - $PSDefaultParameterValues = @{ 'It:Skip' = ($null -eq (Get-Command brew -CommandType Application -ErrorAction Ignore)) } + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues['it:skip'] = ($null -eq (Get-Command brew -CommandType Application -ErrorAction Ignore)) + } + + AfterAll { + $global:PSDefaultParameterValues = $originalDefaultParameterValues } It 'Config get works' { From c180851cb1520755f2d15106de00a54056df3225 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Sun, 18 Feb 2024 15:29:10 -0800 Subject: [PATCH 12/14] change skipping of test --- dsc/tests/dsc_brew.tests.ps1 | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/dsc/tests/dsc_brew.tests.ps1 b/dsc/tests/dsc_brew.tests.ps1 index 6739cc09..77a82cda 100644 --- a/dsc/tests/dsc_brew.tests.ps1 +++ b/dsc/tests/dsc_brew.tests.ps1 @@ -3,22 +3,17 @@ Describe 'Brew resource tests' { BeforeAll { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() - $PSDefaultParameterValues['it:skip'] = ($null -eq (Get-Command brew -CommandType Application -ErrorAction Ignore)) + $brewExists = ($null -ne (Get-Command brew -CommandType Application -ErrorAction Ignore)) } - AfterAll { - $global:PSDefaultParameterValues = $originalDefaultParameterValues - } - - It 'Config get works' { + It 'Config get works' -Skip:(-not $brewExists) { $out = dsc config get -p $PSScriptRoot/../examples/brew.dsc.yaml | ConvertFrom-Json -Depth 10 $LASTEXITCODE | Should -Be 0 $exists = $null -ne (Get-Command gitui -CommandType Application -ErrorAction Ignore) $out.results[1].result.actualState._exist | Should -Be $exists } - It 'Config test works' { + It 'Config test works' -Skip:(-not $brewExists) { $out = dsc config test -p $PSScriptRoot/../examples/brew.dsc.yaml | ConvertFrom-Json -Depth 10 $LASTEXITCODE | Should -Be 0 $exists = $null -ne (Get-Command gitui -CommandType Application -ErrorAction Ignore) From dc861143cea2ff5ae0ae5f3a841ea484ed2021db Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 22 Feb 2024 10:43:01 -0800 Subject: [PATCH 13/14] change some debug to trace --- dsc/src/util.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/dsc/src/util.rs b/dsc/src/util.rs index b8eb3f1c..abbebebb 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -38,7 +38,7 @@ use syntect::{ parsing::SyntaxSet, util::{as_24_bit_terminal_escaped, LinesWithEndings} }; -use tracing::{Level, debug, error}; +use tracing::{Level, debug, error, trace}; use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, Layer}; pub const EXIT_SUCCESS: i32 = 0; @@ -302,6 +302,43 @@ pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { } } +/// Validate the JSON against the schema. +/// +/// # Arguments +/// +/// * `source` - The source of the JSON +/// * `schema` - The schema to validate against +/// * `json` - The JSON to validate +/// +/// # Returns +/// +/// Nothing on success. +/// +/// # Errors +/// +/// * `DscError` - The JSON is invalid +pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { + debug!("Validating {source} against schema"); + trace!("JSON: {json}"); + trace!("Schema: {schema}"); + let compiled_schema = match JSONSchema::compile(schema) { + Ok(compiled_schema) => compiled_schema, + Err(err) => { + return Err(DscError::Validation(format!("JSON Schema Compilation Error: {err}"))); + } + }; + + if let Err(err) = compiled_schema.validate(json) { + let mut error = format!("'{source}' failed validation: "); + for e in err { + error.push_str(&format!("\n{e} ")); + } + return Err(DscError::Validation(error)); + }; + + Ok(()) +} + pub fn parse_input_to_json(value: &str) -> String { match serde_json::from_str(value) { Ok(json) => json, From 9fb305839bd6dcc75b233e8d4b427ee767ce47d1 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 23 Feb 2024 11:32:11 -0800 Subject: [PATCH 14/14] fix merge conflicts --- dsc/examples/groups.dsc.yaml | 8 +++---- dsc/src/subcommand.rs | 5 +++-- dsc/src/util.rs | 42 ++--------------------------------- dsc/tests/dsc_group.tests.ps1 | 8 +++---- 4 files changed, 13 insertions(+), 50 deletions(-) diff --git a/dsc/examples/groups.dsc.yaml b/dsc/examples/groups.dsc.yaml index 81f909c1..734cfbe8 100644 --- a/dsc/examples/groups.dsc.yaml +++ b/dsc/examples/groups.dsc.yaml @@ -9,7 +9,7 @@ resources: - name: Last type: Test/Echo properties: - text: Last + output: Last dependsOn: - "[resourceId('DSC/Group','First Group')]" - name: First Group @@ -20,7 +20,7 @@ resources: - name: First type: Test/Echo properties: - text: First + output: First - name: Nested Group type: DSC/Group properties: @@ -29,10 +29,10 @@ resources: - name: Nested Second type: Test/Echo properties: - text: Nested Second + output: Nested Second dependsOn: - "[resourceId('Test/Echo','Nested First')]" - name: Nested First type: Test/Echo properties: - text: Nested First + output: Nested First diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index e6c847c4..dd9dbf15 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -3,9 +3,10 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; 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_SUCCESS, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json}; -use dsc_lib::configure::config_result::ResourceGetResult; +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 dsc_lib::dscerror::DscError; use dsc_lib::dscresources::invoke_result::{ GroupResourceSetResponse, GroupResourceTestResponse, TestResult diff --git a/dsc/src/util.rs b/dsc/src/util.rs index abbebebb..287ba02a 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -2,8 +2,6 @@ // Licensed under the MIT License. use crate::args::{DscType, OutputFormat, TraceFormat, TraceLevel}; -use jsonschema::JSONSchema; -use serde_json::Value; use atty::Stream; use dsc_lib::{ @@ -26,8 +24,9 @@ use dsc_lib::{ resource_manifest::ResourceManifest } }; +use jsonschema::JSONSchema; use schemars::{schema_for, schema::RootSchema}; -use serde_yaml::Value; +use serde_json::Value; use std::collections::HashMap; use std::env; use std::path::Path; @@ -420,40 +419,3 @@ pub fn set_dscconfigroot(config_path: &str) debug!("Setting 'DSCConfigRoot' env var as '{}'", config_root); env::set_var("DSCConfigRoot", config_root.clone()); } - -/// Validate the JSON against the schema. -/// -/// # Arguments -/// -/// * `source` - The source of the JSON -/// * `schema` - The schema to validate against -/// * `json` - The JSON to validate -/// -/// # Returns -/// -/// Nothing on success. -/// -/// # Errors -/// -/// * `DscError` - The JSON is invalid -pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { - debug!("Validating {source} against schema"); - debug!("JSON: {json}"); - debug!("Schema: {schema}"); - let compiled_schema = match JSONSchema::compile(schema) { - Ok(compiled_schema) => compiled_schema, - Err(err) => { - return Err(DscError::Validation(format!("JSON Schema Compilation Error: {err}"))); - } - }; - - if let Err(err) = compiled_schema.validate(json) { - let mut error = format!("'{source}' failed validation: "); - for e in err { - error.push_str(&format!("\n{e} ")); - } - return Err(DscError::Validation(error)); - }; - - Ok(()) -} diff --git a/dsc/tests/dsc_group.tests.ps1 b/dsc/tests/dsc_group.tests.ps1 index df73d18b..66b2259d 100644 --- a/dsc/tests/dsc_group.tests.ps1 +++ b/dsc/tests/dsc_group.tests.ps1 @@ -14,7 +14,7 @@ results: type: Test/Echo result: actualState: - text: First + output: First - name: Nested Group type: DSC/Group result: @@ -22,12 +22,12 @@ results: type: Test/Echo result: actualState: - text: Nested First + output: Nested First - name: Nested Second type: Test/Echo result: actualState: - text: Nested Second + output: Nested Second - name: Last Group type: DSC/Group result: @@ -35,7 +35,7 @@ results: type: Test/Echo result: actualState: - text: Last + output: Last messages: [] hadErrors: false '@