diff --git a/dsc/Cargo.lock b/dsc/Cargo.lock index a83d9952e..5db179a66 100644 --- a/dsc/Cargo.lock +++ b/dsc/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "serde", "version_check", @@ -551,6 +551,7 @@ dependencies = [ "tree-sitter", "tree-sitter-dscexpression", "tree-sitter-rust", + "uuid", ] [[package]] @@ -668,7 +669,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -1098,7 +1111,7 @@ dependencies = [ "hermit-abi", "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2172,9 +2185,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +dependencies = [ + "getrandom 0.3.1", +] [[package]] name = "uuid-simd" @@ -2254,6 +2270,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -2493,6 +2518,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 9da575d27..fcd9010ba 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -4,7 +4,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use clap_complete::Shell; use dsc_lib::dscresources::command_resource::TraceLevel; -use dsc_lib::util::ProgressFormat; +use dsc_lib::progress::ProgressFormat; use rust_i18n::t; use serde::Deserialize; diff --git a/dsc/src/main.rs b/dsc/src/main.rs index cf977777c..34cd1d2ad 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -8,7 +8,7 @@ use rust_i18n::{i18n, t}; use std::{io, process::exit}; use sysinfo::{Process, RefreshKind, System, get_current_pid, ProcessRefreshKind}; use tracing::{error, info, warn, debug}; -use dsc_lib::util::ProgressFormat; +use dsc_lib::progress::ProgressFormat; #[cfg(debug_assertions)] use crossterm::event; diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 177638649..a516ae80e 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -25,7 +25,7 @@ use dsc_lib::{ }, dscresources::dscresource::{Capability, ImplementedAs, Invoke}, dscresources::resource_manifest::{import_manifest, ResourceManifest}, - util::ProgressFormat, + progress::ProgressFormat, }; use rust_i18n::t; use std::{ @@ -288,7 +288,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounte } }; - let mut configurator = match Configurator::new(&json_string) { + let mut configurator = match Configurator::new(&json_string, progress_format) { Ok(configurator) => configurator, Err(err) => { error!("Error: {err}"); @@ -296,8 +296,6 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounte } }; - configurator.set_progress_format(progress_format); - if let ConfigSubCommand::Set { what_if , .. } = subcommand { if *what_if { configurator.context.execution_type = ExecutionKind::WhatIf; diff --git a/dsc/tests/dsc_args.tests.ps1 b/dsc/tests/dsc_args.tests.ps1 index 5a888c8f1..9563780b6 100644 --- a/dsc/tests/dsc_args.tests.ps1 +++ b/dsc/tests/dsc_args.tests.ps1 @@ -303,4 +303,10 @@ resources: $LASTEXITCODE | Should -Be 1 "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Target path does not exist: '/invalid/path'" } + + It '--progress-format can be None' { + dsc -p none resource list 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/tracing.txt -Raw) | Should -BeNullOrEmpty + } } diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index ae98b4477..07c1b7d7e 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -60,23 +60,92 @@ Describe 'dsc config get tests' { $config_yaml = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json resources: - - name: Echo + - name: Echo 1 type: Microsoft.DSC.Debug/Echo properties: output: hello + - name: Echo 2 + type: Microsoft.DSC.Debug/Echo + properties: + output: world "@ $config_yaml | dsc --progress-format json config get -f - 2> $TestDrive/ErrorStream.txt $LASTEXITCODE | Should -Be 0 $lines = Get-Content $TestDrive/ErrorStream.txt - $ProgressMessagesFound = $False + $ProgressMessagesFound = $false + $InstanceOneFound = $false + $InstanceTwoFound = $false foreach ($line in $lines) { $jp = $line | ConvertFrom-Json if ($jp.activity) { # if line is a progress message - $jp.percent_complete | Should -BeIn (0..100) - $ProgressMessagesFound = $True + $jp.id | Should -Not -BeNullOrEmpty + $jp.totalItems | Should -Not -BeNullOrEmpty + $jp.completedItems | Should -Not -BeNullOrEmpty + $ProgressMessagesFound = $true + } + + if ($null -ne $jp.result -and $jp.resourceType -eq 'Microsoft.DSC.Debug/Echo') { + if ($jp.resourceName -eq 'Echo 1') { + $InstanceOneFound = $true + $jp.result.actualState.output | Should -BeExactly 'hello' + } elseif ($jp.resourceName -eq 'Echo 2') { + $InstanceTwoFound = $true + $jp.result.actualState.output | Should -BeExactly 'world' + } + } + } + $ProgressMessagesFound | Should -BeTrue + $InstanceOneFound | Should -BeTrue + $InstanceTwoFound | Should -BeTrue + } + + It 'json progress returns correctly for failed resource' { + $config_yaml = @' + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: Echo 1 + type: Microsoft.DSC.Debug/Echo + properties: + output: hello + - name: ErrorTest + type: Test/ExitCode + properties: + exitCode: 8 +'@ + dsc --progress-format json --trace-format json config get -i $config_yaml 2> $TestDrive/ErrorStream.txt + $LASTEXITCODE | Should -Be 2 + $lines = Get-Content $TestDrive/ErrorStream.txt + $ProgressMessagesFound = $false + $InstanceOneFound = $false + $InstanceTwoFound = $false + foreach ($line in $lines) { + $jp = $line | ConvertFrom-Json + if ($jp.activity) { # if line is a progress message + $jp.id | Should -Not -BeNullOrEmpty + $jp.totalItems | Should -Not -BeNullOrEmpty + $jp.completedItems | Should -Not -BeNullOrEmpty + $ProgressMessagesFound = $true + } + + if ($null -ne $jp.result -and $jp.resourceType -eq 'Microsoft.DSC.Debug/Echo') { + if ($jp.resourceName -eq 'Echo 1') { + $InstanceOneFound = $true + $jp.result.actualState.output | Should -BeExactly 'hello' + $jp.failed | Should -BeNullOrEmpty + } + } + elseif ($null -ne $jp.failure -and $jp.resourceType -eq 'Test/ExitCode') { + if ($jp.resourceName -eq 'ErrorTest') { + $InstanceTwoFound = $true + $jp.result | Should -BeNullOrEmpty + $jp.failure.exitCode | Should -Be 8 + $jp.failure.message | Should -Not -BeNullOrEmpty + } } } $ProgressMessagesFound | Should -BeTrue + $InstanceOneFound | Should -BeTrue + $InstanceTwoFound | Should -BeTrue } It 'contentVersion is ignored' { diff --git a/dsc/tests/dsc_resource_list.tests.ps1 b/dsc/tests/dsc_resource_list.tests.ps1 index b8fd4ff40..4e2e00c02 100644 --- a/dsc/tests/dsc_resource_list.tests.ps1 +++ b/dsc/tests/dsc_resource_list.tests.ps1 @@ -66,7 +66,9 @@ Describe 'Tests for listing resources' { foreach ($line in $lines) { $jp = $line | ConvertFrom-Json if ($jp.activity) { # if line is a progress message - $jp.percent_complete | Should -BeIn (0..100) + $jp.id | Should -Not -BeNullOrEmpty + $jp.totalItems | Should -Not -BeNullOrEmpty + $jp.completedItems | Should -Not -BeNullOrEmpty $ProgressMessagesFound = $True } } diff --git a/dsc_lib/Cargo.lock b/dsc_lib/Cargo.lock index 9a1ae9b23..7a9f8f754 100644 --- a/dsc_lib/Cargo.lock +++ b/dsc_lib/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "tree-sitter", "tree-sitter-dscexpression", "tree-sitter-rust", + "uuid", ] [[package]] @@ -1833,9 +1834,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +dependencies = [ + "getrandom 0.3.1", +] [[package]] name = "uuid-simd" diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index 6ef40745e..b41c12d87 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -35,6 +35,7 @@ tracing-indicatif = { version = "0.3" } tree-sitter = "0.25" tree-sitter-rust = "0.23" tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" } +uuid = { version = "1.13", features = ["v4"] } [dev-dependencies] serde_yaml = "0.9" diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index cb8cd2905..04055ce87 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -314,6 +314,9 @@ unknown = "Unknown" validation = "Validation" setting = "Setting" +[progress] +failedToSerialize = "Failed to serialize progress JSON: %{json}" + [util] foundSetting = "Found setting '%{name}' in %{path}" notFoundSetting = "Setting '%{name}' not found in %{path}" diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 144e03145..bf0886485 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -13,19 +13,17 @@ use crate::dscresources::{ use crate::DscResource; use crate::discovery::Discovery; use crate::parser::Statement; -use crate::ProgressFormat; -use crate::util::ProgressBar; +use crate::progress::{Failure, ProgressBar, ProgressFormat}; use self::context::Context; use self::config_doc::{Configuration, DataType, MicrosoftDscMetadata, Operation, SecurityContextKind}; use self::depends_on::get_resource_invocation_order; use self::config_result::{ConfigurationExportResult, ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult}; use self::constraints::{check_length, check_number_limits, check_allowed_values}; -use indicatif::ProgressStyle; use rust_i18n::t; use security_context_lib::{SecurityContext, get_security_context}; use serde_json::{Map, Value}; use std::path::PathBuf; -use std::{collections::HashMap, mem}; +use std::collections::HashMap; use tracing::{debug, info, trace}; pub mod context; pub mod config_doc; @@ -138,16 +136,6 @@ fn escape_property_values(properties: &Map) -> Result Result { - let mut pb_span = ProgressBar::new(progress_format == ProgressFormat::Json); - - pb_span.pb_set_style(&ProgressStyle::with_template( - "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" - )?); - pb_span.pb_set_length(len); - Ok(pb_span) -} - fn add_metadata(kind: &Kind, mut properties: Option> ) -> Result { if *kind == Kind::Adapter { // add metadata to the properties so the adapter knows this is a config @@ -206,7 +194,7 @@ impl Configurator { /// # Errors /// /// This function will return an error if the configuration is invalid or the underlying discovery fails. - pub fn new(json: &str) -> Result { + pub fn new(json: &str, progress_format: ProgressFormat) -> Result { let discovery = Discovery::new()?; let mut config = Configurator { json: json.to_owned(), @@ -214,7 +202,7 @@ impl Configurator { context: Context::new(), discovery, statement_parser: Statement::new()?, - progress_format: ProgressFormat::Default, + progress_format, }; config.validate_config()?; Ok(config) @@ -230,11 +218,6 @@ impl Configurator { &self.config } - /// Sets progress format for the configuration. - pub fn set_progress_format(&mut self, progress_format: ProgressFormat) { - self.progress_format = progress_format; - } - /// Invoke the get operation on a resource. /// /// # Returns @@ -247,11 +230,10 @@ impl Configurator { pub fn invoke_get(&mut self) -> Result { let mut result = ConfigurationGetResult::new(); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &self.context)?; - let mut pb_span = get_progress_bar_span(resources.len() as u64, self.progress_format)?; - pb_span.enter(); + let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; for resource in resources { - pb_span.pb_set_message(format!("Get '{}'", resource.name).as_str()); - pb_span.pb_inc(1); + progress.set_resource(&resource.name, &resource.resource_type); + progress.write_activity(format!("Get '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(resource.properties.as_ref())?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -260,7 +242,14 @@ impl Configurator { let filter = add_metadata(&dsc_resource.kind, properties)?; trace!("filter: {filter}"); let start_datetime = chrono::Local::now(); - let get_result = dsc_resource.get(&filter)?; + let get_result = match dsc_resource.get(&filter) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; let end_datetime = chrono::Local::now(); match &get_result { GetResult::Resource(resource_result) => { @@ -271,7 +260,7 @@ impl Configurator { for result in group { results.push(serde_json::to_value(&result.result)?); } - self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), Value::Array(results)); + self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), Value::Array(results.clone())); }, } let resource_result = config_result::ResourceGetResult { @@ -287,15 +276,16 @@ impl Configurator { ), name: resource.name.clone(), resource_type: resource.resource_type.clone(), - result: get_result, + result: get_result.clone(), }; result.results.push(resource_result); + progress.set_result(&serde_json::to_value(get_result)?); + progress.write_increment(1); } result.metadata = Some( self.get_result_metadata(Operation::Get) ); - std::mem::drop(pb_span); Ok(result) } @@ -312,14 +302,14 @@ impl Configurator { /// # Errors /// /// This function will return an error if the underlying resource fails. + #[allow(clippy::too_many_lines)] pub fn invoke_set(&mut self, skip_test: bool) -> Result { let mut result = ConfigurationSetResult::new(); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &self.context)?; - let mut pb_span = get_progress_bar_span(resources.len() as u64, self.progress_format)?; - pb_span.enter(); + let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; for resource in resources { - pb_span.pb_set_message(format!("Set '{}'", resource.name).as_str()); - pb_span.pb_inc(1); + progress.set_resource(&resource.name, &resource.resource_type); + progress.write_activity(format!("Set '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(resource.properties.as_ref())?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -349,7 +339,14 @@ impl Configurator { if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) { debug!("{}", t!("configure.mod.handlesExist")); start_datetime = chrono::Local::now(); - set_result = dsc_resource.set(&desired, skip_test, &self.context.execution_type)?; + set_result = match dsc_resource.set(&desired, skip_test, &self.context.execution_type) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; end_datetime = chrono::Local::now(); } else if dsc_resource.capabilities.contains(&Capability::Delete) { if self.context.execution_type == ExecutionKind::WhatIf { @@ -357,10 +354,28 @@ impl Configurator { return Err(DscError::NotSupported(t!("configure.mod.whatIfNotSupportedForDelete").to_string())); } debug!("{}", t!("configure.mod.implementsDelete")); - let before_result = dsc_resource.get(&desired)?; + let before_result = match dsc_resource.get(&desired) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; start_datetime = chrono::Local::now(); - dsc_resource.delete(&desired)?; - let after_result = dsc_resource.get(&desired)?; + if let Err(e) = dsc_resource.delete(&desired) { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + } + let after_result = match dsc_resource.get(&desired) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; // convert get result to set result set_result = match before_result { GetResult::Resource(before_response) => { @@ -393,10 +408,9 @@ impl Configurator { for result in group { results.push(serde_json::to_value(&result.result)?); } - self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), Value::Array(results)); + self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), Value::Array(results.clone())); }, } - let resource_result = config_result::ResourceSetResult { metadata: Some( Metadata { @@ -410,15 +424,16 @@ impl Configurator { ), name: resource.name.clone(), resource_type: resource.resource_type.clone(), - result: set_result, + result: set_result.clone(), }; result.results.push(resource_result); + progress.set_result(&serde_json::to_value(set_result)?); + progress.write_increment(1); } result.metadata = Some( self.get_result_metadata(Operation::Set) ); - mem::drop(pb_span); Ok(result) } @@ -434,11 +449,10 @@ impl Configurator { pub fn invoke_test(&mut self) -> Result { let mut result = ConfigurationTestResult::new(); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &self.context)?; - let mut pb_span = get_progress_bar_span(resources.len() as u64, self.progress_format)?; - pb_span.enter(); + let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; for resource in resources { - pb_span.pb_set_message(format!("Test '{}'", resource.name).as_str()); - pb_span.pb_inc(1); + progress.set_resource(&resource.name, &resource.resource_type); + progress.write_activity(format!("Test '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(resource.properties.as_ref())?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -447,7 +461,14 @@ impl Configurator { let expected = add_metadata(&dsc_resource.kind, properties)?; trace!("{}", t!("configure.mod.expectedState", state = expected)); let start_datetime = chrono::Local::now(); - let test_result = dsc_resource.test(&expected)?; + let test_result = match dsc_resource.test(&expected) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; let end_datetime = chrono::Local::now(); match &test_result { TestResult::Resource(resource_test_result) => { @@ -458,7 +479,7 @@ impl Configurator { for result in group { results.push(serde_json::to_value(&result.result)?); } - self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), Value::Array(results)); + self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), Value::Array(results.clone())); }, } let resource_result = config_result::ResourceTestResult { @@ -474,15 +495,16 @@ impl Configurator { ), name: resource.name.clone(), resource_type: resource.resource_type.clone(), - result: test_result, + result: test_result.clone(), }; result.results.push(resource_result); + progress.set_result( &serde_json::to_value(test_result)?); + progress.write_increment(1); } result.metadata = Some( self.get_result_metadata(Operation::Test) ); - std::mem::drop(pb_span); Ok(result) } @@ -499,25 +521,32 @@ impl Configurator { let mut result = ConfigurationExportResult::new(); let mut conf = config_doc::Configuration::new(); - let mut pb_span = get_progress_bar_span(self.config.resources.len() as u64, self.progress_format)?; - pb_span.enter(); + let mut progress = ProgressBar::new(self.config.resources.len() as u64, self.progress_format)?; let resources = self.config.resources.clone(); for resource in &resources { - pb_span.pb_set_message(format!("Export '{}'", resource.name).as_str()); - pb_span.pb_inc(1); + progress.set_resource(&resource.name, &resource.resource_type); + progress.write_activity(format!("Export '{}'", resource.name).as_str()); let properties = self.invoke_property_expressions(resource.properties.as_ref())?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; let input = add_metadata(&dsc_resource.kind, properties)?; trace!("{}", t!("configure.mod.exportInput", input = input)); - let export_result = add_resource_export_results_to_configuration(dsc_resource, Some(dsc_resource), &mut conf, input.as_str())?; + let export_result = match add_resource_export_results_to_configuration(dsc_resource, Some(dsc_resource), &mut conf, input.as_str()) { + Ok(result) => result, + Err(e) => { + progress.set_failure(get_failure_from_error(&e)); + progress.write_increment(1); + return Err(e); + }, + }; self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&export_result.actual_state)?); + progress.set_result(&serde_json::to_value(export_result)?); + progress.write_increment(1); } conf.metadata = Some(self.get_result_metadata(Operation::Export)); result.result = Some(conf); - std::mem::drop(pb_span); Ok(result) } @@ -763,3 +792,21 @@ impl Configurator { Ok(Some(result)) } } + +fn get_failure_from_error(err: &DscError) -> Option { + match err { + DscError::CommandExit(_resource, exit_code, reason) => { + Some(Failure { + message: reason.to_string(), + exit_code: *exit_code, + }) + }, + DscError::CommandExitFromManifest(_resource, exit_code, reason) => { + Some(Failure { + message: reason.to_string(), + exit_code: *exit_code, + }) + }, + _ => None, + } +} diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index f763e1246..989e6e8ca 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -7,8 +7,7 @@ use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest}; use crate::dscresources::command_resource::invoke_command; use crate::dscerror::DscError; -use crate::util::ProgressFormat; -use indicatif::ProgressStyle; +use crate::progress::{ProgressBar, ProgressFormat}; use linked_hash_map::LinkedHashMap; use regex::RegexBuilder; use rust_i18n::t; @@ -22,10 +21,9 @@ use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; use std::str::FromStr; -use tracing::{debug, info, trace, warn, warn_span}; -use tracing_indicatif::span_ext::IndicatifSpanExt; +use tracing::{debug, info, trace, warn}; -use crate::util::{get_setting, ProgressBar}; +use crate::util::get_setting; use crate::util::get_exe_path; pub struct CommandDiscovery { @@ -33,6 +31,7 @@ pub struct CommandDiscovery { resources: BTreeMap>, adapters: BTreeMap>, adapted_resources: BTreeMap>, + progress_format: ProgressFormat, } #[derive(Deserialize)] @@ -58,11 +57,12 @@ impl Default for ResourcePathSetting { } impl CommandDiscovery { - pub fn new() -> CommandDiscovery { + pub fn new(progress_format: ProgressFormat) -> CommandDiscovery { CommandDiscovery { resources: BTreeMap::new(), adapters: BTreeMap::new(), adapted_resources: BTreeMap::new(), + progress_format, } } @@ -163,7 +163,7 @@ impl CommandDiscovery { impl Default for CommandDiscovery { fn default() -> Self { - Self::new() + Self::new(ProgressFormat::Default) } } @@ -180,12 +180,8 @@ impl ResourceDiscovery for CommandDiscovery { return Err(DscError::Operation(t!("discovery.commandDiscovery.invalidAdapterFilter").to_string())); }; - let pb_span = warn_span!(""); - pb_span.pb_set_style(&ProgressStyle::with_template( - "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" - )?); - pb_span.pb_set_message(t!("discovery.commandDiscovery.progressSearching").to_string().as_str()); - let _ = pb_span.enter(); + let mut progress = ProgressBar::new(1, self.progress_format)?; + progress.write_activity(t!("discovery.commandDiscovery.progressSearching").to_string().as_str()); let mut resources = BTreeMap::>::new(); let mut adapters = BTreeMap::>::new(); @@ -241,13 +237,14 @@ impl ResourceDiscovery for CommandDiscovery { } } } + progress.write_increment(1); debug!("Found {} matching non-adapter-based resources", resources.len()); self.resources = resources; self.adapters = adapters; Ok(()) } - fn discover_adapted_resources(&mut self, name_filter: &str, adapter_filter: &str, progress_format: ProgressFormat) -> Result<(), DscError> { + fn discover_adapted_resources(&mut self, name_filter: &str, adapter_filter: &str) -> Result<(), DscError> { if self.resources.is_empty() && self.adapters.is_empty() { self.discover_resources("*")?; } @@ -272,20 +269,15 @@ impl ResourceDiscovery for CommandDiscovery { return Err(DscError::Operation("Could not build Regex filter for resource name".to_string())); }; - let mut pb_span = ProgressBar::new(progress_format == ProgressFormat::Json); - pb_span.pb_set_style(&ProgressStyle::with_template( - "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" - )?); - pb_span.pb_set_message("Searching for adapted resources"); - pb_span.pb_set_length(self.adapters.len() as u64); - pb_span.enter(); + let mut progress = ProgressBar::new(self.adapters.len() as u64, self.progress_format)?; + progress.write_activity("Searching for adapted resources"); let mut adapted_resources = BTreeMap::>::new(); let mut found_adapter: bool = false; for (adapter_name, adapters) in &self.adapters { for adapter in adapters { - pb_span.pb_inc(1); + progress.write_increment(1); if !regex.is_match(adapter_name) { continue; @@ -293,12 +285,8 @@ impl ResourceDiscovery for CommandDiscovery { found_adapter = true; info!("Enumerating resources for adapter '{}'", adapter_name); - let mut pb_adapter_span = ProgressBar::new(progress_format == ProgressFormat::Json); - pb_adapter_span.pb_set_style(&ProgressStyle::with_template( - "{spinner:.green} [{elapsed_precise:.cyan}] {msg:.white}" - )?); - pb_adapter_span.pb_set_message(format!("Enumerating resources for adapter '{adapter_name}'").as_str()); - pb_adapter_span.enter(); + let mut adapter_progress = ProgressBar::new(1, self.progress_format)?; + adapter_progress.write_activity(format!("Enumerating resources for adapter '{adapter_name}'").as_str()); let manifest = if let Some(manifest) = &adapter.manifest { if let Ok(manifest) = import_manifest(manifest.clone()) { manifest @@ -350,6 +338,7 @@ impl ResourceDiscovery for CommandDiscovery { }; } + adapter_progress.write_increment(1); debug!("Adapter '{}' listed {} resources", adapter_name, adapter_resources_count); } } @@ -363,7 +352,7 @@ impl ResourceDiscovery for CommandDiscovery { Ok(()) } - fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str, progress_format: ProgressFormat) -> Result>, DscError> { + fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str) -> Result>, DscError> { trace!("Listing resources with type_name_filter '{type_name_filter}' and adapter_name_filter '{adapter_name_filter}'"); let mut resources = BTreeMap::>::new(); @@ -374,7 +363,7 @@ impl ResourceDiscovery for CommandDiscovery { resources.append(&mut self.adapters); } else { self.discover_resources("*")?; - self.discover_adapted_resources(type_name_filter, adapter_name_filter, progress_format)?; + self.discover_adapted_resources(type_name_filter, adapter_name_filter)?; // add/update found adapted resources to the lookup_table add_resources_to_lookup_table(&self.adapted_resources); @@ -387,7 +376,7 @@ impl ResourceDiscovery for CommandDiscovery { } // TODO: handle version requirements - fn find_resources(&mut self, required_resource_types: &[String], progress_format: ProgressFormat) -> Result, DscError> + fn find_resources(&mut self, required_resource_types: &[String]) -> Result, DscError> { debug!("Searching for resources: {:?}", required_resource_types); self.discover_resources("*")?; @@ -439,7 +428,7 @@ impl ResourceDiscovery for CommandDiscovery { } } - self.discover_adapted_resources("*", &adapter_name, progress_format)?; + self.discover_adapted_resources("*", &adapter_name)?; // add/update found adapted resources to the lookup_table add_resources_to_lookup_table(&self.adapted_resources); diff --git a/dsc_lib/src/discovery/discovery_trait.rs b/dsc_lib/src/discovery/discovery_trait.rs index 96911e759..a6eb226f2 100644 --- a/dsc_lib/src/discovery/discovery_trait.rs +++ b/dsc_lib/src/discovery/discovery_trait.rs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{dscresources::dscresource::DscResource, dscerror::DscError, util::ProgressFormat}; +use crate::{dscresources::dscresource::DscResource, dscerror::DscError}; use std::collections::BTreeMap; pub trait ResourceDiscovery { fn discover_resources(&mut self, filter: &str) -> Result<(), DscError>; - fn discover_adapted_resources(&mut self, name_filter: &str, adapter_filter: &str, progress_format: ProgressFormat) -> Result<(), DscError>; - fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str, progress_format: ProgressFormat) -> Result>, DscError>; - fn find_resources(&mut self, required_resource_types: &[String], progress_format: ProgressFormat) -> Result, DscError>; + fn discover_adapted_resources(&mut self, name_filter: &str, adapter_filter: &str) -> Result<(), DscError>; + fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str) -> Result>, DscError>; + fn find_resources(&mut self, required_resource_types: &[String]) -> Result, DscError>; } diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index c216d809c..177969991 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -5,7 +5,7 @@ mod command_discovery; mod discovery_trait; use crate::discovery::discovery_trait::ResourceDiscovery; -use crate::{dscresources::dscresource::DscResource, dscerror::DscError, util::ProgressFormat}; +use crate::{dscresources::dscresource::DscResource, dscerror::DscError, progress::ProgressFormat}; use std::collections::BTreeMap; use tracing::error; @@ -38,14 +38,14 @@ impl Discovery { /// A vector of `DscResource` instances. pub fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str, progress_format: ProgressFormat) -> Vec { let discovery_types: Vec> = vec![ - Box::new(command_discovery::CommandDiscovery::new()), + Box::new(command_discovery::CommandDiscovery::new(progress_format)), ]; let mut resources: Vec = Vec::new(); for mut discovery_type in discovery_types { - let discovered_resources = match discovery_type.list_available_resources(type_name_filter, adapter_name_filter, progress_format) { + let discovered_resources = match discovery_type.list_available_resources(type_name_filter, adapter_name_filter) { Ok(value) => value, Err(err) => { error!("{err}"); @@ -75,12 +75,12 @@ impl Discovery { /// * `required_resource_types` - The required resource types. pub fn find_resources(&mut self, required_resource_types: &[String], progress_format: ProgressFormat) { let discovery_types: Vec> = vec![ - Box::new(command_discovery::CommandDiscovery::new()), + Box::new(command_discovery::CommandDiscovery::new(progress_format)), ]; let mut remaining_required_resource_types = required_resource_types.to_owned(); for mut discovery_type in discovery_types { - let discovered_resources = match discovery_type.find_resources(&remaining_required_resource_types, progress_format) { + let discovered_resources = match discovery_type.find_resources(&remaining_required_resource_types) { Ok(value) => value, Err(err) => { error!("{err}"); diff --git a/dsc_lib/src/lib.rs b/dsc_lib/src/lib.rs index bb61d5578..ddc1b2892 100644 --- a/dsc_lib/src/lib.rs +++ b/dsc_lib/src/lib.rs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::util::ProgressFormat; +use crate::progress::ProgressFormat; + use configure::config_doc::ExecutionKind; use dscerror::DscError; use dscresources::{dscresource::{DscResource, Invoke}, invoke_result::{GetResult, SetResult, TestResult}}; @@ -13,6 +14,7 @@ pub mod dscerror; pub mod dscresources; pub mod functions; pub mod parser; +pub mod progress; pub mod util; i18n!("locales", fallback = "en-us"); diff --git a/dsc_lib/src/progress.rs b/dsc_lib/src/progress.rs new file mode 100644 index 000000000..41c4ae54d --- /dev/null +++ b/dsc_lib/src/progress.rs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; + +use clap::ValueEnum; +use indicatif::ProgressStyle; +use rust_i18n::t; +use serde::Serialize; +use serde_json::Value; +use tracing_indicatif::span_ext::IndicatifSpanExt; +use tracing::{trace, warn_span}; +use tracing::span::Span; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum ProgressFormat { + /// If interactive, use a progress bar. If not interactive, no progress is shown. + Default, + /// No progress is shown. + None, + /// Show progress as JSON. + Json, +} + +#[derive(Default, Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Failure { + pub message: String, + pub exit_code: i32, +} + +#[derive(Default, Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Progress { + /// The unique identifier for the operation. + pub id: String, + /// The activity being performed. + pub activity: Option, + /// The total number of items to process. + pub total_items: u64, + /// The number of items processed. + pub completed_items: u64, + /// The status of the operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// The number of seconds remaining in the operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub seconds_remaining: Option, + /// The name of the resource being operated on. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_name: Option, + /// The type of the resource being operated on. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_type: Option, + /// The result of the operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Failure information. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, +} + +impl Progress { + #[must_use] + pub fn new(total_items: u64) -> Progress { + Progress { + id: Uuid::new_v4().to_string(), + total_items, + ..Default::default() + } + } +} + +pub struct ProgressBar { + progress_value: Progress, + console_bar: Span, + format: ProgressFormat +} + +impl ProgressBar { + + /// Create a `ProgressBar` object to update progress + /// + /// # Arguments + /// + /// * `item_count` - Total number of steps to complete. Use '1' if unknown and increment when complete. + /// * `format` - The `ProgressFormat` to render. + /// + /// # Returns + /// + /// A `ProgressBar` oject to update progress + /// + /// # Errors + /// + /// Fails if progress style for console rendering can't be set. + /// + pub fn new(total_items: u64, format: ProgressFormat) -> Result { + let bar = warn_span!(""); + if format == ProgressFormat::Default { + bar.pb_set_style(&ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" + )?); + bar.pb_set_length(total_items); + let _guard = bar.enter(); + } + + Ok(ProgressBar { + progress_value: Progress::new(total_items), + console_bar: bar, + format + }) + } + + /// Increment the progress bar by the specified amount and write the progress + /// + /// # Arguments + /// + /// * `delta` - The amount to increment the progress bar by + /// + pub fn write_increment(&mut self, delta: u64) { + if self.format == ProgressFormat::None { + return; + } + + self.progress_value.completed_items += delta; + if self.format == ProgressFormat::Json { + self.write_json(); + } else { + self.console_bar.pb_inc(delta); + } + } + + /// Set the resource being operated on + /// + /// # Arguments + /// + /// * `name` - The name of the resource being operated on + /// * `resource_type` - The type of the resource being operated on + /// * `result` - The result of the operation + /// + pub fn set_resource(&mut self, name: &str, resource_type: &str) { + self.progress_value.resource_name = Some(name.to_string()); + self.progress_value.resource_type = Some(resource_type.to_string()); + self.progress_value.result = None; + self.progress_value.failure = None; + } + + /// Set the result of the operation. This will clear any error. + /// + /// # Arguments + /// + /// * `result` - The result of the operation + /// + pub fn set_result(&mut self, result: &Value) { + self.progress_value.failure = None; + self.progress_value.result = Some(result.clone()); + } + + /// Indicate that the operation failed. This will clear any result. + /// + pub fn set_failure(&mut self, failure: Option) { + self.progress_value.result = None; + self.progress_value.failure = failure; + } + + /// Set the status of the operation and write the progress + /// + /// # Arguments + /// + /// * `status` - The status of the operation + /// + pub fn write_activity(&mut self, activity: &str) { + match self.format { + ProgressFormat::Json => { + self.progress_value.activity = Some(activity.to_string()); + self.write_json(); + }, + ProgressFormat::Default => { + self.console_bar.pb_set_message(activity); + }, + ProgressFormat::None => {} + } + } + + /// Set the number of total items to complete + /// + /// # Arguments + /// + /// * `len` - The number of total items to complete + /// + pub fn set_total_items(&mut self, len: u64) { + match self.format { + ProgressFormat::Json => { + self.progress_value.total_items = len; + }, + ProgressFormat::Default => { + self.console_bar.pb_set_length(len); + }, + ProgressFormat::None => {} + } + } + + fn write_json(&mut self) { + if let Ok(json) = serde_json::to_string(&self.progress_value) { + eprintln!("{json}"); + } else { + trace!("{}", t!("progress.failedToSerialize", json = self.progress_value : {:?})); + } + } +} diff --git a/dsc_lib/src/util.rs b/dsc_lib/src/util.rs index e82504a41..671db87bb 100644 --- a/dsc_lib/src/util.rs +++ b/dsc_lib/src/util.rs @@ -2,10 +2,8 @@ // Licensed under the MIT License. use crate::dscerror::DscError; -use clap::ValueEnum; use rust_i18n::t; use serde_json::Value; -use serde::Serialize; use std::fs; use std::fs::File; use std::io::BufReader; @@ -13,16 +11,6 @@ use std::path::PathBuf; use std::path::Path; use std::env; use tracing::debug; -use tracing_indicatif::span_ext::IndicatifSpanExt; -use tracing::warn_span; -use tracing::span::Span; -use indicatif::ProgressStyle; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum ProgressFormat { - Default, - Json, -} pub struct DscSettingValue { pub setting: Value, @@ -38,92 +26,6 @@ impl Default for DscSettingValue { } } -/// Only `activity` and `percent_complete` fields are mandatory for Progress messages -#[derive(Default, Debug, Clone, Serialize)] -pub struct Progress { - pub activity: String, - pub percent_complete: u16, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub seconds_remaining: Option, -} - -pub struct ProgressBar { - progress_value: Progress, - ui_bar: Span, //IndicatifSpanExt - length: u64, - position: u64, - emit_json: bool -} - -impl ProgressBar { - pub fn new(emit_json: bool) -> ProgressBar { - ProgressBar { - progress_value: Progress::default(), - ui_bar: warn_span!(""), - length: 0, - position: 0, - emit_json - } - } - - #[allow(clippy::cast_possible_truncation)] - pub fn pb_inc(&mut self, delta: u64) { - self.ui_bar.pb_inc(delta); - self.position += delta; - if self.length > 0 { - self.progress_value.percent_complete = if self.position >= self.length {100} - else { ((self.position * 100) / self.length) as u16}; - - self.emit_json(); - } - } - - pub fn pb_set_style(&mut self, style: &ProgressStyle) { - self.ui_bar.pb_set_style(style); - } - - pub fn pb_set_message(&mut self, msg: &str) { - self.ui_bar.pb_set_message(msg); - self.progress_value.activity = msg.to_string(); - self.emit_json(); - } - - #[allow(clippy::cast_possible_truncation)] - pub fn pb_set_length(&mut self, len: u64) { - self.ui_bar.pb_set_length(len); - self.length = len; - if self.length > 0 { - self.progress_value.percent_complete = if self.position >= self.length {100} - else { ((self.position * 100) / self.length) as u16}; - } - } - - #[allow(clippy::cast_possible_truncation)] - pub fn pb_set_position(&mut self, pos: u64) { - self.ui_bar.pb_set_position(pos); - self.position = pos; - if self.length > 0 { - self.progress_value.percent_complete = if self.position >= self.length {100} - else { ((self.position * 100) / self.length) as u16}; - self.emit_json(); - } - } - - pub fn enter(&self) { - _ = self.ui_bar.enter(); - } - - fn emit_json(&self) { - if self.emit_json { - if let Ok(json) = serde_json::to_string(&self.progress_value) { - eprintln!("{json}"); - } - } - } -} - /// Return JSON string whether the input is JSON or YAML /// /// # Arguments diff --git a/tools/test_group_resource/Cargo.lock b/tools/test_group_resource/Cargo.lock index 04d20d2e2..a3a615ea7 100644 --- a/tools/test_group_resource/Cargo.lock +++ b/tools/test_group_resource/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "tree-sitter", "tree-sitter-dscexpression", "tree-sitter-rust", + "uuid", ] [[package]] @@ -1844,9 +1845,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +dependencies = [ + "getrandom 0.3.1", +] [[package]] name = "uuid-simd"