From 81220029ea476afa8abd519a33fd319fc90878dd Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 24 Aug 2023 12:43:51 -0700 Subject: [PATCH 01/18] Individual resource export functionality --- dsc/src/args.rs | 5 ++++ dsc/src/resource_command.rs | 24 +++++++++++++++++ dsc/src/subcommand.rs | 3 +++ dsc_lib/src/dscresources/command_resource.rs | 26 ++++++++++++++++++- dsc_lib/src/dscresources/dscresource.rs | 24 ++++++++++++++++- dsc_lib/src/dscresources/invoke_result.rs | 8 ++++++ dsc_lib/src/dscresources/resource_manifest.rs | 11 ++++++++ process/process.dsc.resource.json | 6 +++++ 8 files changed, 105 insertions(+), 2 deletions(-) diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 188dfb0d..8ed02bff 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -89,6 +89,11 @@ pub enum ResourceSubCommand { #[clap(short, long, help = "The name of the resource to get the JSON schema")] resource: String, }, + #[clap(name = "export", about = "Retrieve all resource instances", arg_required_else_help = true)] + Export { + #[clap(short, long, help = "The name or DscResource JSON of the resource to invoke `export` on")] + resource: String, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 53b52a6d..081f541b 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -129,6 +129,30 @@ pub fn schema(dsc: &mut DscManager, resource: &str, format: &Option) { + let resource = get_resource(dsc, resource); + + match resource.export() { + Ok(result) => { + for instance in result.actual_state + { + let json = match serde_json::to_string(&instance) { + Ok(json) => json, + Err(err) => { + eprintln!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + } + } + Err(err) => { + eprintln!("Error: {err}"); + exit(EXIT_DSC_ERROR); + } + } +} + pub fn get_resource(dsc: &mut DscManager, resource: &str) -> DscResource { // check if resource is JSON or just a name match serde_json::from_str(resource) { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 37aa638d..f708f95a 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -365,5 +365,8 @@ pub fn resource(subcommand: &ResourceSubCommand, format: &Option, ResourceSubCommand::Schema { resource } => { resource_command::schema(&mut dsc, resource, format); }, + ResourceSubCommand::Export { resource} => { + resource_command::export(&mut dsc, resource, format); + }, } } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 33be8841..1d661dce 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::{process::Command, io::{Write, Read}, process::Stdio}; use crate::dscerror::DscError; -use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult}}; +use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; @@ -255,6 +255,30 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result Result { + + let (exit_code, stdout, stderr) = invoke_command(&resource.export.clone().unwrap().executable, resource.export.clone().unwrap().args.clone(), None, Some(cwd))?; + if exit_code != 0 { + return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); + } + + let mut instances: Vec = Vec::new(); + for line in stdout.lines() + { + let instance: Value = match serde_json::from_str(&line){ + Result::Ok(r) => {r}, + Result::Err(err) => { + return Err(DscError::Operation(format!("Failed to parse json from export {}|{}|{} -> {err}", &resource.export.clone().unwrap().executable, stdout, stderr))) + } + }; + instances.push(instance); + } + + Ok(ExportResult { + actual_state: instances, + }) +} + /// Invoke a command and return the exit code, stdout, and stderr. /// /// # Arguments diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 6b5dac7f..b06138f6 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -6,7 +6,7 @@ use resource_manifest::ResourceManifest; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use super::{command_resource, dscerror, resource_manifest, invoke_result::{GetResult, SetResult, TestResult, ValidateResult}}; +use super::{command_resource, dscerror, resource_manifest, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}}; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] @@ -120,6 +120,13 @@ pub trait Invoke { /// /// This function will return an error if the underlying resource fails. fn schema(&self) -> Result; + + /// Invoke the export operation on the resource. + /// + /// # Errors + /// + /// This function will return an error if the underlying resource fails. + fn export(&self) -> Result; } impl Invoke for DscResource { @@ -213,6 +220,21 @@ impl Invoke for DscResource { }, } } + + fn export(&self) -> Result { + match &self.implemented_as { + ImplementedAs::Custom(_custom) => { + Err(DscError::NotImplemented("export custom resources".to_string())) + }, + ImplementedAs::Command => { + let Some(manifest) = &self.manifest else { + return Err(DscError::MissingManifest(self.type_name.clone())); + }; + let resource_manifest = serde_json::from_value::(manifest.clone())?; + command_resource::invoke_export(&resource_manifest, &self.directory) + }, + } + } } #[must_use] diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index 2d2f9229..63dd3b7d 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -52,3 +52,11 @@ pub struct ValidateResult { /// Reason for the validation result. pub reason: Option, } + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ExportResult { + /// The state of the resource as it was returned by the Get method. + #[serde(rename = "actualState")] + pub actual_state: Vec, +} diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index d668a677..5f97f025 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -29,6 +29,9 @@ pub struct ResourceManifest { /// Details how to call the Test method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub test: Option, + /// Details how to call the Export method of the resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub export: Option, /// Details how to call the Validate method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub validate: Option, @@ -132,6 +135,14 @@ pub struct ValidateMethod { // TODO: enable validation via schema or command pub args: Option>, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct ExportMethod { + /// The command to run to enumerate instances of the resource. + pub executable: String, + /// The arguments to pass to the command to perform a Export. + pub args: Option>, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct Provider { /// The way to list provider supported resources. diff --git a/process/process.dsc.resource.json b/process/process.dsc.resource.json index 9ce5d375..fd9ad371 100644 --- a/process/process.dsc.resource.json +++ b/process/process.dsc.resource.json @@ -17,5 +17,11 @@ "input": "stdin", "preTest": true, "return": "state" + }, + "export": { + "executable": "process", + "args": [ + "list" + ] } } From 67709275ec9c504d63b85f85ecf4c826eb52b791 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 24 Aug 2023 22:32:11 -0700 Subject: [PATCH 02/18] Configuration export functionality and tests --- dsc/src/args.rs | 2 + dsc/src/subcommand.rs | 32 ++++++++++++++ dsc/tests/dsc_export.tests.ps1 | 45 ++++++++++++++++++++ dsc_lib/src/configure/config_doc.rs | 25 +++++++++++ dsc_lib/src/configure/config_result.rs | 38 ++++++++++++++++- dsc_lib/src/configure/mod.rs | 37 +++++++++++++++- dsc_lib/src/dscresources/command_resource.rs | 6 +-- process/ExportTest.dsc.yaml | 7 +++ process/process.dsc.resource.json | 22 +++++++++- 9 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 dsc/tests/dsc_export.tests.ps1 create mode 100644 process/ExportTest.dsc.yaml diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 8ed02bff..d06312af 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -50,6 +50,8 @@ pub enum ConfigSubCommand { Test, #[clap(name = "validate", about = "Validate the current configuration", hide = true)] Validate, + #[clap(name = "export", about = "Export the current configuration")] + Export } #[derive(Debug, PartialEq, Eq, Subcommand)] diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index f708f95a..cb382f71 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -86,6 +86,35 @@ pub fn config_test(configurator: Configurator, format: &Option) } } +pub fn config_export(configurator: Configurator, format: &Option) +{ + match configurator.invoke_export(ErrorAction::Continue, || { /* code */ }) { + Ok(result) => { + let json = match serde_json::to_string(&result.result) { + Ok(json) => json, + Err(err) => { + eprintln!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + if result.had_errors { + + for msg in result.messages + { + eprintln!("{:?} message {}", msg.level, msg.message); + }; + + exit(EXIT_DSC_ERROR); + } + }, + Err(err) => { + eprintln!("Error: {err}"); + exit(EXIT_DSC_ERROR); + } + } +} + pub fn config(subcommand: &ConfigSubCommand, format: &Option, stdin: &Option) { if stdin.is_none() { eprintln!("Configuration must be piped to STDIN"); @@ -134,6 +163,9 @@ pub fn config(subcommand: &ConfigSubCommand, format: &Option, stdi }, ConfigSubCommand::Validate => { validate_config(&json_string); + }, + ConfigSubCommand::Export => { + config_export(configurator, format); } } } diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 new file mode 100644 index 00000000..daf5e87f --- /dev/null +++ b/dsc/tests/dsc_export.tests.ps1 @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'resource export tests' { + + It 'Export can be called on individual resource' { + + $processes = dsc resource export -r Microsoft/Process + $LASTEXITCODE | Should -Be 0 + $processes.count | Should -BeGreaterThan 1 + } + + It 'Export can be called on a configuration' { + + $json = @' + $schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json + resources: + - name: Processes + type: Microsoft/Process + properties: + pid: 0 +'@ + $out = $json | dsc config export + $LASTEXITCODE | Should -Be 0 + $config_with_process_list = $out | ConvertFrom-Json + $config_with_process_list.resources.count | Should -BeGreaterThan 1 + } + + It 'Configuration Export can be piped to configuration Set' { + + $json = @' + $schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json + resources: + - name: Processes + type: Microsoft/Process + properties: + pid: 0 +'@ + $out = $json | dsc config export | dsc config set + $LASTEXITCODE | Should -Be 0 + $set_results = $out | ConvertFrom-Json + $set_results.results.count | Should -BeGreaterThan 1 + $set_results.results[0].result.afterState.result | Should -BeExactly "Ok" + } +} diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 9268aaee..7eea8c4e 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -91,3 +91,28 @@ impl Default for Configuration { } } } + +impl Configuration { + #[must_use] + pub fn new() -> Self { + Self { + schema: SCHEMA.to_string(), + parameters: None, + variables: None, + resources: Vec::new(), + metadata: None, + } + } +} + +impl Resource { + #[must_use] + pub fn new() -> Self { + Self { + resource_type: String::new(), + name: String::new(), + depends_on: None, + properties: None, + } + } +} diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index dffbb5a7..1091dfe4 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -3,7 +3,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; +use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult, ExportResult}; +use crate::configure::config_doc; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub enum MessageLevel { @@ -126,3 +127,38 @@ impl Default for ConfigurationTestResult { Self::new() } } + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResourceExportResult { + pub name: String, + #[serde(rename="type")] + pub resource_type: String, + pub result: ExportResult, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ConfigurationExportResult { + pub result: Option, + pub messages: Vec, + #[serde(rename = "hadErrors")] + pub had_errors: bool, +} + +impl ConfigurationExportResult { + #[must_use] + pub fn new() -> Self { + Self { + result: None, + messages: Vec::new(), + had_errors: false, + } + } +} + +impl Default for ConfigurationExportResult { + fn default() -> Self { + Self::new() + } +} diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index d866d34c..25089110 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -7,8 +7,9 @@ use crate::dscerror::DscError; use crate::dscresources::dscresource::Invoke; use crate::discovery::Discovery; use self::config_doc::Configuration; -use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ResourceMessage, MessageLevel}; use self::depends_on::get_resource_invocation_order; +use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult, ResourceMessage, MessageLevel}; +use std::collections::HashMap; pub mod config_doc; pub mod config_result; @@ -152,6 +153,40 @@ impl Configurator { Ok(result) } + pub fn invoke_export(&self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { + let (config, messages, had_errors) = self.validate_config()?; + let mut result = ConfigurationExportResult::new(); + result.messages = messages; + result.had_errors = had_errors; + if had_errors { + return Ok(result); + }; + let mut conf = config_doc::Configuration::new(); + + for resource in &config.resources { + let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type).next() else { + return Err(DscError::ResourceNotFound(resource.resource_type.clone())); + }; + let export_result = dsc_resource.export()?; + + for (i, instance) in export_result.actual_state.iter().enumerate() + { + let mut r = config_doc::Resource::new(); + r.resource_type = dsc_resource.type_name.clone(); + r.name = format!("{}-{i}", r.resource_type); + let props: HashMap = serde_json::from_value(instance.clone()).unwrap(); + r.properties = Some(props); + + conf.resources.push(r); + } + + } + + result.result = Some(conf); + + Ok(result) + } + fn validate_config(&self) -> Result<(Configuration, Vec, bool), DscError> { let config: Configuration = serde_json::from_str(self.config.as_str())?; let mut messages: Vec = Vec::new(); diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 1d661dce..132e5598 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -54,7 +54,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul /// /// Error returned if the resource does not successfully set the desired state pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str) -> Result { - let Some(set) = &resource.set else { + let Some(set) = resource.set.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; verify_json(resource, cwd, desired)?; @@ -69,7 +69,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str) -> Resu }); } } - let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), Some(desired), Some(cwd))?; + let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), Some(desired), Some(cwd))?; let pre_state: Value = if exit_code == 0 { serde_json::from_str(&stdout)? } @@ -256,12 +256,10 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result Result { - let (exit_code, stdout, stderr) = invoke_command(&resource.export.clone().unwrap().executable, resource.export.clone().unwrap().args.clone(), None, Some(cwd))?; if exit_code != 0 { return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); } - let mut instances: Vec = Vec::new(); for line in stdout.lines() { diff --git a/process/ExportTest.dsc.yaml b/process/ExportTest.dsc.yaml new file mode 100644 index 00000000..1fc6cbc9 --- /dev/null +++ b/process/ExportTest.dsc.yaml @@ -0,0 +1,7 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json +resources: +- name: Processes + type: Microsoft/Process + properties: + pid: 0 diff --git a/process/process.dsc.resource.json b/process/process.dsc.resource.json index fd9ad371..d3b3817c 100644 --- a/process/process.dsc.resource.json +++ b/process/process.dsc.resource.json @@ -1,7 +1,7 @@ { "manifestVersion": "1.0", "description": "Returns information about running processes.", - "type": "Microsoft/ProcessList", + "type": "Microsoft/Process", "version": "0.1.0", "get": { "executable": "process", @@ -23,5 +23,25 @@ "args": [ "list" ] + }, + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Process", + "type": "object", + "required": [], + "properties": { + "pid": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "cmdline": { + "type": "string" + } + }, + "additionalProperties": false + } } } From 2cdd1030ed80508c7e19759a1f9160898f7c2e20 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 24 Aug 2023 22:36:38 -0700 Subject: [PATCH 03/18] Removed unused type --- dsc_lib/src/configure/config_result.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index 1091dfe4..96ab11ea 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult, ExportResult}; +use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; use crate::configure::config_doc; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -128,15 +128,6 @@ impl Default for ConfigurationTestResult { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct ResourceExportResult { - pub name: String, - #[serde(rename="type")] - pub resource_type: String, - pub result: ExportResult, -} - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ConfigurationExportResult { From 7294a4eac90e9cd41cff62b3688b7dfa32241d54 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 24 Aug 2023 23:08:53 -0700 Subject: [PATCH 04/18] Fixed style warnings --- dsc_lib/src/configure/config_doc.rs | 6 ++++++ dsc_lib/src/dscresources/command_resource.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 7eea8c4e..2938bf39 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -116,3 +116,9 @@ impl Resource { } } } + +impl Default for Resource { + fn default() -> Self { + Self::new() + } +} diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 132e5598..8c99b3e3 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -263,7 +263,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str) -> Result = Vec::new(); for line in stdout.lines() { - let instance: Value = match serde_json::from_str(&line){ + let instance: Value = match serde_json::from_str(line){ Result::Ok(r) => {r}, Result::Err(err) => { return Err(DscError::Operation(format!("Failed to parse json from export {}|{}|{} -> {err}", &resource.export.clone().unwrap().executable, stdout, stderr))) From 50f57cf81e7c3b382b8f7aa04839ef392e6da905 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 25 Aug 2023 12:29:03 -0700 Subject: [PATCH 05/18] Feedback 1 --- dsc/tests/dsc_export.tests.ps1 | 8 ++++---- dsc_lib/src/configure/mod.rs | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index daf5e87f..ac8550e7 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -12,7 +12,7 @@ Describe 'resource export tests' { It 'Export can be called on a configuration' { - $json = @' + $yaml = @' $schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json resources: - name: Processes @@ -20,7 +20,7 @@ Describe 'resource export tests' { properties: pid: 0 '@ - $out = $json | dsc config export + $out = $yaml | dsc config export $LASTEXITCODE | Should -Be 0 $config_with_process_list = $out | ConvertFrom-Json $config_with_process_list.resources.count | Should -BeGreaterThan 1 @@ -28,7 +28,7 @@ Describe 'resource export tests' { It 'Configuration Export can be piped to configuration Set' { - $json = @' + $yaml = @' $schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json resources: - name: Processes @@ -36,7 +36,7 @@ Describe 'resource export tests' { properties: pid: 0 '@ - $out = $json | dsc config export | dsc config set + $out = $yaml | dsc config export | dsc config set $LASTEXITCODE | Should -Be 0 $set_results = $out | ConvertFrom-Json $set_results.results.count | Should -BeGreaterThan 1 diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 25089110..149f54d8 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -155,9 +155,13 @@ impl Configurator { pub fn invoke_export(&self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let (config, messages, had_errors) = self.validate_config()?; - let mut result = ConfigurationExportResult::new(); - result.messages = messages; - result.had_errors = had_errors; + + let mut result = ConfigurationExportResult { + result: None, + messages, + had_errors + }; + if had_errors { return Ok(result); }; From 8961115d669dfb8c61392d3d8ebd080b89aa5948 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 24 Aug 2023 22:32:11 -0700 Subject: [PATCH 06/18] Configuration export functionality and tests --- dsc_lib/src/configure/config_result.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index 96ab11ea..4e71cca7 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -3,7 +3,11 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +<<<<<<< HEAD use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; +======= +use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult, ExportResult}; +>>>>>>> 660b56b (Configuration export functionality and tests) use crate::configure::config_doc; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] From 0ff23f4232d21d8e7827bf684f402d86d0b8095e Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 09:31:21 -0700 Subject: [PATCH 07/18] Updated 'dsc resource export' to return configuration document --- dsc/src/resource_command.rs | 43 +++++++++++++++++--------- dsc_lib/src/configure/config_result.rs | 4 --- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 081f541b..ebcf0278 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -3,6 +3,9 @@ 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::Resource; +use dsc_lib::configure::config_doc::Configuration; +use std::collections::HashMap; use dsc_lib::{ dscresources::dscresource::{Invoke, DscResource}, @@ -130,27 +133,37 @@ pub fn schema(dsc: &mut DscManager, resource: &str, format: &Option) { - let resource = get_resource(dsc, resource); + let dsc_resource = get_resource(dsc, resource); - match resource.export() { - Ok(result) => { - for instance in result.actual_state - { - let json = match serde_json::to_string(&instance) { - Ok(json) => json, - Err(err) => { - eprintln!("JSON Error: {err}"); - exit(EXIT_JSON_ERROR); - } - }; - write_output(&json, format); - } - } + let mut conf = Configuration::new(); + + let export_result = match dsc_resource.export() { + Ok(export) => { export } Err(err) => { eprintln!("Error: {err}"); exit(EXIT_DSC_ERROR); } + }; + + for (i, instance) in export_result.actual_state.iter().enumerate() + { + let mut r = Resource::new(); + r.resource_type = dsc_resource.type_name.clone(); + r.name = format!("{}-{i}", r.resource_type); + let props: HashMap = serde_json::from_value(instance.clone()).unwrap(); + r.properties = Some(props); + + conf.resources.push(r); } + + let json = match serde_json::to_string(&conf) { + Ok(json) => json, + Err(err) => { + eprintln!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); } pub fn get_resource(dsc: &mut DscManager, resource: &str) -> DscResource { diff --git a/dsc_lib/src/configure/config_result.rs b/dsc_lib/src/configure/config_result.rs index 4e71cca7..96ab11ea 100644 --- a/dsc_lib/src/configure/config_result.rs +++ b/dsc_lib/src/configure/config_result.rs @@ -3,11 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -<<<<<<< HEAD use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; -======= -use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult, ExportResult}; ->>>>>>> 660b56b (Configuration export functionality and tests) use crate::configure::config_doc; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] From 950d8bc468ddf49ef45e83214428896c714d8179 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 10:08:54 -0700 Subject: [PATCH 08/18] Added get-all operation --- dsc/src/args.rs | 2 ++ dsc/src/resource_command.rs | 29 ++++++++++++++++++++ dsc/src/subcommand.rs | 5 ++-- dsc_lib/src/dscresources/command_resource.rs | 6 ++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/dsc/src/args.rs b/dsc/src/args.rs index d06312af..3a310b36 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -67,6 +67,8 @@ pub enum ResourceSubCommand { }, #[clap(name = "get", about = "Invoke the get operation to a resource", arg_required_else_help = true)] Get { + #[clap(short, long, help = "Get all instances of the resource")] + all: bool, #[clap(short, long, help = "The name or DscResource JSON of the resource to invoke `get` on")] resource: String, #[clap(short, long, help = "The input to pass to the resource as JSON")] diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index ebcf0278..4634dd67 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -6,6 +6,7 @@ use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, add_type_n use dsc_lib::configure::config_doc::Resource; use dsc_lib::configure::config_doc::Configuration; use std::collections::HashMap; +use dsc_lib::dscresources::invoke_result::GetResult; use dsc_lib::{ dscresources::dscresource::{Invoke, DscResource}, @@ -45,6 +46,34 @@ pub fn get(dsc: &mut DscManager, resource: &str, input: &Option, stdin: } } +pub fn get_all(dsc: &mut DscManager, resource: &str, _input: &Option, _stdin: &Option, format: &Option) { + let resource = get_resource(dsc, resource); + + let export_result = match resource.export() { + Ok(export) => { export } + Err(err) => { + eprintln!("Error: {err}"); + exit(EXIT_DSC_ERROR); + } + }; + + for instance in export_result.actual_state + { + let get_result = GetResult { + actual_state: instance.clone(), + }; + + let json = match serde_json::to_string(&get_result) { + Ok(json) => json, + Err(err) => { + eprintln!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + } +} + pub fn set(dsc: &mut DscManager, resource: &str, input: &Option, stdin: &Option, format: &Option) { let mut input = get_input(input, stdin); let mut resource = get_resource(dsc, resource); diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index cb382f71..679e6a25 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -385,8 +385,9 @@ pub fn resource(subcommand: &ResourceSubCommand, format: &Option, table.print(); } }, - ResourceSubCommand::Get { resource, input } => { - resource_command::get(&mut dsc, resource, input, stdin, format); + ResourceSubCommand::Get { resource, input, all } => { + if *all { resource_command::get_all(&mut dsc, resource, input, stdin, format); } + else { resource_command::get(&mut dsc, resource, input, stdin, format); }; }, ResourceSubCommand::Set { resource, input } => { resource_command::set(&mut dsc, resource, input, stdin, format); diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 8c99b3e3..083a940b 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -256,6 +256,12 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result Result { + + if resource.export.is_none() + { + return Err(DscError::Operation(format!("Export is not supported by resource {}", &resource.resource_type))) + } + let (exit_code, stdout, stderr) = invoke_command(&resource.export.clone().unwrap().executable, resource.export.clone().unwrap().args.clone(), None, Some(cwd))?; if exit_code != 0 { return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); From 4510e0896049f29b1b1f5d039239e9c9bb9b6cb8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 10:32:37 -0700 Subject: [PATCH 09/18] Updated tests --- dsc/tests/dsc_export.tests.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index ac8550e7..4218bb37 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -5,9 +5,18 @@ Describe 'resource export tests' { It 'Export can be called on individual resource' { - $processes = dsc resource export -r Microsoft/Process + $out = dsc resource export -r Microsoft/Process $LASTEXITCODE | Should -Be 0 - $processes.count | Should -BeGreaterThan 1 + $config_with_process_list = $out | ConvertFrom-Json + $config_with_process_list.resources.count | Should -BeGreaterThan 1 + } + + It 'get --all can be called on individual resource' { + + $out = dsc resource get --all -r Microsoft/Process + $LASTEXITCODE | Should -Be 0 + $process_list = $out | ConvertFrom-Json + $process_list.resources.count | Should -BeGreaterThan 1 } It 'Export can be called on a configuration' { From e36fd0b4b5e8b7fa8c854f2b31826332f20a38ba Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 11:30:29 -0700 Subject: [PATCH 10/18] Duplicate resource types in Configuration Export should result in error --- dsc/tests/dsc_export.tests.ps1 | 19 +++++++++++++++++++ dsc_lib/src/configure/mod.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index 4218bb37..8c163629 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -51,4 +51,23 @@ Describe 'resource export tests' { $set_results.results.count | Should -BeGreaterThan 1 $set_results.results[0].result.afterState.result | Should -BeExactly "Ok" } + + It 'Duplicate resource types in Configuration Export should result in error' { + + $yaml = @' + $schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json + resources: + - name: Processes + type: Microsoft/Process + properties: + pid: 0 + - name: Processes + type: Microsoft/Process + properties: + pid: 0 +'@ + $out = $yaml | dsc config export 2>&1 + $LASTEXITCODE | Should -Be 2 + $out | Should -BeLike '*specified multiple times*' + } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 149f54d8..78815d37 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -10,6 +10,7 @@ use self::config_doc::Configuration; use self::depends_on::get_resource_invocation_order; use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult, ResourceMessage, MessageLevel}; use std::collections::HashMap; +use std::collections::HashSet; pub mod config_doc; pub mod config_result; @@ -153,9 +154,35 @@ impl Configurator { Ok(result) } + fn find_duplicate_resource_types(config: &Configuration) -> Vec + { + let mut map: HashMap<&String, i32> = HashMap::new(); + let mut result: HashSet = HashSet::new(); + let resource_list = &config.resources; + if resource_list.len() == 0 { + return Vec::new(); + } + + for r in resource_list + { + let v = map.entry(&r.resource_type).or_insert(0); + *v += 1; + if *v > 1 { + result.insert(r.resource_type.clone()); + } + } + + result.into_iter().collect() + } + pub fn invoke_export(&self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let (config, messages, had_errors) = self.validate_config()?; + for duplicate in Self::find_duplicate_resource_types(&config) + { + return Err(DscError::Validation(format!("Resource {duplicate} specified multiple times"))); + } + let mut result = ConfigurationExportResult { result: None, messages, From 00b804c64f90d47ed6447e0795fabde94cfacd4d Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 11:39:13 -0700 Subject: [PATCH 11/18] Fixed clippy errors --- dsc_lib/src/configure/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 78815d37..8ebe5983 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -159,7 +159,7 @@ impl Configurator { let mut map: HashMap<&String, i32> = HashMap::new(); let mut result: HashSet = HashSet::new(); let resource_list = &config.resources; - if resource_list.len() == 0 { + if resource_list.is_empty() { return Vec::new(); } @@ -178,8 +178,10 @@ impl Configurator { pub fn invoke_export(&self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let (config, messages, had_errors) = self.validate_config()?; - for duplicate in Self::find_duplicate_resource_types(&config) + let duplicates = Self::find_duplicate_resource_types(&config); + if !duplicates.is_empty() { + let duplicate = &duplicates[0]; return Err(DscError::Validation(format!("Resource {duplicate} specified multiple times"))); } From b7607369161c42aaec6d83b6b7be4f77e8513c7e Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 12:18:39 -0700 Subject: [PATCH 12/18] Updated piping test --- dsc/tests/dsc_export.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index 8c163629..ed05a66a 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -35,7 +35,7 @@ Describe 'resource export tests' { $config_with_process_list.resources.count | Should -BeGreaterThan 1 } - It 'Configuration Export can be piped to configuration Set' { + It 'Configuration Export can be piped to configuration Set' -Skip:(!$IsWindows) { $yaml = @' $schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json From c83e56cf7c9fe7e3cace1d69c0b87a10cbf33a6e Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 12:56:46 -0700 Subject: [PATCH 13/18] Fixed bug in Set --- dsc_lib/src/dscresources/command_resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 083a940b..e0ebc65d 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -69,7 +69,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str) -> Resu }); } } - let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), Some(desired), Some(cwd))?; + let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), Some(desired), Some(cwd))?; let pre_state: Value = if exit_code == 0 { serde_json::from_str(&stdout)? } From 0265beb92b569f0e067eb22bb70033bf7b0ecf76 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 14:06:28 -0700 Subject: [PATCH 14/18] Updated Process 1 --- process/process.dsc.resource.json | 9 ++++++++- process/src/main.rs | 33 +++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/process/process.dsc.resource.json b/process/process.dsc.resource.json index d3b3817c..ee3f3ce5 100644 --- a/process/process.dsc.resource.json +++ b/process/process.dsc.resource.json @@ -15,9 +15,16 @@ "set" ], "input": "stdin", - "preTest": true, + "preTest": false, "return": "state" }, + "test": { + "executable": "process", + "args": [ + "test" + ], + "input": "stdin" + }, "export": { "executable": "process", "args": [ diff --git a/process/src/main.rs b/process/src/main.rs index 5bd47034..2b8dc608 100644 --- a/process/src/main.rs +++ b/process/src/main.rs @@ -4,21 +4,24 @@ mod process_info; use std::env; use std::process::exit; +use std::io::{self, Read}; use sysinfo::{ProcessExt, System, SystemExt, PidExt}; +use crate::process_info::ProcessInfo; -fn print_task_list() { - +fn get_task_list() -> Vec +{ + let mut result = Vec::new(); let mut s = System::new(); s.refresh_processes(); for (pid, process) in s.processes() { - let mut p = process_info::ProcessInfo::new(); + let mut p = ProcessInfo::new(); p.pid = pid.as_u32(); p.name = String::from(process.name()); p.cmdline = format!("{:?}", process.cmd()); - - let json = serde_json::to_string(&p).unwrap(); - println!("{json}"); + result.push(p); } + + result } fn help() { @@ -31,11 +34,25 @@ fn main() { // one argument passed match args[1].as_str() { "list" => { - print_task_list(); + for p in get_task_list() + { + let json = serde_json::to_string(&p).unwrap(); + println!("{json}"); + } + exit(0); + }, + "get" => { // used for testing only + let mut buffer: Vec = Vec::new(); + io::stdin().read_to_end(&mut buffer).unwrap(); + let input = String::from_utf8(buffer); + println!("{}", input.unwrap()); exit(0); }, "set" => { // used for testing only - println!("{{\"result\":\"Ok\"}}"); + let mut buffer: Vec = Vec::new(); + io::stdin().read_to_end(&mut buffer).unwrap(); + let input = String::from_utf8(buffer); + println!("{}", input.unwrap()); exit(0); }, _ => { From 1aa95049309998ea8220aeaa8de516546002e773 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 14:12:23 -0700 Subject: [PATCH 15/18] Updated Process 2 --- process/process.dsc.resource.json | 5 +++-- process/src/main.rs | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/process/process.dsc.resource.json b/process/process.dsc.resource.json index ee3f3ce5..e5391cc2 100644 --- a/process/process.dsc.resource.json +++ b/process/process.dsc.resource.json @@ -23,8 +23,9 @@ "args": [ "test" ], - "input": "stdin" - }, + "input": "stdin", + "return": "state" + }, "export": { "executable": "process", "args": [ diff --git a/process/src/main.rs b/process/src/main.rs index 2b8dc608..48e50076 100644 --- a/process/src/main.rs +++ b/process/src/main.rs @@ -28,6 +28,13 @@ fn help() { println!("usage: process list"); } +fn print_input() { + let mut buffer: Vec = Vec::new(); + io::stdin().read_to_end(&mut buffer).unwrap(); + let input = String::from_utf8(buffer); + println!("{}", input.unwrap()); +} + fn main() { let args: Vec = env::args().collect(); if args.len() == 2 { @@ -42,17 +49,15 @@ fn main() { exit(0); }, "get" => { // used for testing only - let mut buffer: Vec = Vec::new(); - io::stdin().read_to_end(&mut buffer).unwrap(); - let input = String::from_utf8(buffer); - println!("{}", input.unwrap()); + print_input(); exit(0); }, "set" => { // used for testing only - let mut buffer: Vec = Vec::new(); - io::stdin().read_to_end(&mut buffer).unwrap(); - let input = String::from_utf8(buffer); - println!("{}", input.unwrap()); + print_input(); + exit(0); + }, + "test" => { // used for testing only + print_input(); exit(0); }, _ => { From 52d0415e9833865914a5d09258e322a4c1796532 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 14:33:20 -0700 Subject: [PATCH 16/18] Updated Process 3 --- dsc/tests/dsc_export.tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index ed05a66a..94b55659 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -49,7 +49,6 @@ Describe 'resource export tests' { $LASTEXITCODE | Should -Be 0 $set_results = $out | ConvertFrom-Json $set_results.results.count | Should -BeGreaterThan 1 - $set_results.results[0].result.afterState.result | Should -BeExactly "Ok" } It 'Duplicate resource types in Configuration Export should result in error' { From 6d5177626bc89945cbb4e4fa1bd2cac22d7622d6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 16:03:53 -0700 Subject: [PATCH 17/18] Feedback latest --- dsc/src/resource_command.rs | 22 ++------------------ dsc/tests/dsc_export.tests.ps1 | 5 +++++ dsc_lib/src/configure/mod.rs | 37 ++++++++++++++++++---------------- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 4634dd67..0cae2db2 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -3,9 +3,8 @@ 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::Resource; use dsc_lib::configure::config_doc::Configuration; -use std::collections::HashMap; +use dsc_lib::configure::add_resource_export_results_to_configuration; use dsc_lib::dscresources::invoke_result::GetResult; use dsc_lib::{ @@ -166,24 +165,7 @@ pub fn export(dsc: &mut DscManager, resource: &str, format: &Option { export } - Err(err) => { - eprintln!("Error: {err}"); - exit(EXIT_DSC_ERROR); - } - }; - - for (i, instance) in export_result.actual_state.iter().enumerate() - { - let mut r = Resource::new(); - r.resource_type = dsc_resource.type_name.clone(); - r.name = format!("{}-{i}", r.resource_type); - let props: HashMap = serde_json::from_value(instance.clone()).unwrap(); - r.properties = Some(props); - - conf.resources.push(r); - } + add_resource_export_results_to_configuration(&dsc_resource, &mut conf); let json = match serde_json::to_string(&conf) { Ok(json) => json, diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index 94b55659..df70fe42 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -8,6 +8,8 @@ Describe 'resource export tests' { $out = dsc resource export -r Microsoft/Process $LASTEXITCODE | Should -Be 0 $config_with_process_list = $out | ConvertFrom-Json + $config_with_process_list.'$schema' | Should -Not -BeNullOrEmpty + $config_with_process_list.'resources' | Should -Not -BeNullOrEmpty $config_with_process_list.resources.count | Should -BeGreaterThan 1 } @@ -17,6 +19,7 @@ Describe 'resource export tests' { $LASTEXITCODE | Should -Be 0 $process_list = $out | ConvertFrom-Json $process_list.resources.count | Should -BeGreaterThan 1 + $process_list | % {$_.actualState | Should -Not -BeNullOrEmpty} } It 'Export can be called on a configuration' { @@ -32,6 +35,8 @@ Describe 'resource export tests' { $out = $yaml | dsc config export $LASTEXITCODE | Should -Be 0 $config_with_process_list = $out | ConvertFrom-Json + $config_with_process_list.'$schema' | Should -Not -BeNullOrEmpty + $config_with_process_list.'resources' | Should -Not -BeNullOrEmpty $config_with_process_list.resources.count | Should -BeGreaterThan 1 } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 8ebe5983..30348914 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -5,12 +5,12 @@ use jsonschema::JSONSchema; use crate::dscerror::DscError; use crate::dscresources::dscresource::Invoke; +use crate::DscResource; use crate::discovery::Discovery; use self::config_doc::Configuration; use self::depends_on::get_resource_invocation_order; use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult, ResourceMessage, MessageLevel}; -use std::collections::HashMap; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; pub mod config_doc; pub mod config_result; @@ -27,6 +27,21 @@ pub enum ErrorAction { Stop, } +pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf: &mut Configuration) { + let export_result = resource.export().unwrap(); + + for (i, instance) in export_result.actual_state.iter().enumerate() + { + let mut r = config_doc::Resource::new(); + r.resource_type = resource.type_name.clone(); + r.name = format!("{}-{i}", r.resource_type); + let props: HashMap = serde_json::from_value(instance.clone()).unwrap(); + r.properties = Some(props); + + conf.resources.push(r); + } +} + impl Configurator { /// Create a new `Configurator` instance. /// @@ -181,8 +196,8 @@ impl Configurator { let duplicates = Self::find_duplicate_resource_types(&config); if !duplicates.is_empty() { - let duplicate = &duplicates[0]; - return Err(DscError::Validation(format!("Resource {duplicate} specified multiple times"))); + let duplicates_string = &duplicates.join(","); + return Err(DscError::Validation(format!("Resource(s) {duplicates_string} specified multiple times"))); } let mut result = ConfigurationExportResult { @@ -200,19 +215,7 @@ impl Configurator { let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type).next() else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; - let export_result = dsc_resource.export()?; - - for (i, instance) in export_result.actual_state.iter().enumerate() - { - let mut r = config_doc::Resource::new(); - r.resource_type = dsc_resource.type_name.clone(); - r.name = format!("{}-{i}", r.resource_type); - let props: HashMap = serde_json::from_value(instance.clone()).unwrap(); - r.properties = Some(props); - - conf.resources.push(r); - } - + add_resource_export_results_to_configuration(&dsc_resource, &mut conf); } result.result = Some(conf); From 1c6b8c8ba9d41a91aeb86714411692160a1d668c Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 16:25:02 -0700 Subject: [PATCH 18/18] Feedback latest 2 --- dsc/tests/dsc_export.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index df70fe42..7c8ae7ee 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -8,7 +8,7 @@ Describe 'resource export tests' { $out = dsc resource export -r Microsoft/Process $LASTEXITCODE | Should -Be 0 $config_with_process_list = $out | ConvertFrom-Json - $config_with_process_list.'$schema' | Should -Not -BeNullOrEmpty + $config_with_process_list.'$schema' | Should -BeExactly 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json' $config_with_process_list.'resources' | Should -Not -BeNullOrEmpty $config_with_process_list.resources.count | Should -BeGreaterThan 1 } @@ -35,7 +35,7 @@ Describe 'resource export tests' { $out = $yaml | dsc config export $LASTEXITCODE | Should -Be 0 $config_with_process_list = $out | ConvertFrom-Json - $config_with_process_list.'$schema' | Should -Not -BeNullOrEmpty + $config_with_process_list.'$schema' | Should -BeExactly 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json' $config_with_process_list.'resources' | Should -Not -BeNullOrEmpty $config_with_process_list.resources.count | Should -BeGreaterThan 1 }