diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index a930cec4..62501207 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -56,7 +56,7 @@ Describe 'resource export tests' { $set_results.results.count | Should -BeGreaterThan 1 } - It 'Duplicate resource types in Configuration Export should result in error' { + It 'Duplicate resource types in Configuration Export should not result in error' { $yaml = @' $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json @@ -71,8 +71,7 @@ Describe 'resource export tests' { pid: 0 '@ $out = $yaml | dsc config export 2>&1 - $LASTEXITCODE | Should -Be 2 - $out | out-string | Should -BeLike '*specified multiple times*' + $LASTEXITCODE | Should -Be 0 } It 'Export can be called on individual resource with the use of --format as a subcommand' { diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index f32d78c4..e7ff8818 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -5,7 +5,8 @@ edition = "2021" [dependencies] base64 = "0.21" -derive_builder ="0.12" +derive_builder ="0.20" +indicatif = { version = "0.17" } jsonschema = "0.17" regex = "1.7" reqwest = { version = "0.11", features = ["blocking"] } @@ -16,7 +17,7 @@ serde_yaml = { version = "0.9.3" } thiserror = "1.0" chrono = "0.4.26" tracing = "0.1.37" -tree-sitter = "~0.20.10" +tree-sitter = "0.20" tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" } [dev-dependencies] diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index e4290eda..f717c2f8 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -12,8 +12,10 @@ use self::config_doc::{Configuration, DataType}; use self::depends_on::get_resource_invocation_order; use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult}; use self::contraints::{check_length, check_number_limits, check_allowed_values}; +use indicatif::{ProgressBar, ProgressStyle}; use serde_json::{Map, Value}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; +use std::time::Duration; use tracing::{debug, trace}; pub mod context; @@ -131,6 +133,15 @@ fn escape_property_values(properties: &Map) -> Result Result { + let pb = ProgressBar::new(len); + pb.enable_steady_tick(Duration::from_millis(120)); + pb.set_style(ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" + )?); + Ok(pb) +} + impl Configurator { /// Create a new `Configurator` instance. /// @@ -164,8 +175,11 @@ impl Configurator { pub fn invoke_get(&mut self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let config = self.validate_config()?; let mut result = ConfigurationGetResult::new(); - for resource in get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)? { - trace!("Get resource '{}' named: {}", resource.resource_type, resource.name); + let resources = get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)?; + let pb = get_progress_bar(resources.len() as u64)?; + for resource in resources { + pb.inc(1); + pb.set_message(format!("Get '{}'", resource.name)); let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -181,6 +195,7 @@ impl Configurator { result.results.push(resource_result); } + pb.finish_with_message("Get configuration completed"); Ok(result) } @@ -197,7 +212,11 @@ impl Configurator { pub fn invoke_set(&mut self, skip_test: bool, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let config = self.validate_config()?; let mut result = ConfigurationSetResult::new(); - for resource in get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)? { + let resources = get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)?; + let pb = get_progress_bar(resources.len() as u64)?; + for resource in resources { + pb.inc(1); + pb.set_message(format!("Set '{}'", resource.name)); let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -213,6 +232,7 @@ impl Configurator { result.results.push(resource_result); } + pb.finish_with_message("Set configuration completed"); Ok(result) } @@ -229,7 +249,11 @@ impl Configurator { pub fn invoke_test(&mut self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let config = self.validate_config()?; let mut result = ConfigurationTestResult::new(); - for resource in get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)? { + let resources = get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)?; + let pb = get_progress_bar(resources.len() as u64)?; + for resource in resources { + pb.inc(1); + pb.set_message(format!("Test '{}'", resource.name)); let properties = self.invoke_property_expressions(&resource.properties)?; let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -245,6 +269,7 @@ impl Configurator { result.results.push(resource_result); } + pb.finish_with_message("Test configuration completed"); Ok(result) } @@ -265,17 +290,13 @@ impl Configurator { pub fn invoke_export(&mut self, _error_action: ErrorAction, _progress_callback: impl Fn() + 'static) -> Result { let config = self.validate_config()?; - let duplicates = Self::find_duplicate_resource_types(&config); - if !duplicates.is_empty() - { - let duplicates_string = &duplicates.join(","); - return Err(DscError::Validation(format!("Resource(s) {duplicates_string} specified multiple times"))); - } - let mut result = ConfigurationExportResult::new(); let mut conf = config_doc::Configuration::new(); - for resource in get_resource_invocation_order(&config, &mut self.statement_parser, &self.context)? { + let pb = get_progress_bar(config.resources.len() as u64)?; + for resource in &config.resources { + pb.inc(1); + pb.set_message(format!("Export '{}'", resource.name)); let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; @@ -285,7 +306,7 @@ impl Configurator { } result.result = Some(conf); - + pb.finish_with_message("Export configuration completed"); Ok(result) } @@ -369,27 +390,6 @@ impl Configurator { Ok(()) } - 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.is_empty() { - 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() - } - fn validate_config(&mut self) -> Result { let config: Configuration = serde_json::from_str(self.config.as_str())?; diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 5b13f4a5..d4575c6f 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -6,12 +6,14 @@ use crate::dscresources::dscresource::{DscResource, ImplementedAs}; use crate::dscresources::resource_manifest::{ResourceManifest, import_manifest}; use crate::dscresources::command_resource::invoke_command; use crate::dscerror::DscError; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use std::collections::BTreeMap; use std::env; use std::ffi::OsStr; use std::fs::File; use std::io::BufReader; use std::path::Path; +use std::time::Duration; use tracing::{debug, error, warn}; pub struct CommandDiscovery { @@ -28,6 +30,26 @@ impl CommandDiscovery { { let return_all_resources = required_resource_types.len() == 1 && required_resource_types[0] == "*"; + let multi_progress_bar = MultiProgress::new(); + let pb = multi_progress_bar.add( + if return_all_resources { + let pb = ProgressBar::new(1); + pb.enable_steady_tick(Duration::from_millis(120)); + pb.set_style(ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise:.cyan}] {msg:.yellow}" + )?); + pb + } else { + let pb = ProgressBar::new(required_resource_types.len() as u64); + pb.enable_steady_tick(Duration::from_millis(120)); + pb.set_style(ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" + )?); + pb + } + ); + pb.set_message("Searching for resources"); + let mut resources: BTreeMap = BTreeMap::new(); let mut provider_resources: Vec = Vec::new(); let mut remaining_required_resource_types = required_resource_types.to_owned(); @@ -88,6 +110,7 @@ impl CommandDiscovery { { remaining_required_resource_types.retain(|x| *x != resource.type_name.to_lowercase()); debug!("Found {} in {}", &resource.type_name, path.display()); + pb.inc(1); resources.insert(resource.type_name.to_lowercase(), resource); if remaining_required_resource_types.is_empty() { @@ -105,6 +128,12 @@ impl CommandDiscovery { // now go through the provider resources and add them to the list of resources for provider in provider_resources { debug!("Enumerating resources for provider {}", provider); + let pb_adapter = multi_progress_bar.add(ProgressBar::new(1)); + pb_adapter.enable_steady_tick(Duration::from_millis(120)); + pb_adapter.set_style(ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise:.cyan}] {msg:.white}" + )?); + pb_adapter.set_message(format!("Enumerating resources for adapter {provider}")); let provider_resource = resources.get(&provider).unwrap(); let provider_type_name = provider_resource.type_name.clone(); let manifest = import_manifest(provider_resource.manifest.clone().unwrap())?; @@ -175,10 +204,12 @@ impl CommandDiscovery { } }; } + pb_adapter.finish_with_message(format!("Done with {provider}")); debug!("Provider {} listed {} matching resources", provider_type_name, provider_resources_count); } + pb.finish_with_message("Discovery complete"); Ok(resources) } } diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index 2ee14576..42616645 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -36,7 +36,7 @@ impl Discovery { let mut regex: Option> = None; let mut resources: Vec = Vec::new(); - if !type_name_filter.is_empty() + if !type_name_filter.is_empty() { let regex_str = convert_wildcard_to_regex(type_name_filter); let mut regex_builder = RegexBuilder::new(regex_str.as_str()); diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index 214be45f..ab97c16a 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -3,6 +3,7 @@ use std::str::Utf8Error; +use indicatif::style::TemplateError; use reqwest::StatusCode; use thiserror::Error; use tracing::error; @@ -76,6 +77,9 @@ pub enum DscError { #[error("Parser: {0}")] Parser(String), + #[error("Progress: {0}")] + Progress(#[from] TemplateError), + #[error("Resource not found: {0}")] ResourceNotFound(String), diff --git a/tree-sitter-dscexpression/Cargo.toml b/tree-sitter-dscexpression/Cargo.toml index 317c64b1..ceae15fb 100644 --- a/tree-sitter-dscexpression/Cargo.toml +++ b/tree-sitter-dscexpression/Cargo.toml @@ -20,7 +20,7 @@ include = [ path = "bindings/rust/lib.rs" [dependencies] -tree-sitter = "~0.20.10" +tree-sitter = "0.20" [build-dependencies] cc = "1.0"