diff --git a/build.ps1 b/build.ps1 index f186ed7fb..268e727fc 100644 --- a/build.ps1 +++ b/build.ps1 @@ -161,6 +161,8 @@ if (!$SkipBuild) { # make sure dependencies are built first so clippy runs correctly $windows_projects = @("pal", "registry", "reboot_pending", "wmi-adapter") + $macOS_projects = @("resources/brew") + # projects are in dependency order $projects = @( "tree-sitter-dscexpression", @@ -170,7 +172,6 @@ if (!$SkipBuild) { "osinfo", "powershell-adapter", "process", - "resources/brew", "runcommandonset", "tools/dsctest", "tools/test_group_resource", @@ -187,6 +188,10 @@ if (!$SkipBuild) { Get-ChildItem -Path $target -Recurse -Hidden | ForEach-Object { $_.Attributes = 'Normal' } } + if ($IsMacOS) { + $projects += $macOS_projects + } + $failed = $false foreach ($project in $projects) { ## Build format_json diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index ba40fcac0..94d59bb39 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -27,7 +27,7 @@ pub fn get(dsc: &DscManager, resource_type: &str, mut input: String, format: &Op if let Some(pr) = get_resource(dsc, requires) { resource = pr; } else { - error!("Adapter {} not found", requires); + error!("Adapter '{}' not found", requires); return; }; } @@ -112,7 +112,7 @@ pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: &Op if let Some(pr) = get_resource(dsc, requires) { resource = pr; } else { - error!("Adapter {} not found", requires); + error!("Adapter '{}' not found", requires); return; }; } @@ -149,7 +149,7 @@ pub fn test(dsc: &DscManager, resource_type: &str, mut input: String, format: &O if let Some(pr) = get_resource(dsc, requires) { resource = pr; } else { - error!("Adapter {} not found", requires); + error!("Adapter '{}' not found", requires); return; }; } @@ -186,7 +186,7 @@ pub fn delete(dsc: &DscManager, resource_type: &str, mut input: String) { if let Some(pr) = get_resource(dsc, requires) { resource = pr; } else { - error!("Adapter {} not found", requires); + error!("Adapter '{}' not found", requires); return; }; } diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 2d153c39a..df75c4039 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -337,7 +337,7 @@ pub fn validate_config(config: &str) -> Result<(), DscError> { resource_types.push(type_name.to_lowercase().to_string()); } - dsc.discover_resources(&resource_types); + dsc.find_resources(&resource_types); for resource_block in resources { let Some(type_name) = resource_block["type"].as_str() else { @@ -402,15 +402,15 @@ pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { list_resources(&mut dsc, resource_name, adapter_name, description, tags, format); }, ResourceSubCommand::Schema { resource , format } => { - dsc.discover_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_lowercase().to_string()]); resource_command::schema(&dsc, resource, format); }, ResourceSubCommand::Export { resource, format } => { - dsc.discover_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_lowercase().to_string()]); resource_command::export(&mut dsc, resource, format); }, ResourceSubCommand::Get { resource, input, path, all, format } => { - dsc.discover_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_lowercase().to_string()]); if *all { resource_command::get_all(&dsc, resource, format); } else { let parsed_input = get_input(input, stdin, path); @@ -418,17 +418,17 @@ pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { } }, ResourceSubCommand::Set { resource, input, path, format } => { - dsc.discover_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_lowercase().to_string()]); let parsed_input = get_input(input, stdin, path); resource_command::set(&dsc, resource, parsed_input, format); }, ResourceSubCommand::Test { resource, input, path, format } => { - dsc.discover_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_lowercase().to_string()]); let parsed_input = get_input(input, stdin, path); resource_command::test(&dsc, resource, parsed_input, format); }, ResourceSubCommand::Delete { resource, input, path } => { - dsc.discover_resources(&[resource.to_lowercase().to_string()]); + dsc.find_resources(&[resource.to_lowercase().to_string()]); let parsed_input = get_input(input, stdin, path); resource_command::delete(&dsc, resource, parsed_input); }, diff --git a/dsc/tests/dsc_tracing.tests.ps1 b/dsc/tests/dsc_tracing.tests.ps1 index 9ce01fe79..24ae2edb9 100644 --- a/dsc/tests/dsc_tracing.tests.ps1 +++ b/dsc/tests/dsc_tracing.tests.ps1 @@ -19,7 +19,7 @@ Describe 'tracing tests' { It 'trace level error does not emit other levels' { $logPath = "$TestDrive/dsc_trace.log" - $null = '{}' | dsc --trace-level error resource get -r 'DoesNotExist' 2> $logPath + $null = '{}' | dsc --trace-level error resource list 'DoesNotExist' 2> $logPath $log = Get-Content $logPath -Raw $log | Should -Not -BeLikeExactly "* WARNING *" $log | Should -Not -BeLikeExactly "* INFO *" @@ -29,18 +29,18 @@ Describe 'tracing tests' { It 'trace format plaintext does not emit ANSI' { $logPath = "$TestDrive/dsc_trace.log" - $null = '{}' | dsc --trace-format plaintext resource get -r 'DoesNotExist' 2> $logPath + $null = '{}' | dsc --trace-format plaintext resource list 'DoesNotExist' 2> $logPath $log = Get-Content $logPath -Raw $log | Should -Not -BeLikeExactly "*``[0m*" } It 'trace format json emits json' { $logPath = "$TestDrive/dsc_trace.log" - $null = '{}' | dsc --trace-format json resource get -r 'DoesNotExist' 2> $logPath + $null = '{}' | dsc --trace-format json resource list 'DoesNotExist' 2> $logPath foreach ($line in (Get-Content $logPath)) { $trace = $line | ConvertFrom-Json -Depth 10 $trace.timestamp | Should -Not -BeNullOrEmpty - $trace.level | Should -BeIn 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE' + $trace.level | Should -BeIn 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE' $trace.fields.message | Should -Not -BeNullOrEmpty } } @@ -55,12 +55,12 @@ Describe 'tracing tests' { param($level, $sourceExpected) $logPath = "$TestDrive/dsc_trace.log" - $null = '{}' | dsc -l $level resource get -r 'DoesNotExist' 2> $logPath + $null = '{}' | dsc -l $level resource list 'DoesNotExist' 2> $logPath $log = Get-Content $logPath -Raw if ($sourceExpected) { - $log | Should -BeLike "*dsc*: *" + $log | Should -BeLike "*dsc_lib*: *" } else { - $log | Should -Not -BeLike "*dsc*: *" + $log | Should -Not -BeLike "*dsc_lib*: *" } } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index c2132faf2..d4ee0d9c5 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -335,7 +335,7 @@ impl Configurator { resource_type: resource.resource_type.clone(), result: set_result, }; - result.results.push(resource_result); + result.results.push(resource_result); } else if dsc_resource.capabilities.contains(&Capability::Delete) { debug!("Resource implements delete and _exist is false"); let before_result = dsc_resource.get(&desired)?; @@ -343,10 +343,10 @@ impl Configurator { dsc_resource.delete(&desired)?; let end_datetime = chrono::Local::now(); let after_result = dsc_resource.get(&desired)?; - // convert get result to set result + // convert get result to set result let set_result = match before_result { GetResult::Resource(before_response) => { - let GetResult::Resource(after_result) = after_result else { + let GetResult::Resource(after_result) = after_result else { return Err(DscError::NotSupported("Group resources not supported for delete".to_string())) }; let before_value = serde_json::to_value(&before_response.actual_state)?; @@ -614,7 +614,7 @@ impl Configurator { let mut required_resources = config.resources.iter().map(|p| p.resource_type.to_lowercase()).collect::>(); required_resources.sort_unstable(); required_resources.dedup(); - self.discovery.discover_resources(&required_resources); + self.discovery.find_resources(&required_resources); Ok(config) } diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 5adf163a3..fb536db20 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -6,25 +6,32 @@ use crate::discovery::convert_wildcard_to_regex; 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::dscresources::command_resource::log_resource_traces; use crate::dscerror::DscError; use indicatif::ProgressStyle; use regex::RegexBuilder; +use semver::Version; use std::collections::{BTreeMap, HashSet}; use std::env; use std::ffi::OsStr; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; -use tracing::{debug, error, trace, warn, warn_span, Span}; +use tracing::{debug, info, trace, warn, warn_span}; use tracing_indicatif::span_ext::IndicatifSpanExt; pub struct CommandDiscovery { + // use BTreeMap so that the results are sorted by the typename, the Vec is sorted by version + resources: BTreeMap>, + adapters: BTreeMap>, + adapted_resources: BTreeMap>, } impl CommandDiscovery { pub fn new() -> CommandDiscovery { CommandDiscovery { + resources: BTreeMap::new(), + adapters: BTreeMap::new(), + adapted_resources: BTreeMap::new(), } } @@ -41,7 +48,7 @@ impl CommandDiscovery { trace!("DSC_RESOURCE_PATH not set, trying PATH"); match env::var_os("PATH") { Some(value) => { - debug!("Using PATH: {:?}", value.to_string_lossy()); + trace!("Using PATH: {:?}", value.to_string_lossy()); value }, None => { @@ -76,64 +83,73 @@ impl Default for CommandDiscovery { impl ResourceDiscovery for CommandDiscovery { - #[allow(clippy::too_many_lines)] - fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str) -> Result, DscError> { + fn discover_resources(&mut self, filter: &str) -> Result<(), DscError> { + info!("Discovering resources using filter: {filter}"); - debug!("Listing resources with type_name_filter/adapter_name_filter: {type_name_filter}/{adapter_name_filter}"); + let regex_str = convert_wildcard_to_regex(filter); + debug!("Using regex {regex_str} as filter for adapter name"); + let mut regex_builder = RegexBuilder::new(®ex_str); + regex_builder.case_insensitive(true); + let Ok(regex) = regex_builder.build() else { + return Err(DscError::Operation("Could not build Regex filter for adapter name".to_string())); + }; let pb_span = warn_span!(""); pb_span.pb_set_style(&ProgressStyle::with_template( - "{spinner:.green} [{elapsed_precise:.cyan}] {msg:.yellow}" + "{spinner:.green} [{elapsed_precise:.cyan}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg:.yellow}" )?); pb_span.pb_set_message("Searching for resources"); let _ = pb_span.enter(); - let mut resources: BTreeMap = BTreeMap::new(); - let mut adapter_resources: BTreeMap = BTreeMap::new(); - - let regex_str = convert_wildcard_to_regex(type_name_filter); - debug!("Using regex {regex_str} as filter for resource type"); - let mut regex_builder = RegexBuilder::new(®ex_str); - regex_builder.case_insensitive(true); - let Ok(type_regex) = regex_builder.build() else { - let err_str = "Could not build Regex filter for resource type"; - error!(err_str); - return Err(DscError::Operation(err_str.to_string())); - }; + let mut resources = BTreeMap::>::new(); + let mut adapters = BTreeMap::>::new(); if let Ok(paths) = CommandDiscovery::get_resource_paths() { for path in paths { - debug!("Searching in {:?}", path); + trace!("Searching in {:?}", path); if path.exists() && path.is_dir() { for entry in path.read_dir().unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_file() { - let file_name = path.file_name().unwrap().to_str().unwrap(); + let Some(os_file_name) = path.file_name() else { + // skip if not a file + continue; + }; + let Some(file_name) = os_file_name.to_str() else { + // skip if not a valid file name + continue; + }; let file_name_lowercase = file_name.to_lowercase(); if file_name_lowercase.ends_with(".dsc.resource.json") || - file_name_lowercase.ends_with(".dsc.resource.yaml") || - file_name_lowercase.ends_with(".dsc.resource.yml") { + file_name_lowercase.ends_with(".dsc.resource.yaml") || + file_name_lowercase.ends_with(".dsc.resource.yml") { + trace!("Found resource manifest: {path:?}"); let resource = match load_manifest(&path) { Ok(r) => r, Err(e) => { - // In case of "resource list" operation - print all failures to read manifests as warnings - warn!("{}", e); + // At this point we can't determine whether or not the bad manifest contains + // resource that is requested by resource/config operation + // if it is, then "ResouceNotFound" error will be issued later + // and here we just write as warning + warn!("{e}"); continue; }, }; - if let Some(ref manifest) = resource.manifest { - let manifest = import_manifest(manifest.clone())?; - if manifest.kind == Some(Kind::Adapter) { - adapter_resources.insert(resource.type_name.to_lowercase(),resource.clone()); + if regex.is_match(&resource.type_name) { + if let Some(ref manifest) = resource.manifest { + let manifest = import_manifest(manifest.clone())?; + if manifest.kind == Some(Kind::Adapter) { + trace!("Resource adapter {} found", resource.type_name); + insert_resource(&mut adapters, &resource, true); + } else { + trace!("Resource {} found", resource.type_name); + insert_resource(&mut resources, &resource, true); + } } } - - if adapter_name_filter.is_empty() && type_regex.is_match(&resource.type_name) { - resources.insert(resource.type_name.to_lowercase(), resource); - } } } } @@ -141,222 +157,249 @@ impl ResourceDiscovery for CommandDiscovery { } } debug!("Found {} matching non-adapter-based resources", resources.len()); + self.resources = resources; + self.adapters = adapters; + Ok(()) + } - if !adapter_name_filter.is_empty() { - let regex_str = convert_wildcard_to_regex(adapter_name_filter); - debug!("Using regex {regex_str} as filter for adapter name"); - let mut regex_builder = RegexBuilder::new(®ex_str); - regex_builder.case_insensitive(true); - let Ok(adapter_regex) = regex_builder.build() else { - let err_str = "Could not build Regex filter for adapter name"; - error!(err_str); - return Err(DscError::Operation(err_str.to_string())); - }; + 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("*")?; + } - // now go through the adapter resources and add them to the list of resources - for adapter in adapter_resources { - if adapter_regex.is_match(&adapter.1.type_name) { - debug!("Enumerating resources for adapter '{}'", adapter.1.type_name); - let pb_adapter_span = warn_span!(""); - 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.1.type_name).as_str()); - let _ = pb_adapter_span.enter(); - let adapter_resource = adapter.1; - let adapter_type_name = adapter_resource.type_name.clone(); - let manifest = if let Some(manifest) = adapter_resource.manifest { - if let Ok(manifest) = import_manifest(manifest) { - manifest - } else { - return Err(DscError::Operation(format!("Failed to import manifest for '{}'", adapter_resource.type_name.clone()))); - } - } else { - return Err(DscError::MissingManifest(adapter_resource.type_name.clone())); - }; - let mut adapter_resources_count = 0; - // invoke the list command - let list_command = manifest.adapter.unwrap().list; - let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter_resource.directory), None) - { - Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), - Err(e) => { - // In case of "resource list" operation - print failure from adapter as warning - warn!("Could not start {}: {}", list_command.executable, e); - continue; - }, - }; - log_resource_traces(&stderr); + if self.adapters.is_empty() { + return Ok(()); + } + + let regex_str = convert_wildcard_to_regex(adapter_filter); + debug!("Using regex {regex_str} as filter for adapter name"); + let mut regex_builder = RegexBuilder::new(®ex_str); + regex_builder.case_insensitive(true); + let Ok(regex) = regex_builder.build() else { + return Err(DscError::Operation("Could not build Regex filter for adapter name".to_string())); + }; + + let name_regex_str = convert_wildcard_to_regex(name_filter); + debug!("Using regex {name_regex_str} as filter for resource name"); + let mut name_regex_builder = RegexBuilder::new(&name_regex_str); + name_regex_builder.case_insensitive(true); + let Ok(name_regex) = name_regex_builder.build() else { + return Err(DscError::Operation("Could not build Regex filter for resource name".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("Searching for adapted resources"); + let _ = pb_span.enter(); + + let mut adapted_resources = BTreeMap::>::new(); - if exit_code != 0 { - // In case of "resource list" operation - print failure from adapter as warning - warn!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); + for (adapter_name, adapters) in &self.adapters { + for adapter in adapters { + if !regex.is_match(adapter_name) { + continue; + } + + info!("Enumerating resources for adapter '{}'", adapter_name); + let pb_adapter_span = warn_span!(""); + 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()); + let _ = pb_adapter_span.enter(); + let manifest = if let Some(manifest) = &adapter.manifest { + if let Ok(manifest) = import_manifest(manifest.clone()) { + manifest + } else { + return Err(DscError::Operation(format!("Failed to import manifest for '{}'", adapter_name.clone()))); } + } else { + return Err(DscError::MissingManifest(adapter_name.clone())); + }; - for line in stdout.lines() { - match serde_json::from_str::(line){ - Result::Ok(resource) => { - if resource.require_adapter.is_none() { - warn!("{}", DscError::MissingRequires(adapter.0.clone(), resource.type_name.clone()).to_string()); - continue; - } - if type_regex.is_match(&resource.type_name) { - resources.insert(resource.type_name.to_lowercase(), resource); - adapter_resources_count += 1; - } - }, - Result::Err(err) => { - warn!("Failed to parse resource: {line} -> {err}"); + let mut adapter_resources_count = 0; + // invoke the list command + let list_command = manifest.adapter.unwrap().list; + let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter.directory), None) + { + Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), + Err(e) => { + // In case of error, log and continue + warn!("{e}"); + continue; + }, + }; + + if exit_code != 0 { + // in case of failure, log and continue + warn!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); + continue; + } + + for line in stdout.lines() { + match serde_json::from_str::(line){ + Result::Ok(resource) => { + if resource.require_adapter.is_none() { + warn!("{}", DscError::MissingRequires(adapter_name.clone(), resource.type_name.clone()).to_string()); continue; } - }; - } - debug!("Adapter '{}' listed {} matching resources", adapter_type_name, adapter_resources_count); + if name_regex.is_match(&resource.type_name) { + // we allow duplicate versions since it can come from different adapters + // like PowerShell vs WindowsPowerShell + insert_resource(&mut adapted_resources, &resource, false); + adapter_resources_count += 1; + } + }, + Result::Err(err) => { + warn!("Failed to parse resource: {line} -> {err}"); + continue; + } + }; } + + debug!("Adapter '{}' listed {} resources", adapter_name, adapter_resources_count); } } + + self.adapted_resources = adapted_resources; + Ok(()) + } + + 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(); + + if adapter_name_filter.is_empty() { + self.discover_resources(type_name_filter)?; + resources.append(&mut self.resources); + resources.append(&mut self.adapters); + } else { + self.discover_resources("*")?; + self.discover_adapted_resources(type_name_filter, adapter_name_filter)?; + resources.append(&mut self.adapted_resources); + } + Ok(resources) } - #[allow(clippy::too_many_lines)] - fn discover_resources(&mut self, required_resource_types: &[String]) -> Result, DscError> + // TODO: handle version requirements + fn find_resources(&mut self, required_resource_types: &[String]) -> Result, DscError> { debug!("Searching for resources: {:?}", required_resource_types); + self.discover_resources("*")?; - 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("Searching for resources"); - let _ = pb_span.enter(); - - let mut resources: BTreeMap = BTreeMap::new(); - let mut adapter_resources: BTreeMap = BTreeMap::new(); + let mut found_resources = BTreeMap::::new(); let mut remaining_required_resource_types = required_resource_types.to_owned(); - if let Ok(paths) = CommandDiscovery::get_resource_paths() { - for path in paths { - debug!("Searching in {:?}", path); - if path.exists() && path.is_dir() { - for entry in path.read_dir().unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - if path.is_file() { - let file_name = path.file_name().unwrap().to_str().unwrap(); - let file_name_lowercase = file_name.to_lowercase(); - if file_name_lowercase.ends_with(".dsc.resource.json") || - file_name_lowercase.ends_with(".dsc.resource.yaml") || - file_name_lowercase.ends_with(".dsc.resource.yml") { - let resource = match load_manifest(&path) - { - Ok(r) => r, - Err(e) => { - /* In case of non-list resource/config operations: - At this point we can't determine whether or not the bad manifest contains resource that is requested by resource/config operation - if it is, then "ResouceNotFound" error will be issued later - and here we just record the error into debug stream.*/ - debug!("{}", e); - continue; - }, - }; + for (resource_name, resources) in &self.resources { + // TODO: handle version requirements + let Some(resource ) = resources.first() else { + // skip if no resources + continue; + }; - if let Some(ref manifest) = resource.manifest { - let manifest = import_manifest(manifest.clone())?; - if manifest.kind == Some(Kind::Adapter) { - adapter_resources.insert(resource.type_name.to_lowercase(), resource.clone()); - resources.insert(resource.type_name.to_lowercase(), resource.clone()); - } - } - if remaining_required_resource_types.contains(&resource.type_name.to_lowercase()) - { - remaining_required_resource_types.retain(|x| *x != resource.type_name.to_lowercase()); - debug!("Found {} in {}", &resource.type_name, path.display()); - Span::current().pb_inc(1); - resources.insert(resource.type_name.to_lowercase(), resource); - if remaining_required_resource_types.is_empty() - { - return Ok(resources); - } - } - } - } - } + if remaining_required_resource_types.contains(&resource_name.to_lowercase()) + { + // remove the resource from the list of required resources + remaining_required_resource_types.retain(|x| *x != resource_name.to_lowercase()); + found_resources.insert(resource_name.to_lowercase(), resource.clone()); + if remaining_required_resource_types.is_empty() + { + return Ok(found_resources); } } } - debug!("Found {} matching non-adapter-based resources", resources.len()); + debug!("Found {} matching non-adapter-based resources", found_resources.len()); + + // now go through the adapters + for (adapter_name, adapters) in self.adapters.clone() { + // TODO: handle version requirements + let Some(adapter) = adapters.first() else { + // skip if no adapters + continue; + }; - // now go through the adapter resources and add them to the list of resources - for adapter in adapter_resources { - debug!("Enumerating resources for adapter '{}'", adapter.1.type_name); - let pb_adapter_span = warn_span!(""); - 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.1.type_name).as_str()); - let _ = pb_adapter_span.enter(); - let adapter_resource = adapter.1; - let adapter_type_name = adapter_resource.type_name.clone(); - let manifest = if let Some(manifest) = adapter_resource.manifest { - if let Ok(manifest) = import_manifest(manifest) { - manifest - } else { - return Err(DscError::Operation(format!("Failed to import manifest for '{}'", adapter_resource.type_name.clone()))); + if remaining_required_resource_types.contains(&adapter_name.to_lowercase()) + { + // remove the adapter from the list of required resources + remaining_required_resource_types.retain(|x| *x != adapter_name.to_lowercase()); + found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); + if remaining_required_resource_types.is_empty() + { + return Ok(found_resources); } - } else { - return Err(DscError::MissingManifest(adapter_resource.type_name.clone())); + } + + self.discover_adapted_resources("*", &adapter_name)?; + + // now go through the adapter resources and add them to the list of resources + for (adapted_name, adapted_resource) in &self.adapted_resources { + let Some(adapted_resource) = adapted_resource.first() else { + // skip if no resources + continue; + }; + + if remaining_required_resource_types.contains(&adapted_name.to_lowercase()) + { + remaining_required_resource_types.retain(|x| *x != adapted_name.to_lowercase()); + found_resources.insert(adapted_name.to_lowercase(), adapted_resource.clone()); + + // also insert the adapter + found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); + if remaining_required_resource_types.is_empty() + { + return Ok(found_resources); + } + } + } + } + Ok(found_resources) + } +} + +// helper to insert a resource into a vector of resources in order of newest to oldest +fn insert_resource(resources: &mut BTreeMap>, resource: &DscResource, skip_duplicate_version: bool) { + if resources.contains_key(&resource.type_name) { + let Some(resource_versions) = resources.get_mut(&resource.type_name) else { + resources.insert(resource.type_name.clone(), vec![resource.clone()]); + return; + }; + // compare the resource versions and insert newest to oldest using semver + let mut insert_index = resource_versions.len(); + for (index, resource_instance) in resource_versions.iter().enumerate() { + let resource_instance_version = match Version::parse(&resource_instance.version) { + Ok(v) => v, + Err(err) => { + // write as info since PowerShell resources tend to have invalid semver + info!("Resource '{}' has invalid version: {err}", resource_instance.type_name); + continue; + }, }; - let mut adapter_resources_count = 0; - // invoke the list command - let list_command = manifest.adapter.unwrap().list; - let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter_resource.directory), None) - { - Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), - Err(e) => { - /* In case of non-list resource/config operations: - print failure from adapter as error because this adapter was specifically requested by current resource/config operation*/ - error!("Could not start {}: {}", list_command.executable, e); + let resource_version = match Version::parse(&resource.version) { + Ok(v) => v, + Err(err) => { + // write as info since PowerShell resources tend to have invalid semver + info!("Resource '{}' has invalid version: {err}", resource.type_name); continue; }, }; - log_resource_traces(&stderr); - - if exit_code != 0 { - /* In case of non-list resource/config operations: - print failure from adapter as error because this adapter was specifically requested by current resource/config operation*/ - error!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); + // if the version already exists, we might skip it + if !skip_duplicate_version && resource_instance_version == resource_version { + return; } - for line in stdout.lines() { - match serde_json::from_str::(line){ - Result::Ok(resource) => { - if resource.require_adapter.is_none() { - error!("{}", DscError::MissingRequires(adapter.0.clone(), resource.type_name.clone()).to_string()); - continue; - } - if remaining_required_resource_types.contains(&resource.type_name.to_lowercase()) - { - remaining_required_resource_types.retain(|x| *x != resource.type_name.to_lowercase()); - debug!("Found {} in {}", &resource.type_name, &resource.path); - resources.insert(resource.type_name.to_lowercase(), resource); - adapter_resources_count += 1; - if remaining_required_resource_types.is_empty() - { - return Ok(resources); - } - } - }, - Result::Err(err) => { - error!("Failed to parse resource: {line} -> {err}"); - continue; - } - }; + if resource_instance_version < resource_version { + insert_index = index; + break; } - - debug!("Adapter '{}' listed {} matching resources", adapter_type_name, adapter_resources_count); } - Ok(resources) + resource_versions.insert(insert_index, resource.clone()); + } else { + resources.insert(resource.type_name.clone(), vec![resource.clone()]); } } diff --git a/dsc_lib/src/discovery/discovery_trait.rs b/dsc_lib/src/discovery/discovery_trait.rs index 4050d3356..a6eb226f2 100644 --- a/dsc_lib/src/discovery/discovery_trait.rs +++ b/dsc_lib/src/discovery/discovery_trait.rs @@ -5,6 +5,8 @@ use crate::{dscresources::dscresource::DscResource, dscerror::DscError}; use std::collections::BTreeMap; pub trait ResourceDiscovery { - fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str) -> Result, DscError>; - fn discover_resources(&mut self, required_resource_types: &[String]) -> Result, DscError>; + fn discover_resources(&mut self, filter: &str) -> 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 9a960d7c0..4a45664a8 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -26,8 +26,16 @@ impl Discovery { }) } - /// List operation. - #[allow(clippy::missing_panics_doc)] // false positive in clippy; this function will never panic + /// List operation for getting available resources based on the filters. + /// + /// # Arguments + /// + /// * `type_name_filter` - The filter for the resource type name. + /// * `adapter_name_filter` - The filter for the adapter name. + /// + /// # Returns + /// + /// A vector of `DscResource` instances. pub fn list_available_resources(&mut self, type_name_filter: &str, adapter_name_filter: &str) -> Vec { let discovery_types: Vec> = vec![ Box::new(command_discovery::CommandDiscovery::new()), @@ -45,8 +53,10 @@ impl Discovery { } }; - for resource in discovered_resources { - resources.push(resource.1); + for (_resource_name, found_resources) in discovered_resources { + for resource in found_resources { + resources.push(resource.clone()); + } }; } @@ -58,22 +68,25 @@ impl Discovery { self.resources.get(type_name) } - pub fn discover_resources(&mut self, required_resource_types: &[String]) { - + /// Find resources based on the required resource types. + /// + /// # Arguments + /// + /// * `required_resource_types` - The required resource types. + pub fn find_resources(&mut self, required_resource_types: &[String]) { let discovery_types: Vec> = vec![ Box::new(command_discovery::CommandDiscovery::new()), ]; - let mut remaining_required_resource_types = required_resource_types.to_owned(); for mut discovery_type in discovery_types { - let discovered_resources = match discovery_type.discover_resources(&remaining_required_resource_types) { + let discovered_resources = match discovery_type.find_resources(&remaining_required_resource_types) { Ok(value) => value, Err(err) => { - error!("{err}"); - continue; - } - }; + error!("{err}"); + continue; + } + }; for resource in discovered_resources { self.resources.insert(resource.0.clone(), resource.1); diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index 5e5ae66fb..fd401eb77 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -17,6 +17,9 @@ pub enum DscError { #[error("Command: Resource '{0}' [Exit code {1}] {2}")] Command(String, i32, String), + #[error("Command: Executable '{0}' [Exit code {1}] {2}")] + CommandExit(String, i32, String), + #[error("CommandOperation: {0} for executable '{1}'")] CommandOperation(String, String), diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index c0718bc06..5f9a5b4d8 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -12,22 +12,22 @@ use tracing::{error, warn, info, debug, trace}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; -pub fn log_resource_traces(stderr: &str) +pub fn log_resource_traces(process_name: &str, stderr: &str) { if !stderr.is_empty() { for trace_line in stderr.lines() { if let Result::Ok(json_obj) = serde_json::from_str::(trace_line) { if let Some(msg) = json_obj.get("Error") { - error!("{}", msg.as_str().unwrap_or_default()); + error!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); } else if let Some(msg) = json_obj.get("Warning") { - warn!("{}", msg.as_str().unwrap_or_default()); + warn!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); } else if let Some(msg) = json_obj.get("Info") { - info!("{}", msg.as_str().unwrap_or_default()); + info!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); } else if let Some(msg) = json_obj.get("Debug") { - debug!("{}", msg.as_str().unwrap_or_default()); + debug!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); } else if let Some(msg) = json_obj.get("Trace") { - trace!("{}", msg.as_str().unwrap_or_default()); + trace!("Process {process_name}: {}", msg.as_str().unwrap_or_default()); }; }; } @@ -54,12 +54,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul } info!("Invoking get '{}' using '{}'", &resource.resource_type, &resource.get.executable); - let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } - + let (_exit_code, stdout, stderr) = invoke_command(&resource.get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; if resource.kind == Some(Kind::Resource) { debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &resource.get.executable); verify_json(resource, cwd, &stdout)?; @@ -131,10 +126,6 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te info!("Getting current state for set by invoking get {} using {}", &resource.resource_type, &resource.get.executable); let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } if resource.kind == Some(Kind::Resource) { debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &resource.get.executable); @@ -165,10 +156,6 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te info!("Invoking set '{}' using '{}'", &resource.resource_type, &set.executable); let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } match set.returns { Some(ReturnKind::State) => { @@ -260,10 +247,6 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re info!("Invoking test '{}' using '{}'", &resource.resource_type, &test.executable); let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } if resource.kind == Some(Kind::Resource) { debug!("Verifying output of test '{}' using '{}'", &resource.resource_type, &test.executable); @@ -377,11 +360,7 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re let command_input = get_command_input(&delete.input, filter)?; info!("Invoking delete '{}' using '{}'", &resource.resource_type, &delete.executable); - let (exit_code, _stdout, stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } + let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; Ok(()) } @@ -412,12 +391,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) -> let command_input = get_command_input(&validate.input, config)?; info!("Invoking validate '{}' using '{}'", &resource.resource_type, &validate.executable); - let (exit_code, stdout, stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } - + let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) } @@ -438,11 +412,7 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result { - let (exit_code, stdout, stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } + let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None)?; Ok(stdout) }, SchemaKind::Embedded(ref schema) => { @@ -499,11 +469,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> args = process_args(&export.args, ""); } - let (exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; - log_resource_traces(&stderr); - if exit_code != 0 { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); - } + let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?; let mut instances: Vec = Vec::new(); for line in stdout.lines() { @@ -594,7 +560,13 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option } if !stderr.is_empty() { trace!("STDERR returned: {}", &stderr); + log_resource_traces(executable, &stderr); } + + if exit_code != 0 { + return Err(DscError::Command(executable.to_string(), exit_code, stderr)); + } + Ok((exit_code, stdout, stderr)) } diff --git a/dsc_lib/src/lib.rs b/dsc_lib/src/lib.rs index d871952f4..743e6b1c4 100644 --- a/dsc_lib/src/lib.rs +++ b/dsc_lib/src/lib.rs @@ -43,8 +43,8 @@ impl DscManager { self.discovery.list_available_resources(type_name_filter, adapter_name_filter) } - pub fn discover_resources(&mut self, required_resource_types: &[String]) { - self.discovery.discover_resources(required_resource_types); + pub fn find_resources(&mut self, required_resource_types: &[String]) { + self.discovery.find_resources(required_resource_types); } /// Invoke the get operation on a resource. ///