From d75c108e670b245063ae293c2ea4236454e29008 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 24 Jun 2026 21:20:05 -0700 Subject: [PATCH 1/3] Add arg to pass file extensions to discover extensions --- dsc/tests/dsc_extension_discover.tests.ps1 | 59 ++++++++ extensions/appx/appx-discover.ps1 | 16 ++- extensions/appx/appx.dsc.extension.json | 8 +- extensions/powershell/powershell.discover.ps1 | 51 ++++--- .../powershell/powershell.dsc.extension.json | 6 +- extensions/test/discover/discover.ps1 | 51 +++++-- lib/dsc-lib/locales/en-us.toml | 2 +- .../src/discovery/command_discovery.rs | 11 +- lib/dsc-lib/src/extensions/discover.rs | 127 ++++++++++++++---- 9 files changed, 263 insertions(+), 68 deletions(-) diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index 8501e6d69..c8909e6b6 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -212,4 +212,63 @@ Describe 'Discover extension tests' { $env:TestDrive = $null } } + + It 'ExtensionsArg is received correctly' { + $extension_json = @' +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/DiscoverExtensions", + "version": "0.1.0", + "description": "Test discover resource", + "discover": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./discover.ps1", + { + "extensionsArg": "-Extensions", + "includeQuotes": true + } + ] + } +} +'@ + + $extensions = @{ + '.dsc.manifests.yml' = $false + '.dsc.manifests.yaml' = $false + '.dsc.manifests.json' = $false + '.dsc.extension.yml' = $false + '.dsc.extension.yaml' = $false + '.dsc.extension.json' = $false + '.dsc.adaptedresource.yml' = $false + '.dsc.adaptedresource.yaml' = $false + '.dsc.adaptedresource.json' = $false + '.dsc.resource.yml' = $false + '.dsc.resource.yaml' = $false + '.dsc.resource.json' = $false + } + + Set-Content -Path "$TestDrive/test.dsc.extension.json" -Value $extension_json + Copy-Item -Path "$toolPath/discover.ps1" -Destination $TestDrive | Out-Null + $env:DSC_RESOURCE_PATH = "$TestDrive" + [System.IO.Path]::PathSeparator + (Split-Path (Get-Command pwsh).Source -Parent) + try { + $out = dsc resource list 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + foreach ($resource in $out) { + if ($resource.type.startsWith('TestDiscover/')) { + $extensions[$resource.adaptedContent.extension] = $true + } + } + } finally { + $env:DSC_RESOURCE_PATH = $null + } + + foreach ($key in $extensions.Keys) { + $extensions[$key] | Should -BeTrue -Because "Extension $key was not found" + } + } } diff --git a/extensions/appx/appx-discover.ps1 b/extensions/appx/appx-discover.ps1 index f76d11ca7..966baa3a1 100644 --- a/extensions/appx/appx-discover.ps1 +++ b/extensions/appx/appx-discover.ps1 @@ -1,6 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[CmdletBinding()] +param( + [Parameter()] + [string]$extensions +) + +$fileextensions = [System.Collections.Generic.List[string]]::new() +foreach ($extension in $extensions.Split(',')) { + $fileextensions.Add('*' + $extension) +} + $packages = Get-AppxPackage foreach ($package in $packages) { - $manifests = Get-ChildItem -Path "$($package.InstallLocation)\*" -File -Include '*.dsc.resource.json','*.dsc.resource.yaml','*.dsc.resource.yml' -ErrorAction Ignore + $manifests = Get-ChildItem -Path "$($package.InstallLocation)\*" -File -Include $fileextensions -ErrorAction Ignore foreach ($manifest in $manifests) { @{ manifestPath = $manifest.FullName } | ConvertTo-Json -Compress } diff --git a/extensions/appx/appx.dsc.extension.json b/extensions/appx/appx.dsc.extension.json index d08294863..3f7a42af0 100644 --- a/extensions/appx/appx.dsc.extension.json +++ b/extensions/appx/appx.dsc.extension.json @@ -12,7 +12,11 @@ "Bypass", "-NoProfile", "-Command", - "./appx-discover.ps1" - ] + "./appx-discover.ps1", + { + "extensionsArg": "-extensions", + "includeQuotes": true + } + ] } } diff --git a/extensions/powershell/powershell.discover.ps1 b/extensions/powershell/powershell.discover.ps1 index df36f16c0..e501d6ec2 100644 --- a/extensions/powershell/powershell.discover.ps1 +++ b/extensions/powershell/powershell.discover.ps1 @@ -2,7 +2,10 @@ # Licensed under the MIT License. [CmdletBinding()] -param () +param ( + [Parameter()] + [string]$extensions +) function Get-CacheFilePath { if ($IsWindows) { @@ -14,33 +17,33 @@ function Get-CacheFilePath { function Test-CacheValid { param([string]$CacheFilePath, [string[]]$PSPaths) - + if (-not (Test-Path $CacheFilePath)) { return $false } - + try { $cache = Get-Content -Raw $CacheFilePath | ConvertFrom-Json - + foreach ($entry in $cache.PathInfo.PSObject.Properties) { $path = $entry.Name if (-not (Test-Path $path)) { return $false } - + $currentLastWrite = (Get-Item $path).LastWriteTimeUtc $cachedLastWrite = [DateTime]$entry.Value - + if ($currentLastWrite -ne $cachedLastWrite) { return $false } } - + $cachedPaths = [string[]]$cache.PSModulePaths if ($cachedPaths.Count -ne $PSPaths.Count) { return $false } - + $diff = Compare-Object $cachedPaths $PSPaths if ($null -ne $diff) { return $false @@ -60,11 +63,14 @@ function Test-CacheValid { function Invoke-DscResourceDiscovery { [CmdletBinding()] - param() - + param( + [Parameter()] + [string]$extensions + ) + begin { $psPaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator | Where-Object { $_ -notmatch 'WindowsPowerShell' } - + $cacheFilePath = Get-CacheFilePath $useCache = Test-CacheValid -CacheFilePath $cacheFilePath -PSPaths $psPaths } @@ -74,17 +80,10 @@ function Invoke-DscResourceDiscovery { $manifests = $cache.Manifests } else { $manifests = $psPaths | ForEach-Object -Parallel { - $searchPatterns = @( - '*.dsc.resource.json' - '*.dsc.resource.yaml' - '*.dsc.resource.yml' - '*.dsc.adaptedresource.json' - '*.dsc.adaptedresource.yaml' - '*.dsc.adaptedresource.yml' - '*.dsc.manifests.json' - '*.dsc.manifests.yaml' - '*.dsc.manifests.yml' - ) + $searchPatterns = [System.Collections.Generic.List[string]]::new() + foreach ($extension in $extensions.Split(',')) { + $searchPatterns.Add('*' + $extension) + } $enumOptions = [System.IO.EnumerationOptions]@{ IgnoreInaccessible = $true; RecurseSubdirectories = $true } foreach ($pattern in $searchPatterns) { try { @@ -94,7 +93,7 @@ function Invoke-DscResourceDiscovery { } catch { } } } -ThrottleLimit 10 - + $pathInfo = @{} foreach ($path in $psPaths) { $item = Get-Item -LiteralPath $path -ErrorAction Ignore @@ -107,13 +106,13 @@ function Invoke-DscResourceDiscovery { } } } - + $cacheObject = @{ PSModulePaths = $psPaths PathInfo = $pathInfo Manifests = $manifests } - + $cacheDir = Split-Path $cacheFilePath -Parent if (-not (Test-Path $cacheDir)) { New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null @@ -129,4 +128,4 @@ function Invoke-DscResourceDiscovery { } } -Invoke-DscResourceDiscovery +Invoke-DscResourceDiscovery -extensions $extensions diff --git a/extensions/powershell/powershell.dsc.extension.json b/extensions/powershell/powershell.dsc.extension.json index 16081a13a..9282c3a12 100644 --- a/extensions/powershell/powershell.dsc.extension.json +++ b/extensions/powershell/powershell.dsc.extension.json @@ -13,7 +13,11 @@ "Bypass", "-NoProfile", "-Command", - "./powershell.discover.ps1" + "./powershell.discover.ps1", + { + "extensionsArg": "-Extensions", + "includeQuotes": true + } ] } } diff --git a/extensions/test/discover/discover.ps1 b/extensions/test/discover/discover.ps1 index 97a66c6f0..8bf249154 100644 --- a/extensions/test/discover/discover.ps1 +++ b/extensions/test/discover/discover.ps1 @@ -4,16 +4,51 @@ [CmdletBinding()] param( [Parameter()] - [switch]$RelativePath + [switch]$RelativePath, + [Parameter()] + [string]$Extensions ) -Get-ChildItem -Path $PSScriptRoot/resources/*.json | ForEach-Object { - $resource = [pscustomobject]@{ - manifestPath = if ($RelativePath) { - Resolve-Path -Path $_.FullName -Relative - } else { - $_.FullName +if ($Extensions) { + $count = 1 + foreach ($extension in $Extensions.Split(',')) { + $resource = [pscustomobject]@{ + manifestContent = @{ + '$schema' = "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json" + type = "TestDiscover/$count" + kind = "resource" + version = "1.0.0" + capabilities = @("get") + description = "Test discover $count" + requireAdapter = "Test/Adapter" + content = @{ + extension = $extension + } + schema = @{ + embedded = @{ + '$schema' = "http://json-schema.org/draft-07/schema#" + type = "object" + properties = @{ + extension = @{ + type = "string" + } + } + } + } + } + } + $resource | ConvertTo-Json -Compress -Depth 10 + $count++ + } +} else { + Get-ChildItem -Path $PSScriptRoot/resources/*.json | ForEach-Object { + $resource = [pscustomobject]@{ + manifestPath = if ($RelativePath) { + Resolve-Path -Path $_.FullName -Relative + } else { + $_.FullName + } } + $resource | ConvertTo-Json -Compress } - $resource | ConvertTo-Json -Compress } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index f5f89b4e2..bfa3981ed 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -120,7 +120,7 @@ resourceFound = "Resource '%{resource}' version %{version} found" adaptedResourceFound = "Adapted resource '%{resource}' version %{version} found" executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'" invalidResourceManifest = "Invalid manifest for resource '%{resource}': %{err}" -invalidExtensionManifest = "Invalid manifest for extension '%{extension}': %{err}" +invalidExtensionManifest = "Invalid manifest for extension '%{resource}': %{err}" invalidAdaptedResourceManifest = "Invalid manifest for adapted resource '%{resource}': %{err}" invalidManifestList = "Invalid manifest list '%{resource}': %{err}" invalidManifestFile = "Invalid manifest file '%{resource}': %{err}" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 454c5752b..9567148ae 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -27,10 +27,11 @@ use tracing::{debug, info, trace, warn}; use crate::util::get_setting; use crate::util::{canonicalize_which, get_exe_path}; -const DSC_ADAPTED_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.adaptedresource.json", ".dsc.adaptedresource.yaml", ".dsc.adaptedresource.yml"]; -const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extension.yaml", ".dsc.extension.yml"]; -const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"]; -const DSC_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.resource.json", ".dsc.resource.yaml", ".dsc.resource.yml"]; +// NOTE: if new types of extensions are added, ensure they are added to `process_discover_args` in `lib/dsc-lib/src/extensions/discover.rs` +pub const DSC_ADAPTED_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.adaptedresource.json", ".dsc.adaptedresource.yaml", ".dsc.adaptedresource.yml"]; +pub const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extension.yaml", ".dsc.extension.yml"]; +pub const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"]; +pub const DSC_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.resource.json", ".dsc.resource.yaml", ".dsc.resource.yml"]; static ADAPTERS: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryResourceCache::new())); static RESOURCES: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryResourceCache::new())); @@ -775,7 +776,7 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidManifestFile", resource = path.to_string_lossy()).to_string())) } -fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceManifest) -> Result { +pub fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceManifest) -> Result { if manifest.version.is_date_version() { warn!("{}", t!( "discovery.commandDiscovery.invalidManifestVersion", diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 488703590..f6e68cad6 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -3,15 +3,11 @@ use crate::{ discovery::command_discovery::{ - load_manifest, ImportedManifest + DSC_ADAPTED_RESOURCE_EXTENSIONS, DSC_EXTENSION_EXTENSIONS, DSC_MANIFEST_LIST_EXTENSIONS, DSC_RESOURCE_EXTENSIONS, ImportedManifest, load_adapted_resource_manifest, load_manifest }, dscerror::DscError, dscresources::{ - command_resource::{ - invoke_command, process_get_args, - }, - dscresource::DscResource, - resource_manifest::GetArgKind, + adapted_resource_manifest, command_resource::invoke_command, dscresource::DscResource }, extensions::{ dscextension::{ @@ -25,6 +21,7 @@ use crate::{ use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::path::PathBuf; use tracing::{info, trace, warn}; @@ -34,15 +31,36 @@ pub struct DiscoverMethod { /// The command to run to get the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Get. - pub args: Option>, + pub args: Option>, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum ManifestKind { + /// The path to the resource manifest, must be absolute. + ManifestPath(PathBuf), + /// The content of the resource manifest. + ManifestContent(Value), } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[dsc_repo_schema(base_name = "discover", folder_path = "extension/stdout")] pub struct DiscoverResult { - /// The path to the resource manifest, must be absolute. - #[serde(rename = "manifestPath")] - pub manifest_path: PathBuf, + #[serde(flatten)] + pub path_or_content: ManifestKind, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] +pub enum DiscoverArgKind { + String(String), + Extensions { + /// The argument that accepts the file path. + #[serde(rename = "extensionsArg")] + extensions_arg: String, + #[serde(rename = "includeQuotes", default)] + include_quotes: bool, + }, } impl DscExtension { @@ -71,7 +89,7 @@ impl DscExtension { let mut extension_resource = DscResource::new(); extension_resource.type_name = self.type_name.clone(); extension_resource.path = self.path.clone(); - let args = process_get_args(discover.args.as_ref(), "", &extension_resource); + let args = process_discover_args(discover.args.as_ref())?; if let Some(deprecation_message) = extension.deprecation_message.as_ref() { warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); } @@ -94,20 +112,50 @@ impl DscExtension { return Err(DscError::Json(err)); } }; - if !discover_result.manifest_path.is_absolute() { - return Err(DscError::Extension(t!("extensions.dscextension.discoverNotAbsolutePath", extension = self.type_name.clone(), path = discover_result.manifest_path.display()).to_string())); - } - // Currently we don't support extensions discovering other extensions - let manifests = match load_manifest(&discover_result.manifest_path) { - Ok(manifests) => manifests, - Err(err) => { - info!("{}", t!("extensions.dscextension.failedLoadManifest", extension = self.type_name, err = err)); - continue; + match discover_result.path_or_content { + ManifestKind::ManifestContent(manifest_value) => { + let imported_manifest = match serde_json::from_value::(manifest_value.clone()) { + Ok(manifest) => manifest, + Err(err) => { + // see if adapted resource manifest + let adapted_resource_manifest = match serde_json::from_value::(manifest_value.clone()) { + Ok(manifest) => manifest, + Err(err) => { + return Err(DscError::Json(err)); + } + }; + load_adapted_resource_manifest(self.path.as_path(), &adapted_resource_manifest).map_or_else( + |_| { + Err(DscError::Json(err)) + }, + |resource| { + Ok(ImportedManifest::Resource(resource)) + } + )? + } + }; + info!("Manifest imported from extension {}", self.type_name); + if let ImportedManifest::Resource(resource) = imported_manifest { + resources.push(resource); + } } - }; - for imported_manifest in manifests { - if let ImportedManifest::Resource(resource) = imported_manifest { - resources.push(resource); + ManifestKind::ManifestPath(manifest_path) => { + if !manifest_path.is_absolute() { + return Err(DscError::Extension(t!("extensions.dscextension.discoverNotAbsolutePath", extension = self.type_name.clone(), path = manifest_path.display()).to_string())); + } + // Currently we don't support extensions discovering other extensions + let manifests = match load_manifest(&manifest_path) { + Ok(manifests) => manifests, + Err(err) => { + info!("{}", t!("extensions.dscextension.failedLoadManifest", extension = self.type_name, err = err)); + continue; + } + }; + for imported_manifest in manifests { + if let ImportedManifest::Resource(resource) = imported_manifest { + resources.push(resource); + } + } } } } @@ -122,3 +170,34 @@ impl DscExtension { } } } + +fn process_discover_args(args: Option<&Vec>) -> Result>, DscError> { + let Some(arg_values) = args else { + return Ok(None); + }; + + let mut processed_args = Vec::::new(); + for arg in arg_values { + match arg { + DiscoverArgKind::String(s) => { + processed_args.push(s.clone()); + } + DiscoverArgKind::Extensions { extensions_arg, include_quotes } => { + processed_args.push(extensions_arg.clone()); + let mut extensions = Vec::::new(); + extensions.extend(DSC_ADAPTED_RESOURCE_EXTENSIONS.iter().map(|s| s.to_string())); + extensions.extend(DSC_EXTENSION_EXTENSIONS.iter().map(|s| s.to_string())); + extensions.extend(DSC_MANIFEST_LIST_EXTENSIONS.iter().map(|s| s.to_string())); + extensions.extend(DSC_RESOURCE_EXTENSIONS.iter().map(|s| s.to_string())); + let extensions_arg_value = extensions.join(","); + if *include_quotes { + processed_args.push(format!("\"{}\"", extensions_arg_value)); + } else { + processed_args.push(extensions_arg_value); + } + } + } + } + + Ok(Some(processed_args)) +} From 703059b95626c00fc5d9ee5c0889f3657d4f810c Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 25 Jun 2026 09:12:00 -0700 Subject: [PATCH 2/3] address copilot feedback --- extensions/powershell/powershell.discover.ps1 | 2 +- extensions/powershell/powershell.dsc.extension.json | 2 +- lib/dsc-lib/src/discovery/command_discovery.rs | 2 +- lib/dsc-lib/src/extensions/discover.rs | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/extensions/powershell/powershell.discover.ps1 b/extensions/powershell/powershell.discover.ps1 index e501d6ec2..70eb39626 100644 --- a/extensions/powershell/powershell.discover.ps1 +++ b/extensions/powershell/powershell.discover.ps1 @@ -81,7 +81,7 @@ function Invoke-DscResourceDiscovery { } else { $manifests = $psPaths | ForEach-Object -Parallel { $searchPatterns = [System.Collections.Generic.List[string]]::new() - foreach ($extension in $extensions.Split(',')) { + foreach ($extension in ($using:extensions).Split(',')) { $searchPatterns.Add('*' + $extension) } $enumOptions = [System.IO.EnumerationOptions]@{ IgnoreInaccessible = $true; RecurseSubdirectories = $true } diff --git a/extensions/powershell/powershell.dsc.extension.json b/extensions/powershell/powershell.dsc.extension.json index 9282c3a12..edb241eff 100644 --- a/extensions/powershell/powershell.dsc.extension.json +++ b/extensions/powershell/powershell.dsc.extension.json @@ -15,7 +15,7 @@ "-Command", "./powershell.discover.ps1", { - "extensionsArg": "-Extensions", + "extensionsArg": "-extensions", "includeQuotes": true } ] diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 9567148ae..53fbc0a01 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -27,7 +27,7 @@ use tracing::{debug, info, trace, warn}; use crate::util::get_setting; use crate::util::{canonicalize_which, get_exe_path}; -// NOTE: if new types of extensions are added, ensure they are added to `process_discover_args` in `lib/dsc-lib/src/extensions/discover.rs` +// NOTE: if new types of file extensions are added, ensure they are added to `process_discover_args` in `lib/dsc-lib/src/extensions/discover.rs` pub const DSC_ADAPTED_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.adaptedresource.json", ".dsc.adaptedresource.yaml", ".dsc.adaptedresource.yml"]; pub const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extension.yaml", ".dsc.extension.yml"]; pub const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"]; diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index f6e68cad6..0058fd032 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -55,9 +55,10 @@ pub struct DiscoverResult { pub enum DiscoverArgKind { String(String), Extensions { - /// The argument that accepts the file path. + /// The argument that accepts the extensions list. The extensions list will be passed as a comma separated list of extensions. #[serde(rename = "extensionsArg")] extensions_arg: String, + /// Whether to include quotes around the extensions list. If true, the extensions list will be passed as a quoted string. #[serde(rename = "includeQuotes", default)] include_quotes: bool, }, @@ -86,9 +87,6 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let mut extension_resource = DscResource::new(); - extension_resource.type_name = self.type_name.clone(); - extension_resource.path = self.path.clone(); let args = process_discover_args(discover.args.as_ref())?; if let Some(deprecation_message) = extension.deprecation_message.as_ref() { warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); From 896cede97bfaced7cfbbab2864c5d59757c5eeb0 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 25 Jun 2026 15:52:17 -0700 Subject: [PATCH 3/3] address copilot concern --- lib/dsc-lib/locales/en-us.toml | 1 + .../src/discovery/command_discovery.rs | 72 +++++++++++++++++++ lib/dsc-lib/src/extensions/discover.rs | 32 +++------ 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index bfa3981ed..4aee5acd9 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -124,6 +124,7 @@ invalidExtensionManifest = "Invalid manifest for extension '%{resource}': %{err} invalidAdaptedResourceManifest = "Invalid manifest for adapted resource '%{resource}': %{err}" invalidManifestList = "Invalid manifest list '%{resource}': %{err}" invalidManifestFile = "Invalid manifest file '%{resource}': %{err}" +invalidManifestContent = "Manifest content did not match any known manifest types" extensionResourceFound = "Extension found resource '%{resource}'" callingExtension = "Calling extension '%{extension}' to discover resources" extensionFoundResources = "Extension '%{extension}' found %{count} resources" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 53fbc0a01..2fb07fd01 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -16,6 +16,7 @@ use crate::schemas::transforms::idiomaticize_externally_tagged_enum; use rust_i18n::t; use schemars::JsonSchema; use serde::Deserialize; +use serde_json::Value; use std::{collections::{HashMap, HashSet}, sync::{LazyLock, RwLock}}; use std::env; use std::ffi::OsStr; @@ -636,6 +637,77 @@ fn evaluate_condition(condition: Option<&str>) -> Result { Ok(true) } +/// Loads a manifest from the given content and returns a vector of `ImportedManifest`. +/// +/// # Arguments +/// +/// * `content` - The content of the manifest file as a string. +/// +/// # Returns +/// +/// * `Vec` if the manifest was loaded successfully. +/// +/// # Errors +/// * Returns a `DscError` if the manifest could not be loaded or parsed. +pub fn load_manifest_content(content: &Value) -> Result, DscError> { + if let Ok(resource) = serde_json::from_value::(content.clone()) { + if !evaluate_condition(resource.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = "manifest content", condition = resource.condition.unwrap_or_default(), resource = resource.type_name)); + return Ok(vec![]); + } + let resource = load_adapted_resource_manifest(Path::new("manifest content"), &resource)?; + return Ok(vec![ImportedManifest::Resource(resource)]); + } else if let Ok(resource) = serde_json::from_value::(content.clone()) { + if !evaluate_condition(resource.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = "manifest content", condition = resource.condition.unwrap_or_default(), resource = resource.resource_type)); + return Ok(vec![]); + } + let resource = load_resource_manifest(Path::new("manifest content"), &resource)?; + return Ok(vec![ImportedManifest::Resource(resource)]); + } else if let Ok(extension) = serde_json::from_value::(content.clone()) { + if !evaluate_condition(extension.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = "manifest content", condition = extension.condition.unwrap_or_default(), resource = extension.r#type)); + return Ok(vec![]); + } + let extension = load_extension_manifest(Path::new("manifest content"), &extension)?; + return Ok(vec![ImportedManifest::Extension(extension)]); + } else if let Ok(manifest_list) = serde_json::from_value::(content.clone()) { + let mut resources: Vec = vec![]; + if let Some(adapted_resources) = manifest_list.adapted_resources { + for adapted_resource in adapted_resources { + if !evaluate_condition(adapted_resource.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = "manifest content", condition = adapted_resource.condition.unwrap_or_default(), resource = adapted_resource.type_name)); + continue; + } + let resource = load_adapted_resource_manifest(Path::new("manifest content"), &adapted_resource)?; + resources.push(ImportedManifest::Resource(resource)); + } + } + if let Some(resource_manifests) = manifest_list.resources { + for resource_manifest in resource_manifests { + if !evaluate_condition(resource_manifest.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = "manifest content", condition = resource_manifest.condition.unwrap_or_default(), resource = resource_manifest.resource_type)); + continue; + } + let resource = load_resource_manifest(Path::new("manifest content"), &resource_manifest)?; + resources.push(ImportedManifest::Resource(resource)); + } + } + if let Some(extension_manifests) = manifest_list.extensions { + for extension_manifest in extension_manifests { + if !evaluate_condition(extension_manifest.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = "manifest content", condition = extension_manifest.condition.unwrap_or_default(), resource = extension_manifest.r#type)); + continue; + } + let extension = load_extension_manifest(Path::new("manifest content"), &extension_manifest)?; + resources.push(ImportedManifest::Extension(extension)); + } + } + return Ok(resources); + } + Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidManifestContent").to_string())) +} + /// Loads a manifest from the given path and returns a vector of `ImportedManifest`. /// /// # Arguments diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 0058fd032..50adfd376 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -3,11 +3,12 @@ use crate::{ discovery::command_discovery::{ - DSC_ADAPTED_RESOURCE_EXTENSIONS, DSC_EXTENSION_EXTENSIONS, DSC_MANIFEST_LIST_EXTENSIONS, DSC_RESOURCE_EXTENSIONS, ImportedManifest, load_adapted_resource_manifest, load_manifest + DSC_ADAPTED_RESOURCE_EXTENSIONS, DSC_EXTENSION_EXTENSIONS, DSC_MANIFEST_LIST_EXTENSIONS, DSC_RESOURCE_EXTENSIONS, ImportedManifest, load_manifest, load_manifest_content }, dscerror::DscError, dscresources::{ - adapted_resource_manifest, command_resource::invoke_command, dscresource::DscResource + command_resource::invoke_command, + dscresource::DscResource }, extensions::{ dscextension::{ @@ -112,29 +113,12 @@ impl DscExtension { }; match discover_result.path_or_content { ManifestKind::ManifestContent(manifest_value) => { - let imported_manifest = match serde_json::from_value::(manifest_value.clone()) { - Ok(manifest) => manifest, - Err(err) => { - // see if adapted resource manifest - let adapted_resource_manifest = match serde_json::from_value::(manifest_value.clone()) { - Ok(manifest) => manifest, - Err(err) => { - return Err(DscError::Json(err)); - } - }; - load_adapted_resource_manifest(self.path.as_path(), &adapted_resource_manifest).map_or_else( - |_| { - Err(DscError::Json(err)) - }, - |resource| { - Ok(ImportedManifest::Resource(resource)) - } - )? - } - }; + let imported_manifests = load_manifest_content(&manifest_value)?; info!("Manifest imported from extension {}", self.type_name); - if let ImportedManifest::Resource(resource) = imported_manifest { - resources.push(resource); + for imported_manifest in imported_manifests { + if let ImportedManifest::Resource(resource) = imported_manifest { + resources.push(resource); + } } } ManifestKind::ManifestPath(manifest_path) => {