Skip to content

Commit c6dc3cb

Browse files
committed
add execution of extension during discovery
1 parent 59aa068 commit c6dc3cb

File tree

13 files changed

+339
-36
lines changed

13 files changed

+339
-36
lines changed

dsc/src/subcommand.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ fn list_extensions(dsc: &mut DscManager, extension_name: Option<&String>, format
624624
write_table = true;
625625
}
626626
let mut include_separator = false;
627-
for manifest_resource in dsc.list_available(&DiscoveryKind::Extension, extension_name.unwrap_or(&String::from("*")), &String::new(), progress_format) {
627+
for manifest_resource in dsc.list_available(&DiscoveryKind::Extension, extension_name.unwrap_or(&String::from("*")), "", progress_format) {
628628
if let ManifestResource::Extension(extension) = manifest_resource {
629629
let mut capabilities = "-".to_string();
630630
let capability_types = [

dsc/tests/dsc_extension_discover.ps1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Discover extension tests' {
5+
BeforeAll {
6+
$oldPath = $env:PATH
7+
$separator = [System.IO.Path]::PathSeparator
8+
$env:PATH = "$PSScriptRoot$separator$oldPath"
9+
}
10+
11+
AfterAll {
12+
$env:PATH = $oldPath
13+
}
14+
15+
It 'Discover extensions' {
16+
$out = dsc extension list | ConvertFrom-Json
17+
$LASTEXITCODE | Should -Be 0
18+
$out.Count | Should -Be 1
19+
$out.type | Should -BeExactly 'Test/Discover'
20+
$out.version | Should -BeExactly '0.1.0'
21+
$out.capabilities | Should -BeExactly @('discover')
22+
$out.manifest | Should -Not -BeNullOrEmpty
23+
}
24+
}

dsc_lib/locales/en-us.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,17 @@ usingResourcePath = "Using Resource Path: %{path}"
8181
discoverResources = "Discovering resources using filter: %{filter}"
8282
invalidAdapterFilter = "Could not build Regex filter for adapter name"
8383
progressSearching = "Searching for resources"
84+
extensionSearching = "Searching for extensions"
8485
foundResourceManifest = "Found resource manifest: %{path}"
8586
extensionFound = "Extension '%{extension}' found"
8687
adapterFound = "Resource adapter '%{adapter}' found"
8788
resourceFound = "Resource '%{resource}' found"
8889
executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'"
8990
extensionInvalidVersion = "Extension '%{extension}' version '%{version}' is invalid"
91+
invalidManifest = "Invalid manifest for resource '%{resource}'"
92+
extensionResourceFound = "Extension found resource '%{resource}'"
93+
callingExtension = "Calling extension '%{extension}' to discover resources"
94+
extensionFoundResources = "Extension '%{extension}' found %{count} resources"
9095

9196
[dscresources.commandResource]
9297
invokeGet = "Invoking get for '%{resource}'"
@@ -165,6 +170,9 @@ diffMissingItem = "diff: actual array missing expected item"
165170
resourceManifestSchemaTitle = "Resource manifest schema URI"
166171
resourceManifestSchemaDescription = "Defines the JSON Schema the resource manifest adheres to."
167172

173+
[extensions.dscextension]
174+
discoverNoResults = "No results returned for discovery extension '%{extension}'"
175+
168176
[extensions.extension_manifest]
169177
extensionManifestSchemaTitle = "Extension manifest schema URI"
170178
extensionManifestSchemaDescription = "Defines the JSON Schema the extension manifest adheres to."
@@ -339,6 +347,8 @@ unknown = "Unknown"
339347
unrecognizedSchemaUri = "Unrecognized $schema URI"
340348
validation = "Validation"
341349
validSchemaUrisAre = "Valid schema URIs are"
350+
extension = "Extension"
351+
unsupportedCapability = "does not support capability"
342352
setting = "Setting"
343353

344354
[progress]

dsc_lib/src/discovery/command_discovery.rs

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs};
77
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest, SchemaKind};
88
use crate::dscresources::command_resource::invoke_command;
99
use crate::dscerror::DscError;
10-
use crate::extensions::dscextension::{self, DscExtension};
10+
use crate::extensions::dscextension::{self, DscExtension, Capability as ExtensionCapability};
1111
use crate::extensions::extension_manifest::ExtensionManifest;
1212
use crate::progress::{ProgressBar, ProgressFormat};
1313
use linked_hash_map::LinkedHashMap;
@@ -68,6 +68,7 @@ impl Default for ResourcePathSetting {
6868
}
6969

7070
impl CommandDiscovery {
71+
#[must_use]
7172
pub fn new(progress_format: ProgressFormat) -> CommandDiscovery {
7273
CommandDiscovery {
7374
adapters: BTreeMap::new(),
@@ -184,6 +185,11 @@ impl ResourceDiscovery for CommandDiscovery {
184185
fn discover(&mut self, kind: &DiscoveryKind, filter: &str) -> Result<(), DscError> {
185186
info!("{}", t!("discovery.commandDiscovery.discoverResources", filter = filter));
186187

188+
// if kind is DscResource, we need to discover extensions first
189+
if *kind == DiscoveryKind::Resource {
190+
self.discover(&DiscoveryKind::Extension, "*")?;
191+
}
192+
187193
let regex_str = convert_wildcard_to_regex(filter);
188194
debug!("Using regex {regex_str} as filter for adapter name");
189195
let mut regex_builder = RegexBuilder::new(&regex_str);
@@ -193,7 +199,14 @@ impl ResourceDiscovery for CommandDiscovery {
193199
};
194200

195201
let mut progress = ProgressBar::new(1, self.progress_format)?;
196-
progress.write_activity(t!("discovery.commandDiscovery.progressSearching").to_string().as_str());
202+
match kind {
203+
DiscoveryKind::Resource => {
204+
progress.write_activity(t!("discovery.commandDiscovery.progressSearching").to_string().as_str());
205+
},
206+
DiscoveryKind::Extension => {
207+
progress.write_activity(t!("discovery.commandDiscovery.extensionSearching").to_string().as_str());
208+
}
209+
}
197210

198211
let mut adapters = BTreeMap::<String, Vec<DscResource>>::new();
199212
let mut resources = BTreeMap::<String, Vec<DscResource>>::new();
@@ -217,7 +230,7 @@ impl ResourceDiscovery for CommandDiscovery {
217230
let file_name_lowercase = file_name.to_lowercase();
218231
if (kind == &DiscoveryKind::Resource && DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext))) ||
219232
(kind == &DiscoveryKind::Extension && DSC_EXTENSION_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext))) {
220-
trace!("{}", t!("discovery.commandDiscovery.foundManifest", path = path.to_string_lossy()));
233+
trace!("{}", t!("discovery.commandDiscovery.foundResourceManifest", path = path.to_string_lossy()));
221234
let resource = match load_manifest(&path)
222235
{
223236
Ok(r) => r,
@@ -274,10 +287,31 @@ impl ResourceDiscovery for CommandDiscovery {
274287
}
275288

276289
progress.write_increment(1);
277-
debug!("Found {} matching non-adapter-based resources", resources.len());
278-
self.adapters = adapters;
279-
self.resources = resources;
280-
self.extensions = extensions;
290+
291+
match kind {
292+
DiscoveryKind::Resource => {
293+
// Now we need to call discover extensions and add those resource to the list of resources
294+
for (_extension_name, extension) in self.extensions.iter() {
295+
if extension.capabilities.contains(&ExtensionCapability::Discover) {
296+
debug!("{}", t!("discovery.commandDiscovery.callingExtension", extension = extension.type_name));
297+
let discovered_resources = extension.discover()?;
298+
debug!("{}", t!("discovery.commandDiscovery.extensionFoundResources", extension = extension.type_name, count = discovered_resources.len()));
299+
for resource in discovered_resources {
300+
if regex.is_match(&resource.type_name) {
301+
trace!("{}", t!("discovery.commandDiscovery.extensionResourceFound", resource = resource.type_name));
302+
insert_resource(&mut resources, &resource, true);
303+
}
304+
}
305+
}
306+
}
307+
self.adapters = adapters;
308+
self.resources = resources;
309+
},
310+
DiscoveryKind::Extension => {
311+
self.extensions = extensions;
312+
}
313+
}
314+
281315
Ok(())
282316
}
283317

@@ -505,7 +539,6 @@ impl ResourceDiscovery for CommandDiscovery {
505539
}
506540
}
507541

508-
// helper to insert a resource into a vector of resources in order of newest to oldest
509542
// TODO: This should be a BTreeMap of the resource name and a BTreeMap of the version and DscResource, this keeps it version sorted more efficiently
510543
fn insert_resource(resources: &mut BTreeMap<String, Vec<DscResource>>, resource: &DscResource, skip_duplicate_version: bool) {
511544
if resources.contains_key(&resource.type_name) {
@@ -548,36 +581,48 @@ fn insert_resource(resources: &mut BTreeMap<String, Vec<DscResource>>, resource:
548581
}
549582
}
550583

551-
fn load_manifest(path: &Path) -> Result<ManifestResource, DscError> {
584+
/// Loads a manifest from the given path and returns a `ManifestResource`.
585+
///
586+
/// # Arguments
587+
///
588+
/// * `path` - The path to the manifest file.
589+
///
590+
/// # Returns
591+
///
592+
/// * `ManifestResource` if the manifest was loaded successfully.
593+
///
594+
/// # Errors
595+
///
596+
/// * Returns a `DscError` if the manifest could not be loaded or parsed.
597+
pub fn load_manifest(path: &Path) -> Result<ManifestResource, DscError> {
552598
let contents = fs::read_to_string(path)?;
553599
if path.extension() == Some(OsStr::new("json")) {
554-
if let Ok(manifest) = serde_json::from_str::<ResourceManifest>(&contents) {
555-
let resource = load_resource_manifest(path, &manifest)?;
556-
return Ok(ManifestResource::Resource(resource));
600+
if let Ok(manifest) = serde_json::from_str::<ExtensionManifest>(&contents) {
601+
let extension = load_extension_manifest(path, &manifest)?;
602+
return Ok(ManifestResource::Extension(extension));
557603
}
558-
let manifest = match serde_json::from_str::<ExtensionManifest>(&contents) {
604+
let manifest = match serde_json::from_str::<ResourceManifest>(&contents) {
559605
Ok(manifest) => manifest,
560606
Err(err) => {
561-
return Err(DscError::Validation(format!("Invalid manifest {path:?} version value: {err}")));
607+
return Err(DscError::Manifest(t!("discovery.commandDiscovery.invalidManifest", resource = path.to_string_lossy()).to_string(), err));
562608
}
563609
};
564-
let extension = load_extension_manifest(path, &manifest)?;
565-
return Ok(ManifestResource::Extension(extension));
610+
let resource = load_resource_manifest(path, &manifest)?;
611+
return Ok(ManifestResource::Resource(resource));
566612
}
567-
else {
568-
if let Ok(manifest) = serde_yaml::from_str::<ResourceManifest>(&contents) {
569-
let resource = load_resource_manifest(path, &manifest)?;
570-
return Ok(ManifestResource::Resource(resource));
571-
}
572-
let manifest = match serde_yaml::from_str::<ExtensionManifest>(&contents) {
573-
Ok(manifest) => manifest,
574-
Err(err) => {
575-
return Err(DscError::Validation(format!("Invalid manifest {path:?} version value: {err}")));
576-
}
577-
};
578-
let extension = load_extension_manifest(path, &manifest)?;
579-
return Ok(ManifestResource::Extension(extension));
613+
614+
if let Ok(manifest) = serde_yaml::from_str::<ResourceManifest>(&contents) {
615+
let resource = load_resource_manifest(path, &manifest)?;
616+
return Ok(ManifestResource::Resource(resource));
580617
}
618+
let manifest = match serde_yaml::from_str::<ExtensionManifest>(&contents) {
619+
Ok(manifest) => manifest,
620+
Err(err) => {
621+
return Err(DscError::Validation(format!("Invalid manifest {path:?} version value: {err}")));
622+
}
623+
};
624+
let extension = load_extension_manifest(path, &manifest)?;
625+
Ok(ManifestResource::Extension(extension))
581626
}
582627

583628
fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result<DscResource, DscError> {
@@ -663,7 +708,7 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result<
663708
capabilities,
664709
path: path.to_str().unwrap().to_string(),
665710
directory: path.parent().unwrap().to_str().unwrap().to_string(),
666-
manifest: Some(serde_json::to_value(manifest)?),
711+
manifest: serde_json::to_value(manifest)?,
667712
..Default::default()
668713
};
669714

dsc_lib/src/discovery/discovery_trait.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,68 @@ pub enum DiscoveryKind {
1313
}
1414

1515
pub trait ResourceDiscovery {
16+
/// Discovery method to find resources.
17+
///
18+
/// # Arguments
19+
///
20+
/// * `kind` - The kind of discovery (e.g., Resource).
21+
/// * `filter` - The filter for the resource type name.
22+
///
23+
/// # Returns
24+
///
25+
/// A result indicating success or failure.
26+
///
27+
/// # Errors
28+
///
29+
/// This function will return an error if the underlying discovery fails.
1630
fn discover(&mut self, kind: &DiscoveryKind, filter: &str) -> Result<(), DscError>;
31+
32+
/// Discover adapted resources based on the provided filters.
33+
///
34+
/// # Arguments
35+
///
36+
/// * `name_filter` - The filter for the resource name.
37+
/// * `adapter_filter` - The filter for the adapter name.
38+
///
39+
/// # Returns
40+
///
41+
/// A result indicating success or failure.
42+
///
43+
/// # Errors
44+
///
45+
/// This function will return an error if the underlying discovery fails.
1746
fn discover_adapted_resources(&mut self, name_filter: &str, adapter_filter: &str) -> Result<(), DscError>;
47+
48+
/// List available resources based on the provided filters.
49+
///
50+
/// # Arguments
51+
///
52+
/// * `kind` - The kind of discovery (e.g., Resource).
53+
/// * `type_name_filter` - The filter for the resource type name.
54+
/// * `adapter_name_filter` - The filter for the adapter name (only applies to resources).
55+
///
56+
/// # Returns
57+
///
58+
/// A result containing a map of resource names to their corresponding `ManifestResource` instances.
59+
///
60+
/// # Errors
61+
///
62+
/// This function will return an error if the underlying discovery fails.
1863
fn list_available(&mut self, kind: &DiscoveryKind, type_name_filter: &str, adapter_name_filter: &str) -> Result<BTreeMap<String, Vec<ManifestResource>>, DscError>;
64+
65+
/// Find resources based on the required resource types.
66+
/// This is not applicable for extensions.
67+
///
68+
/// # Arguments
69+
///
70+
/// * `required_resource_types` - A slice of strings representing the required resource types.
71+
///
72+
/// # Returns
73+
///
74+
/// A result containing a map of resource names to their corresponding `DscResource` instances.
75+
///
76+
/// # Errors
77+
///
78+
/// This function will return an error if the underlying discovery fails.
1979
fn find_resources(&mut self, required_resource_types: &[String]) -> Result<BTreeMap<String, DscResource>, DscError>;
2080
}

dsc_lib/src/discovery/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::extensions::dscextension::DscExtension;
99
use crate::{dscresources::dscresource::DscResource, dscerror::DscError, progress::ProgressFormat};
1010
use std::collections::BTreeMap;
1111
use command_discovery::ManifestResource;
12-
use tracing::{debug, error};
12+
use tracing::error;
1313

1414
#[derive(Clone)]
1515
pub struct Discovery {

dsc_lib/src/dscerror.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ pub enum DscError {
116116
#[error("{t}: {0}. {t2}: {1:?}", t = t!("dscerror.unrecognizedSchemaUri"), t2 = t!("dscerror.validSchemaUrisAre"))]
117117
UnrecognizedSchemaUri(String, Vec<String>),
118118

119+
#[error("{t} '{0}' {t2} '{1}'", t = t!("dscerror.extension"), t2 = t!("dscerror.unsupportedCapability"))]
120+
UnsupportedCapability(String, String),
121+
119122
#[error("{t}: {0}", t = t!("dscerror.validation"))]
120123
Validation(String),
121124

dsc_lib/src/dscresources/command_resource.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,17 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
693693
.block_on(run_process_async(executable, args, input, cwd, env, exit_codes))
694694
}
695695

696-
fn process_args(args: Option<&Vec<ArgKind>>, value: &str) -> Option<Vec<String>> {
696+
/// Process the arguments for a command resource.
697+
///
698+
/// # Arguments
699+
///
700+
/// * `args` - The arguments to process
701+
/// * `value` - The value to use for JSON input arguments
702+
///
703+
/// # Returns
704+
///
705+
/// A vector of strings representing the processed arguments
706+
pub fn process_args(args: Option<&Vec<ArgKind>>, value: &str) -> Option<Vec<String>> {
697707
let Some(arg_values) = args else {
698708
debug!("{}", t!("dscresources.commandResource.noArgs"));
699709
return None;

0 commit comments

Comments
 (0)