From 5492deb35998e9916f16a616f2a06f6e258c1c03 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Feb 2026 15:46:35 -0800 Subject: [PATCH 01/11] Enable support for adapted resource manifests --- build.data.json | 1 + dsc/src/subcommand.rs | 20 +- dsc/tests/dsc_adapter.tests.ps1 | 78 +++++ lib/dsc-lib/locales/en-us.toml | 10 +- .../src/discovery/command_discovery.rs | 115 +++++-- lib/dsc-lib/src/dscerror.rs | 6 + .../src/dscresources/command_resource.rs | 302 ++++++++++++------ lib/dsc-lib/src/dscresources/dscresource.rs | 186 ++++++----- .../src/dscresources/resource_manifest.rs | 34 +- lib/dsc-lib/src/extensions/discover.rs | 8 +- tools/dsctest/.project.data.json | 1 + .../adaptedTest.dsc.adaptedResource.json | 38 +++ tools/dsctest/dsctest.dsc.manifests.json | 82 +++++ tools/dsctest/src/adapter.rs | 40 ++- tools/dsctest/src/args.rs | 4 +- tools/dsctest/src/main.rs | 4 +- tools/test_group_resource/src/main.rs | 106 +++--- 17 files changed, 705 insertions(+), 330 deletions(-) create mode 100644 tools/dsctest/adaptedTest.dsc.adaptedResource.json diff --git a/build.data.json b/build.data.json index 1d5463f54..74d8242c2 100644 --- a/build.data.json +++ b/build.data.json @@ -401,6 +401,7 @@ "TestOnly": true, "CopyFiles": { "All": [ + "adaptedTest.dsc.adaptedResource.json", "dsctest.dsc.manifests.json" ] } diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 44d06a48d..9ef3f74af 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -27,7 +27,6 @@ use dsc_lib::{ ValidateResult, }, dscresources::dscresource::{Capability, ImplementedAs, validate_json, validate_properties}, - dscresources::resource_manifest::import_manifest, extensions::dscextension::Capability as ExtensionCapability, functions::FunctionDispatcher, progress::ProgressFormat, @@ -513,7 +512,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat) }; // see if the resource is command based - if resource.implemented_as == ImplementedAs::Command { + if resource.implemented_as == Some(ImplementedAs::Command) { validate_properties(resource, &resource_block["properties"])?; } } @@ -798,28 +797,19 @@ pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adap } // if description, tags, or write_table is specified, pull resource manifest if it exists - if let Some(ref resource_manifest) = resource.manifest { - let manifest = match import_manifest(resource_manifest.clone()) { - Ok(resource_manifest) => resource_manifest, - Err(err) => { - error!("{} {}: {err}", t!("subcommand.invalidManifest"), resource.type_name); - continue; - } - }; - + if let Some(ref manifest) = resource.manifest { // if description is specified, skip if resource description does not contain it - if description.is_some() && - (manifest.description.is_none() | !manifest.description.unwrap_or_default().to_lowercase().contains(&description.unwrap_or(&String::new()).to_lowercase())) { + if description.is_some() && (manifest.description.is_none() | !manifest.description.clone().unwrap_or_default().to_lowercase().contains(&description.unwrap_or(&String::new()).to_lowercase())) { continue; } // if tags is specified, skip if resource tags do not contain the tags if let Some(tags) = tags { - let Some(manifest_tags) = manifest.tags else { continue; }; + let Some(manifest_tags) = &manifest.tags else { continue; }; let mut found = false; for tag_to_find in tags { - for tag in &manifest_tags { + for tag in manifest_tags { if tag.to_lowercase() == tag_to_find.to_lowercase() { found = true; break; diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index 4daf757b1..47d4980f1 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -150,4 +150,82 @@ Describe 'Tests for adapter support' { "$TestDrive/error.log" | Should -FileContentMatch "Invoking get for 'Microsoft.Adapter/PowerShell'" -Because (Get-Content $TestDrive/error.log | Out-String) } } + + Context 'Adapted resource manifests' { + It 'Adapted resource are found in individual manifest' { + $out = dsc resource list 'Adapted/Three' 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + $out.count | Should -Be 1 + $out.type | Should -BeExactly 'Adapted/Three' + $out.kind | Should -BeExactly 'resource' + $out.capabilities | Should -Be @('get', 'set', 'test', 'export') + $parent = (Split-Path -Path (Get-Command dsc).Source -Parent) + $expectedPath = Join-Path -Path $parent -ChildPath 'adaptedTest.dsc.adaptedResource.json' + $out.path | Should -BeExactly $expectedPath + $out.directory | Should -BeExactly $parent + $out.requireAdapter | Should -BeExactly 'Test/Adapter' + $out.schema.embedded | Should -Not -BeNullOrEmpty + } + + It 'Adapted resource found in manifest list' { + $out = dsc resource list 'Adapted/Two' 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + $out.count | Should -Be 1 + $out.type | Should -BeExactly 'Adapted/Two' + $out.kind | Should -BeExactly 'resource' + $out.capabilities | Should -Be @('get', 'set', 'test', 'export') + $parent = (Split-Path -Path (Get-Command dsc).Source -Parent) + $expectedPath = Join-Path -Path $parent -ChildPath 'dsctest' + $out.path | Should -BeExactly $expectedPath + $out.directory | Should -BeExactly $parent + $out.requireAdapter | Should -BeExactly 'Test/Adapter' + $out.schema.embedded | Should -Not -BeNullOrEmpty + } + + It 'Adapted resource with condition false should not be returned' { + $out = dsc -l debug resource list 'Adapted/Four' 2>$TestDrive/error.log + $errorLog = Get-Content $TestDrive/error.log -Raw + $LASTEXITCODE | Should -Be 0 -Because $errorLog + $out | Should -BeNullOrEmpty -Because $errorLog + $errorLog | Should -Match "Condition '.*?' not met, skipping manifest at .*? for resource 'Adapted/Four" -Because $errorLog + } + + It 'Invoking on adapted resource works' -TestCases @( + @{ operation = 'get' }, + @{ operation = 'set' }, + @{ operation = 'test' }, + @{ operation = 'export' } + ){ + param($operation) + $out = dsc resource $operation -r Adapted/Three -i '{"one":"3"}' 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + $parent = (Split-Path -Path (Get-Command dsc).Source -Parent) + $expectedPath = Join-Path -Path $parent -ChildPath 'adaptedTest.dsc.adaptedResource.json' + switch ($operation) { + 'get' { + $out.actualState.one | Should -BeExactly 'value3' + $out.actualState.path | Should -BeExactly $expectedPath + } + 'set' { + $out.afterState.one | Should -BeExactly 'value3' + $out.afterState.path | Should -BeExactly $expectedPath + } + 'test' { + $out.actualState.one | Should -BeExactly 'value3' + $out.actualState.path | Should -BeExactly $expectedPath + $out.inDesiredState | Should -BeFalse + $out.differingProperties | Should -Be @('one') + } + 'export' { + $out.resources.count | Should -Be 2 + $out.resources[0].type | Should -BeExactly 'Adapted/Three' + $out.resources[0].name | Should -BeExactly 'first' + $out.resources[0].properties.one | Should -BeExactly 'first3' + $out.resources[1].type | Should -BeExactly 'Adapted/Three' + $out.resources[1].name | Should -BeExactly 'second' + $out.resources[1].properties.one | Should -BeExactly 'second3' + } + } + } + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 04e250a55..24e328a95 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -108,10 +108,12 @@ foundManifest = "Found manifest: %{path}" extensionFound = "Extension '%{extension}' version %{version} found" adapterFound = "Resource adapter '%{adapter}' version %{version} found" resourceFound = "Resource '%{resource}' version %{version} found" +adaptedResourceFound = "Adapted resource '%{resource}' version %{version} found" executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'" extensionInvalidVersion = "Extension '%{extension}' version '%{version}' is invalid" invalidResourceManifest = "Invalid manifest for resource '%{resource}': %{err}" invalidExtensionManifest = "Invalid manifest for extension '%{extension}': %{err}" +invalidAdaptedResourceManifest = "Invalid manifest for adapted resource '%{resource}': %{err}" invalidManifestList = "Invalid manifest list '%{resource}': %{err}" invalidManifestFile = "Invalid manifest file '%{resource}': %{err}" extensionResourceFound = "Extension found resource '%{resource}'" @@ -125,7 +127,10 @@ foundNonAdapterResources = "Found %{count} non-adapter resources" resourceMissingRequireAdapter = "Resource '%{resource}' is missing 'require_adapter' field." extensionDiscoverFailed = "Extension '%{extension}' failed to discover resources: %{error}" conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean" -conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}'" +conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}' for resource '%{resource}'" +adaptedMissingRequireAdapter = "Adapted resource manifest '%{resource}' missing required 'requireAdapter' property" +adaptedResourcePathNotFound = "Adapted resource '%{resource}' path not found: %{path}" +failedToConvertOsStr = "Failed to convert OsStr to str: %{path}" [dscresources.commandResource] invokeGet = "Invoking get for '%{resource}'" @@ -215,6 +220,7 @@ validationFailed = "Failed validation" adapterResourceNotFound = "Adapter resource '%{adapter}' not found" adapterManifestNotFound = "Adapter manifest for '%{adapter}' not found" adapterDoesNotSupportDelete = "Adapter '%{adapter}' does not support delete operation" +validatingAgainstSchema = "Validating against resource schema" [dscresources.resource_manifest] resourceManifestSchemaTitle = "Resource manifest schema URI" @@ -753,6 +759,8 @@ extension = "Extension" unsupportedCapability = "does not support capability" setting = "Setting" invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{resource}'" +resourceMissingDirectory = "Resource is missing 'directory' field." +resourceMissingPath = "Resource is missing 'path' field." [progress] failedToSerialize = "Failed to serialize progress JSON: %{json}" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index be0601fd1..80e7e6719 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -5,7 +5,7 @@ use crate::{discovery::{discovery_trait::{DiscoveryFilter, DiscoveryKind, Resour use crate::{locked_clear, locked_is_empty, locked_extend, locked_clone, locked_get}; use crate::configure::{config_doc::ResourceDiscoveryMode, context::Context}; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; -use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest, SchemaKind}; +use crate::dscresources::resource_manifest::{validate_semver, Kind, ResourceManifest, SchemaKind}; use crate::dscresources::command_resource::invoke_command; use crate::dscerror::DscError; use crate::extensions::dscextension::{self, DscExtension, Capability as ExtensionCapability}; @@ -29,6 +29,7 @@ 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"]; @@ -41,6 +42,8 @@ static ADAPTED_RESOURCES: LazyLock>>> = #[derive(Deserialize, JsonSchema)] pub struct ManifestList { + #[serde(rename = "adaptedResources")] + pub adapted_resources: Option>, pub resources: Option>, pub extensions: Option>, } @@ -260,7 +263,7 @@ impl ResourceDiscovery for CommandDiscovery { }; let file_name_lowercase = file_name.to_lowercase(); if DSC_MANIFEST_LIST_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) || - (kind == &DiscoveryKind::Resource && (DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)))) || + (kind == &DiscoveryKind::Resource && (DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext))) || DSC_ADAPTED_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext))) || (kind == &DiscoveryKind::Extension && DSC_EXTENSION_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext))) { trace!("{}", t!("discovery.commandDiscovery.foundManifest", path = path.to_string_lossy())); let imported_manifests = match load_manifest(&path) @@ -299,8 +302,7 @@ impl ResourceDiscovery for CommandDiscovery { }, ImportedManifest::Resource(resource) => { if regex.is_match(&resource.type_name) { - if let Some(ref manifest) = resource.manifest { - let manifest = import_manifest(manifest.clone())?; + if let Some(ref manifest) = &resource.manifest { if manifest.kind == Some(Kind::Adapter) { trace!("{}", t!("discovery.commandDiscovery.adapterFound", adapter = resource.type_name, version = resource.version)); insert_resource(&mut adapters, &resource); @@ -309,6 +311,16 @@ impl ResourceDiscovery for CommandDiscovery { trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name, version = resource.version)); insert_resource(&mut resources, &resource); } + if let Some(_adapter) = &resource.require_adapter { + trace!("{}", t!("discovery.commandDiscovery.adaptedResourceFound", resource = resource.type_name, version = resource.version)); + let mut resource = resource.clone(); + let mut directory = path.clone(); + directory.pop(); + let resource_path = directory.join(resource.get_path()?.clone()); + resource.set_path(resource_path); + resource.set_directory(directory); + insert_resource(&mut resources, &resource); + } } } } @@ -398,20 +410,14 @@ impl ResourceDiscovery for CommandDiscovery { found_adapter = true; let mut adapter_progress = ProgressBar::new(1, self.progress_format)?; adapter_progress.write_activity(format!("Enumerating resources for adapter '{adapter_name}'").as_str()); - let manifest = if let Some(manifest) = &adapter.manifest { - if let Ok(manifest) = import_manifest(manifest.clone()) { - manifest - } else { - return Err(DscError::Operation(format!("Failed to import manifest for '{}'", adapter_name.clone()))); - } - } else { + let Some(manifest) = &adapter.manifest else { return Err(DscError::MissingManifest(adapter_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.directory), None, manifest.exit_codes.as_ref()) + let list_command = &manifest.adapter.clone().unwrap().list; + let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.get_directory()?), None, manifest.exit_codes.as_ref()) { Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), Err(e) => { @@ -655,8 +661,39 @@ fn evaluate_condition(condition: Option<&str>) -> Result { /// * Returns a `DscError` if the manifest could not be loaded or parsed. pub fn load_manifest(path: &Path) -> Result, DscError> { let contents = read_to_string(path)?; - let file_name_lowercase = path.file_name().and_then(OsStr::to_str).unwrap_or("").to_lowercase(); - let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); + let file_name_lowercase = path.file_name().and_then(OsStr::to_str).expect(t!("discovery.commandDiscovery.failedToConvertOsStr", path = path.to_string_lossy()).to_string().as_str()).to_lowercase(); let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); + if DSC_ADAPTED_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { + let mut resource = if extension_is_json { + match serde_json::from_str::(&contents) { + Ok(resource) => resource, + Err(err) => { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidAdaptedResourceManifest", resource = path.to_string_lossy(), err = err).to_string())); + } + } + } else { + match serde_yaml::from_str::(&contents) { + Ok(resource) => resource, + Err(err) => { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidAdaptedResourceManifest", resource = path.to_string_lossy(), err = err).to_string())); + } + } + }; + if resource.require_adapter.is_none() { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedMissingRequireAdapter", resource = path.to_string_lossy()).to_string())); + } + let directory = path.parent().unwrap(); + let resource_path = directory.join(resource.get_path()?.clone()); + if !resource_path.exists() { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = resource.type_name).to_string())); + } + if !evaluate_condition(resource.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = resource_path.to_string_lossy(), condition = resource.condition.unwrap_or_default(), resource = resource.type_name)); + return Ok(vec![]); + } + resource.set_path(resource_path); + resource.set_directory(directory.to_path_buf()); + return Ok(vec![ImportedManifest::Resource(resource)]); + } if DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { let manifest = if extension_is_json { match serde_json::from_str::(&contents) { @@ -674,7 +711,7 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { } }; if !evaluate_condition(manifest.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default())); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default(), resource = manifest.resource_type)); return Ok(vec![]); } let resource = load_resource_manifest(path, &manifest)?; @@ -697,7 +734,7 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { } }; if !evaluate_condition(manifest.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default())); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default(), resource = manifest.r#type)); return Ok(vec![]); } let extension = load_extension_manifest(path, &manifest)?; @@ -720,10 +757,28 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { } } }; + if let Some(adapted_resources) = &manifest_list.adapted_resources { + for resource in adapted_resources { + let directory = path.parent().unwrap(); + let resource_path = directory.join(resource.get_path()?); + if !resource_path.exists() { + warn!("{}", t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = resource.type_name).to_string()); + continue; + } + if !evaluate_condition(resource.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = resource.condition.as_ref() : {:?}, resource = resource.type_name)); + continue; + } + let mut resource = resource.clone(); + resource.set_path(resource_path); + resource.set_directory(directory.to_path_buf()); + resources.push(ImportedManifest::Resource(resource.clone())); + } + } if let Some(resource_manifests) = &manifest_list.resources { for res_manifest in resource_manifests { if !evaluate_condition(res_manifest.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = res_manifest.condition.as_ref() : {:?})); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = res_manifest.condition.as_ref() : {:?}, resource = res_manifest.resource_type)); continue; } let resource = load_resource_manifest(path, res_manifest)?; @@ -733,7 +788,7 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { if let Some(extension_manifests) = &manifest_list.extensions { for ext_manifest in extension_manifests { if !evaluate_condition(ext_manifest.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = ext_manifest.condition.as_ref() : {:?})); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = ext_manifest.condition.as_ref() : {:?}, resource = ext_manifest.r#type)); continue; } let extension = load_extension_manifest(path, ext_manifest)?; @@ -790,18 +845,16 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result, +} + /// Invoke the get operation on a resource /// /// # Arguments @@ -25,27 +30,41 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; /// # Errors /// /// Error returned if the resource does not successfully get the current state -pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option) -> Result { - debug!("{}", t!("dscresources.commandResource.invokeGet", resource = &resource.resource_type)); +pub fn invoke_get(resource: &DscResource, cwd: &Path, filter: &str, target_resource: Option<&DscResource>) -> Result { + debug!("{}", t!("dscresources.commandResource.invokeGet", resource = &resource.type_name)); + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; let mut command_input = CommandInput { env: None, stdin: None }; - let Some(get) = &resource.get else { + let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; let resource_type = match target_resource { - Some(r) => r, - None => resource.resource_type.clone(), + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + debug!("Resource requires adapter: {:?}", resource.require_adapter); + debug!("Resource path: {:?}", resource.get_directory()?); + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None }; - let args = process_get_args(get.args.as_ref(), filter, &resource_type); + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, + }; + let args = process_get_args(get.args.as_ref(), filter, &command_resource_info); if !filter.is_empty() { - verify_json(resource, cwd, filter)?; + verify_json_from_manifest(&resource, cwd, filter)?; command_input = get_command_input(get.input.as_ref(), filter)?; } - info!("{}", t!("dscresources.commandResource.invokeGetUsing", resource = &resource.resource_type, executable = &get.executable)); - let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; - if resource.kind == Some(Kind::Resource) { - debug!("{}", t!("dscresources.commandResource.verifyOutputUsing", resource = &resource.resource_type, executable = &get.executable)); - verify_json(resource, cwd, &stdout)?; + info!("{}", t!("dscresources.commandResource.invokeGetUsing", resource = &resource.type_name, executable = &get.executable)); + let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + if resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.verifyOutputUsing", resource = &resource.type_name, executable = &get.executable)); + verify_json_from_manifest(&resource, cwd, &stdout)?; } let result: GetResult = if let Ok(group_response) = serde_json::from_str::>(&stdout) { @@ -78,15 +97,18 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_ /// /// Error returned if the resource does not successfully set the desired state #[allow(clippy::too_many_lines)] -pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option) -> Result { - debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.resource_type)); +pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option<&DscResource>) -> Result { + debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.type_name)); + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; let operation_type: String; let mut is_synthetic_what_if = false; let set_method = match execution_type { ExecutionKind::Actual => { operation_type = "set".to_string(); - &resource.set + &manifest.set }, ExecutionKind::WhatIf => { operation_type = "whatif".to_string(); @@ -113,12 +135,12 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let Some(set) = set_method.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; - verify_json(resource, cwd, desired)?; + verify_json_from_manifest(&resource, cwd, desired)?; // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && set.pre_test != Some(true) { - info!("{}", t!("dscresources.commandResource.noPretest", resource = &resource.resource_type)); - let test_result = invoke_test(resource, cwd, desired, target_resource.clone())?; + info!("{}", t!("dscresources.commandResource.noPretest", resource = &resource.type_name)); + let test_result = invoke_test(resource, cwd, desired, target_resource)?; if is_synthetic_what_if { return Ok(test_result.into()); } @@ -150,35 +172,44 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t return Err(DscError::NotImplemented(t!("dscresources.commandResource.syntheticWhatIf").to_string())); } - let Some(get) = &resource.get else { + let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; let resource_type = match target_resource.clone() { - Some(r) => r, - None => resource.resource_type.clone(), + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None }; - let args = process_get_args(get.args.as_ref(), desired, &resource_type); + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, + }; + let args = process_get_args(get.args.as_ref(), desired, &command_resource_info); let command_input = get_command_input(get.input.as_ref(), desired)?; - info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.resource_type, executable = &get.executable)); - let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.type_name, executable = &get.executable)); + let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; - if resource.kind == Some(Kind::Resource) { - debug!("{}", t!("dscresources.commandResource.setVerifyGet", resource = &resource.resource_type, executable = &get.executable)); - verify_json(resource, cwd, &stdout)?; + if resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.setVerifyGet", resource = &resource.type_name, executable = &get.executable)); + verify_json_from_manifest(&resource, cwd, &stdout)?; } let pre_state_value: Value = if exit_code == 0 { serde_json::from_str(&stdout)? } else { - return Err(DscError::Command(resource.resource_type.to_string(), exit_code, stderr)); + return Err(DscError::Command(resource.type_name.to_string(), exit_code, stderr)); }; let mut pre_state = if pre_state_value.is_object() { let mut pre_state_map: Map = serde_json::from_value(pre_state_value)?; // if the resource is an adapter, then the `get` will return a `result`, but a full `set` expects the before state to be `resources` - if resource.kind == Some(Kind::Adapter) && pre_state_map.contains_key("result") && !pre_state_map.contains_key("resources") { + if resource.kind == Kind::Adapter && pre_state_map.contains_key("result") && !pre_state_map.contains_key("resources") { pre_state_map.insert("resources".to_string(), pre_state_map["result"].clone()); pre_state_map.remove("result"); } @@ -189,7 +220,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &resource_type, execution_type); + let args = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -202,15 +233,15 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t }, } - info!("Invoking {} '{}' using '{}'", operation_type, &resource.resource_type, &set.executable); - let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env, resource.exit_codes.as_ref())?; + info!("Invoking {} '{}' using '{}'", operation_type, &resource.type_name, &set.executable); + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env, manifest.exit_codes.as_ref())?; match set.returns { Some(ReturnKind::State) => { - if resource.kind == Some(Kind::Resource) { - debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.resource_type, executable = &set.executable)); - verify_json(resource, cwd, &stdout)?; + if resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.type_name, executable = &set.executable)); + verify_json_from_manifest(&resource, cwd, &stdout)?; } let actual_value: Value = match serde_json::from_str(&stdout){ @@ -233,12 +264,12 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t // command should be returning actual state as a JSON line and a list of properties that differ as separate JSON line let mut lines = stdout.lines(); let Some(actual_line) = lines.next() else { - return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedOutput").to_string())); + return Err(DscError::Command(resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedOutput").to_string())); }; let actual_value: Value = serde_json::from_str(actual_line)?; // TODO: need schema for diff_properties to validate against let Some(diff_line) = lines.next() else { - return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); + return Err(DscError::Command(resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_line)?; Ok(SetResult::Resource(ResourceSetResponse { @@ -284,31 +315,43 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, target_resource: Option) -> Result { - debug!("{}", t!("dscresources.commandResource.invokeTest", resource = &resource.resource_type)); - let Some(test) = &resource.test else { - info!("{}", t!("dscresources.commandResource.testSyntheticTest", resource = &resource.resource_type)); +pub fn invoke_test(resource: &DscResource, cwd: &Path, expected: &str, target_resource: Option<&DscResource>) -> Result { + debug!("{}", t!("dscresources.commandResource.invokeTest", resource = &resource.type_name)); + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; + let Some(test) = &manifest.test else { + info!("{}", t!("dscresources.commandResource.testSyntheticTest", resource = &resource.type_name)); return invoke_synthetic_test(resource, cwd, expected, target_resource); }; - verify_json(resource, cwd, expected)?; + verify_json_from_manifest(&resource, cwd, expected)?; let resource_type = match target_resource.clone() { - Some(r) => r, - None => resource.resource_type.clone(), + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None + }; + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, }; - let args = process_get_args(test.args.as_ref(), expected, &resource_type); + let args = process_get_args(test.args.as_ref(), expected, &command_resource_info); let command_input = get_command_input(test.input.as_ref(), expected)?; - info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.resource_type, executable = &test.executable)); - let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.type_name, executable = &test.executable)); + let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; - if resource.kind == Some(Kind::Resource) { - debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &resource.resource_type, executable = &test.executable)); - verify_json(resource, cwd, &stdout)?; + if resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &resource.type_name, executable = &test.executable)); + verify_json_from_manifest(&resource, cwd, &stdout)?; } - if resource.kind == Some(Kind::Importer) { + if resource.kind == Kind::Importer { debug!("{}", t!("dscresources.commandResource.testGroupTestResponse")); let group_test_response: Vec = serde_json::from_str(&stdout)?; return Ok(TestResult::Group(group_test_response)); @@ -337,11 +380,11 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, targ // command should be returning actual state as a JSON line and a list of properties that differ as separate JSON line let mut lines = stdout.lines(); let Some(actual_value) = lines.next() else { - return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.testNoActualState").to_string())); + return Err(DscError::Command(resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.testNoActualState").to_string())); }; let actual_value: Value = serde_json::from_str(actual_value)?; let Some(diff_properties) = lines.next() else { - return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string())); + return Err(DscError::Command(resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_properties)?; expected_value = redact(&expected_value); @@ -393,8 +436,7 @@ fn get_desired_state(actual: &Value) -> Result, DscError> { Ok(in_desired_state) } -fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str, target_resource: Option) -> Result { - let get_result = invoke_get(resource, cwd, expected, target_resource)?; +fn invoke_synthetic_test(resource: &DscResource, cwd: &Path, expected: &str, target_resource: Option<&DscResource>) -> Result { let get_result = invoke_get(resource, cwd, expected, target_resource)?; let actual_state = match get_result { GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); @@ -429,23 +471,34 @@ fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option<&str>) -> Result<(), DscError> { - let Some(delete) = &resource.delete else { +pub fn invoke_delete(resource: &DscResource, cwd: &Path, filter: &str, target_resource: Option<&DscResource>) -> Result<(), DscError> { + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; + let Some(delete) = &manifest.delete else { return Err(DscError::NotImplemented("delete".to_string())); }; - verify_json(resource, cwd, filter)?; + verify_json_from_manifest(&resource, cwd, filter)?; let resource_type = match target_resource { - Some(r) => r, - None => &resource.resource_type, + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), }; - let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, &ExecutionKind::Actual); - + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None + }; + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, + }; + let args = process_set_delete_args(delete.args.as_ref(), filter, &command_resource_info, &ExecutionKind::Actual); let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); - let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; + let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; Ok(()) } @@ -465,22 +518,34 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, targ /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_validate(resource: &ResourceManifest, cwd: &Path, config: &str, target_resource: Option<&str>) -> Result { - trace!("{}", t!("dscresources.commandResource.invokeValidateConfig", resource = &resource.resource_type, config = &config)); +pub fn invoke_validate(resource: &DscResource, cwd: &Path, config: &str, target_resource: Option<&DscResource>) -> Result { + trace!("{}", t!("dscresources.commandResource.invokeValidateConfig", resource = &resource.type_name, config = &config)); + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; // TODO: use schema to validate config if validate is not implemented - let Some(validate) = resource.validate.as_ref() else { + let Some(validate) = manifest.validate.as_ref() else { return Err(DscError::NotImplemented("validate".to_string())); }; let resource_type = match target_resource { - Some(r) => r, - None => &resource.resource_type, + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None + }; + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, }; - let args = process_get_args(validate.args.as_ref(), config, resource_type); + let args = process_get_args(validate.args.as_ref(), config, &command_resource_info); let command_input = get_command_input(validate.input.as_ref(), config)?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) } @@ -494,14 +559,17 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &Path, config: &str, ta /// # Errors /// /// Error if schema is not available or if there is an error getting the schema -pub fn get_schema(resource: &ResourceManifest, cwd: &Path) -> Result { - let Some(schema_kind) = resource.schema.as_ref() else { - return Err(DscError::SchemaNotAvailable(resource.resource_type.to_string())); +pub fn get_schema(resource: &DscResource, cwd: &Path) -> Result { + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; + let Some(schema_kind) = manifest.schema.as_ref() else { + return Err(DscError::SchemaNotAvailable(resource.type_name.to_string())); }; match schema_kind { SchemaKind::Command(ref command) => { - let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None, resource.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None, manifest.exit_codes.as_ref())?; Ok(stdout) }, SchemaKind::Embedded(ref schema) => { @@ -526,11 +594,14 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &Path) -> Result, target_resource: Option) -> Result { - let Some(export) = resource.export.as_ref() else { +pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, target_resource: Option<&DscResource>) -> Result { + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; + let Some(export) = manifest.export.as_ref() else { // see if get is supported and use that instead - if resource.get.is_some() { - info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.resource_type)); + if manifest.get.is_some() { + info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.type_name)); let get_result = invoke_get(resource, cwd, input.unwrap_or(""), target_resource)?; let mut instances: Vec = Vec::new(); match get_result { @@ -548,28 +619,37 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str }); } // if neither export nor get is supported, return an error - return Err(DscError::Operation(t!("dscresources.commandResource.exportNotSupported", resource = &resource.resource_type).to_string())) + return Err(DscError::Operation(t!("dscresources.commandResource.exportNotSupported", resource = &resource.type_name).to_string())) }; let mut command_input: CommandInput = CommandInput { env: None, stdin: None }; let args: Option>; let resource_type = match target_resource { - Some(r) => r, - None => resource.resource_type.clone(), + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None + }; + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, }; if let Some(input) = input { if !input.is_empty() { - verify_json(resource, cwd, input)?; + verify_json_from_manifest(&resource, cwd, input)?; command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_get_args(export.args.as_ref(), input, &resource_type); + args = process_get_args(export.args.as_ref(), input, &command_resource_info); } else { - args = process_get_args(export.args.as_ref(), "", &resource_type); + args = process_get_args(export.args.as_ref(), "", &command_resource_info); } - let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; + let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; let mut instances: Vec = Vec::new(); for line in stdout.lines() { @@ -579,9 +659,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str return Err(DscError::Operation(t!("dscresources.commandResource.failedParseJson", executable = &export.executable, stdout = stdout, stderr = stderr, err = err).to_string())) } }; - if resource.kind == Some(Kind::Resource) { - debug!("{}", t!("dscresources.commandResource.exportVerifyOutput", resource = &resource.resource_type, executable = &export.executable)); - verify_json(resource, cwd, line)?; + if resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.exportVerifyOutput", resource = &resource.type_name, executable = &export.executable)); + verify_json_from_manifest(&resource, cwd, line)?; } instances.push(instance); } @@ -606,16 +686,24 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str /// # Errors /// /// Error returned if the resource does not successfully resolve the input -pub fn invoke_resolve(resource: &ResourceManifest, cwd: &Path, input: &str) -> Result { - let Some(resolve) = &resource.resolve else { - return Err(DscError::Operation(t!("dscresources.commandResource.resolveNotSupported", resource = &resource.resource_type).to_string())); +pub fn invoke_resolve(resource: &DscResource, cwd: &Path, input: &str) -> Result { + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; + let Some(resolve) = &manifest.resolve else { + return Err(DscError::Operation(t!("dscresources.commandResource.resolveNotSupported", resource = &resource.type_name).to_string())); }; - let args = process_get_args(resolve.args.as_ref(), input, &resource.resource_type); + let command_resource_info = CommandResourceInfo { + type_name: resource.type_name.clone(), + path: if resource.require_adapter.is_some() { Some(resource.get_path()?.clone()) } else { None }, + }; + + let args = process_get_args(resolve.args.as_ref(), input, &command_resource_info); let command_input = get_command_input(resolve.input.as_ref(), input)?; - info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.resource_type, executable = &resolve.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.type_name, executable = &resolve.executable)); + let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; let result: ResolveResult = serde_json::from_str(&stdout)?; Ok(result) } @@ -825,7 +913,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_get_args(args: Option<&Vec>, input: &str, resource_type: &str) -> Option> { +pub fn process_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -847,7 +935,15 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, resource_ty }, GetArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(resource_type.to_string()); + processed_args.push(command_resource_info.type_name.to_string()); + }, + ArgKind::ResourcePath { resource_path_arg } => { + debug!("ResourcePath Arg: {:?}", resource_path_arg); + debug!("Command Resource Info Path: {:?}", command_resource_info.path); + if let Some(path) = &command_resource_info.path { + processed_args.push(resource_path_arg.clone()); + processed_args.push(path.to_string_lossy().to_string()); + } }, } } @@ -931,12 +1027,14 @@ fn get_command_input(input_kind: Option<&InputKind>, input: &str) -> Result Result<(), DscError> { - - debug!("{}", t!("dscresources.commandResource.verifyJson", resource = resource.resource_type)); +fn verify_json_from_manifest(resource: &DscResource, cwd: &Path, json: &str) -> Result<(), DscError> { + debug!("{}", t!("dscresources.commandResource.verifyJson", resource = resource.type_name)); + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; // see if resource implements validate - if resource.validate.is_some() { + if manifest.validate.is_some() { trace!("{}", t!("dscresources.commandResource.validateJson", json = json)); let result = invoke_validate(resource, cwd, json, None)?; if result.valid { diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index e5b1fcb37..6b848c57a 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -23,9 +23,7 @@ use super::{ invoke_result::{ ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult }, - resource_manifest::{ - import_manifest, ResourceManifest - } + resource_manifest::ResourceManifest, }; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -41,26 +39,30 @@ pub struct DscResource { pub version: String, /// The capabilities of the resource. pub capabilities: Vec, + /// An optional condition for the resource to be active. + pub condition: Option, /// The file path to the resource. - pub path: PathBuf, + path: Option, /// The description of the resource. pub description: Option, // The directory path to the resource. - pub directory: PathBuf, + directory: Option, /// The implementation of the resource. #[serde(rename="implementedAs")] - pub implemented_as: ImplementedAs, + pub implemented_as: Option, /// The author of the resource. pub author: Option, /// The properties of the resource. - pub properties: Vec, + pub properties: Option>, /// The required resource adapter for the resource. #[serde(rename="requireAdapter")] pub require_adapter: Option, + /// The JSON Schema of the resource. + pub schema: Option>, /// The target resource for the resource adapter. - pub target_resource: Option, + pub target_resource: Option>, /// The manifest of the resource. - pub manifest: Option, + pub manifest: Option, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -103,18 +105,42 @@ impl DscResource { kind: Kind::Resource, version: String::new(), capabilities: Vec::new(), + condition: None, description: None, - path: PathBuf::new(), - directory: PathBuf::new(), - implemented_as: ImplementedAs::Command, + path: None, + directory: None, + implemented_as: Some(ImplementedAs::Command), author: None, - properties: Vec::new(), + properties: None, require_adapter: None, + schema: None, target_resource: None, manifest: None, } } + pub fn get_path(&self) -> Result<&PathBuf, DscError> { + match &self.path { + Some(path) => Ok(path), + None => Err(DscError::ResourceMissingPath(self.type_name.to_string())), + } + } + + pub fn set_path(&mut self, path: PathBuf) { + self.path = Some(path); + } + + pub fn get_directory(&self) -> Result<&PathBuf, DscError> { + match &self.directory { + Some(directory) => Ok(directory), + None => Err(DscError::ResourceMissingDirectory(self.type_name.to_string())), + } + } + + pub fn set_directory(&mut self, directory: PathBuf) { + self.directory = Some(directory); + } + fn create_config_for_adapter(self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { // create new configuration with adapter and use this as the resource let mut configuration = Configuration::new(); @@ -141,11 +167,11 @@ impl DscResource { Ok(configurator) } - fn invoke_get_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str) -> Result { + fn invoke_get_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource, filter: &str) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(FullyQualifiedTypeName::new(resource_name)?); + adapter.target_resource = Some(Box::new(target_resource.clone())); return adapter.get(filter); } @@ -165,11 +191,11 @@ impl DscResource { Ok(get_result) } - fn invoke_set_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { + fn invoke_set_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, desired)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(resource_name.clone()); + adapter.target_resource = Some(Box::new(target_resource.clone())); return adapter.set(desired, skip_test, execution_type); } @@ -198,11 +224,11 @@ impl DscResource { Ok(set_result) } - fn invoke_test_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, expected: &str) -> Result { + fn invoke_test_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource, expected: &str) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, expected)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(FullyQualifiedTypeName::new(resource_name)?); + adapter.target_resource = Some(Box::new(target_resource.clone())); return adapter.test(expected); } @@ -232,12 +258,12 @@ impl DscResource { Ok(test_result) } - fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str) -> Result<(), DscError> { + fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource, filter: &str) -> Result<(), DscError> { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { if adapter.capabilities.contains(&Capability::Delete) { - adapter.target_resource = Some(resource_name.clone()); + adapter.target_resource = Some(Box::new(target_resource.clone())); return adapter.delete(filter); } return Err(DscError::NotSupported(t!("dscresources.dscresource.adapterDoesNotSupportDelete", adapter = adapter.type_name).to_string())); @@ -247,11 +273,11 @@ impl DscResource { Ok(()) } - fn invoke_export_with_adapter(&self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { + fn invoke_export_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource,input: &str) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, input)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(self.type_name.clone()); + adapter.target_resource = Some(Box::new(target_resource.clone())); return adapter.export(input); } @@ -285,12 +311,6 @@ impl DscResource { } } -impl Default for DscResource { - fn default() -> Self { - DscResource::new() - } -} - /// The interface for a DSC resource. pub trait Invoke { /// Invoke the get operation on the resource. @@ -383,19 +403,15 @@ impl Invoke for DscResource { fn get(&self, filter: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeGet", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - return self.invoke_get_with_adapter(adapter, &self.type_name, filter); + return self.invoke_get_with_adapter(adapter, &self, filter); } match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) + Some(ImplementedAs::Command) => { + command_resource::invoke_get(&self, &self.get_directory()?, filter, self.target_resource.as_deref()) }, - ImplementedAs::Command => { - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_get(&resource_manifest, &self.directory, filter, self.target_resource.clone()) + _ => { + Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) }, } } @@ -403,19 +419,15 @@ impl Invoke for DscResource { fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { debug!("{}", t!("dscresources.dscresource.invokeSet", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - return self.invoke_set_with_adapter(adapter, &self.type_name, desired, skip_test, execution_type); + return self.invoke_set_with_adapter(adapter, &self, desired, skip_test, execution_type); } match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) + Some(ImplementedAs::Command) => { + command_resource::invoke_set(&self, &self.get_directory()?, desired, skip_test, execution_type, self.target_resource.as_deref()) }, - ImplementedAs::Command => { - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type, self.target_resource.clone()) + _ => { + Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) }, } } @@ -423,21 +435,17 @@ impl Invoke for DscResource { fn test(&self, expected: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeTest", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - return self.invoke_test_with_adapter(adapter, &self.type_name, expected); + return self.invoke_test_with_adapter(adapter, &self, expected); } match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) - }, - ImplementedAs::Command => { + Some(ImplementedAs::Command) => { let Some(manifest) = &self.manifest else { return Err(DscError::MissingManifest(self.type_name.to_string())); }; // if test is not directly implemented, then we need to handle it here - let resource_manifest = import_manifest(manifest.clone())?; - if resource_manifest.test.is_none() { + if manifest.test.is_none() { let get_result = self.get(expected)?; let mut desired_state = serde_json::from_str(expected)?; let actual_state = match get_result { @@ -463,28 +471,27 @@ impl Invoke for DscResource { Ok(test_result) } else { - command_resource::invoke_test(&resource_manifest, &self.directory, expected, self.target_resource.clone()) + command_resource::invoke_test(&self, &self.get_directory()?, expected, self.target_resource.as_deref()) } }, + _ => { + Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) + }, } } fn delete(&self, filter: &str) -> Result<(), DscError> { debug!("{}", t!("dscresources.dscresource.invokeDelete", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - return self.invoke_delete_with_adapter(adapter, &self.type_name, filter); + return self.invoke_delete_with_adapter(adapter, &self, filter); } match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) + Some(ImplementedAs::Command) => { + command_resource::invoke_delete(&self, &self.get_directory()?, filter, self.target_resource.as_deref()) }, - ImplementedAs::Command => { - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_delete(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) + _ => { + Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) }, } } @@ -496,15 +503,11 @@ impl Invoke for DscResource { } match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) + Some(ImplementedAs::Command) => { + command_resource::invoke_validate(&self, &self.get_directory()?, config, self.target_resource.as_deref()) }, - ImplementedAs::Command => { - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_validate(&resource_manifest, &self.directory, config, self.target_resource.as_deref()) + _ => { + Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) }, } } @@ -516,15 +519,11 @@ impl Invoke for DscResource { } match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) + Some(ImplementedAs::Command) => { + command_resource::get_schema(&self, &self.get_directory()?) }, - ImplementedAs::Command => { - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::get_schema(&resource_manifest, &self.directory) + _ => { + Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) }, } } @@ -532,14 +531,10 @@ impl Invoke for DscResource { fn export(&self, input: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeExport", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - return self.invoke_export_with_adapter(adapter, input); + return self.invoke_export_with_adapter(adapter, &self, input); } - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_export(&resource_manifest, &self.directory, Some(input), self.target_resource.clone()) + command_resource::invoke_export(&self, &self.get_directory()?, Some(input), self.target_resource.as_deref()) } fn resolve(&self, input: &str) -> Result { @@ -548,11 +543,7 @@ impl Invoke for DscResource { return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeResolveNotSupported", resource = self.type_name).to_string())); } - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.to_string())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_resolve(&resource_manifest, &self.directory, input) + command_resource::invoke_resolve(&self, &self.get_directory()?, input) } } @@ -605,10 +596,8 @@ pub fn redact(value: &Value) -> Value { /// * `DscError` - The adapter manifest is not found or invalid pub fn get_adapter_input_kind(adapter: &DscResource) -> Result { if let Some(manifest) = &adapter.manifest { - if let Ok(manifest) = serde_json::from_value::(manifest.clone()) { - if let Some(adapter_operation) = manifest.adapter { - return Ok(adapter_operation.input_kind); - } + if let Some(adapter_operation) = &manifest.adapter { + return Ok(adapter_operation.input_kind.clone()); } } Err(DscError::Operation(t!("dscresources.dscresource.adapterManifestNotFound", adapter = adapter.type_name).to_string())) @@ -712,9 +701,12 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec { pub fn validate_properties(resource: &DscResource, properties: &Value) -> Result<(), DscError> { // if so, see if it implements validate via the resource manifest let type_name = resource.type_name.clone(); + if let Some(schema) = &resource.schema { + debug!("{}: {type_name} ", t!("dscresources.dscresource.validatingAgainstSchema")); + let schema = serde_json::to_value(schema)?; + return validate_json(&resource.type_name, &schema, properties); + } if let Some(manifest) = resource.manifest.clone() { - // convert to resource_manifest`` - let manifest: ResourceManifest = serde_json::from_value(manifest)?; if manifest.validate.is_some() { debug!("{}: {type_name} ", t!("dscresources.dscresource.resourceImplementsValidate")); let resource_config = properties.to_string(); diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index be63aa589..d299933f0 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -9,7 +9,6 @@ use serde_json::{Map, Value}; use std::collections::HashMap; use crate::{ - dscerror::DscError, schemas::{dsc_repo::DscRepoSchema, transforms::idiomaticize_string_enum}, types::FullyQualifiedTypeName, }; @@ -108,11 +107,17 @@ pub enum GetArgKind { /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. mandatory: Option, }, + ResourcePath { + /// The argument that accepts the resource path. + #[serde(rename = "resourcePathArg")] + resource_path_arg: String, + }, ResourceType { /// The argument that accepts the resource type name. #[serde(rename = "resourceTypeArg")] resource_type_arg: String, }, +<<<<<<< HEAD } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -140,6 +145,10 @@ pub enum SetDeleteArgKind { #[serde(rename = "whatIfArg")] what_if_arg: String, } +||||||| parent of 89f901ff (Enable support for adapted resource manifests) + } +======= +>>>>>>> 89f901ff (Enable support for adapted resource manifests) } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -307,29 +316,6 @@ pub struct ListMethod { pub args: Option>, } -/// Import a resource manifest from a JSON value. -/// -/// # Arguments -/// -/// * `manifest` - The JSON value to import. -/// -/// # Returns -/// -/// * `Result` - The imported resource manifest. -/// -/// # Errors -/// -/// * `DscError` - The JSON value is invalid or the schema version is not supported. -pub fn import_manifest(manifest: Value) -> Result { - // TODO: enable schema version validation, if not provided, use the latest - // const MANIFEST_SCHEMA_VERSION: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json"; - let manifest = serde_json::from_value::(manifest)?; - // if !manifest.schema_version.eq(MANIFEST_SCHEMA_VERSION) { - // return Err(DscError::InvalidManifestSchemaVersion(manifest.schema_version, MANIFEST_SCHEMA_VERSION.to_string())); - // } - Ok(manifest) -} - /// Validate a semantic version string. /// /// # Arguments diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index d24714869..cb3c63d6e 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -8,7 +8,7 @@ use crate::{ dscerror::DscError, dscresources::{ command_resource::{ - invoke_command, process_get_args + invoke_command, process_get_args, CommandResourceInfo }, dscresource::DscResource, resource_manifest::GetArgKind, @@ -68,7 +68,11 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let args = process_get_args(discover.args.as_ref(), "", self.type_name.as_ref()); + let command_resource_info = CommandResourceInfo { + type_name: self.type_name.clone(), + path: None, + }; + let args = process_get_args(discover.args.as_ref(), "", &command_resource_info); let (_exit_code, stdout, _stderr) = invoke_command( &discover.executable, args, diff --git a/tools/dsctest/.project.data.json b/tools/dsctest/.project.data.json index c6bf80736..8f7300723 100644 --- a/tools/dsctest/.project.data.json +++ b/tools/dsctest/.project.data.json @@ -8,6 +8,7 @@ ], "CopyFiles": { "All": [ + "adaptedTest.dsc.adaptedResource.json", "dsctest.dsc.manifests.json" ] } diff --git a/tools/dsctest/adaptedTest.dsc.adaptedResource.json b/tools/dsctest/adaptedTest.dsc.adaptedResource.json new file mode 100644 index 000000000..46fa470dd --- /dev/null +++ b/tools/dsctest/adaptedTest.dsc.adaptedResource.json @@ -0,0 +1,38 @@ +{ + "type": "Adapted/Three", + "kind": "resource", + "version": "1.0.0", + "capabilities": [ + "get", + "set", + "test", + "export" + ], + "description": "An adapted resource for testing.", + "author": "DSC Team", + "requireAdapter": "Test/Adapter", + "path": "adaptedTest.dsc.adaptedResource.json", + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Microsoft/OSInfo/v0.1.0/schema.json", + "title": "OsInfo", + "description": "Returns information about the operating system.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource\n", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "one": { + "type": "string", + "title": "Property One", + "description": "This is property one of the adapted resource." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the adapted resource instance." + } + } + } + } +} diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 806e61aea..13f8a02ea 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -1,4 +1,83 @@ { + "adaptedResources": [ + { + "type": "Adapted/Two", + "kind": "resource", + "version": "1.0.0", + "capabilities": [ + "get", + "set", + "test", + "export" + ], + "description": "An adapted resource for testing.", + "author": "DSC Team", + "requireAdapter": "Test/Adapter", + "path": "dsctest", + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Microsoft/OSInfo/v0.1.0/schema.json", + "title": "OsInfo", + "description": "Returns information about the operating system.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource\n", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "two": { + "type": "string", + "title": "Property Two", + "description": "This is property two of the adapted resource." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the adapted resource instance." + } + } + } + } + }, + { + "type": "Adapted/Four", + "kind": "resource", + "version": "1.0.0", + "capabilities": [ + "get", + "set", + "test", + "export" + ], + "description": "An adapted resource for testing.", + "author": "DSC Team", + "requireAdapter": "Test/Adapter", + "path": "dsctest", + "condition": "[false()]", + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Microsoft/OSInfo/v0.1.0/schema.json", + "title": "OsInfo", + "description": "Returns information about the operating system.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource\n", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "two": { + "type": "string", + "title": "Property Two", + "description": "This is property two of the adapted resource." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the adapted resource instance." + } + } + } + } + } + ], "resources": [ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", @@ -500,6 +579,9 @@ }, { "resourceTypeArg": "--resource-type" + }, + { + "resourcePathArg": "--resource-path" } ] }, diff --git a/tools/dsctest/src/adapter.rs b/tools/dsctest/src/adapter.rs index d634c541a..6672dde02 100644 --- a/tools/dsctest/src/adapter.rs +++ b/tools/dsctest/src/adapter.rs @@ -12,6 +12,8 @@ pub struct AdaptedOne { pub one: String, #[serde(rename = "_name", skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -20,6 +22,8 @@ pub struct AdaptedTwo { pub two: String, #[serde(rename = "_name", skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] @@ -48,7 +52,7 @@ pub struct DscResource { pub require_adapter: Option, } -pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> Result { +pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, resource_path: &Option) -> Result { match operation { AdapterOperation::List => { let resource_one = DscResource { @@ -83,6 +87,7 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> let adapted_one = AdaptedOne { one: "value1".to_string(), name: None, + path: resource_path.clone(), }; Ok(serde_json::to_string(&adapted_one).unwrap()) }, @@ -90,9 +95,18 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> let adapted_two = AdaptedTwo { two: "value2".to_string(), name: None, + path: resource_path.clone(), }; Ok(serde_json::to_string(&adapted_two).unwrap()) }, + "Adapted/Three" => { + let adapted_three = AdaptedOne { + one: "value3".to_string(), + name: None, + path: resource_path.clone(), + }; + Ok(serde_json::to_string(&adapted_three).unwrap()) + }, _ => Err(format!("Unknown resource type: {resource_type}")), } }, @@ -108,6 +122,11 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> .map_err(|e| format!("Failed to parse input for Adapted/Two: {e}"))?; Ok(serde_json::to_string(&adapted_two).unwrap()) }, + "Adapted/Three" => { + let adapted_three: AdaptedOne = serde_json::from_str(input) + .map_err(|e| format!("Failed to parse input for Adapted/Three: {e}"))?; + Ok(serde_json::to_string(&adapted_three).unwrap()) + }, _ => Err(format!("Unknown resource type: {resource_type}")), } }, @@ -117,11 +136,13 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> let adapted_one = AdaptedOne { one: "first1".to_string(), name: Some("first".to_string()), + path: None, }; println!("{}", serde_json::to_string(&adapted_one).unwrap()); let adapted_one = AdaptedOne { one: "second1".to_string(), name: Some("second".to_string()), + path: None, }; println!("{}", serde_json::to_string(&adapted_one).unwrap()); std::process::exit(0); @@ -130,15 +151,32 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> let adapted_two = AdaptedTwo { two: "first2".to_string(), name: Some("first".to_string()), + path: None, }; println!("{}", serde_json::to_string(&adapted_two).unwrap()); let adapted_two = AdaptedTwo { two: "second2".to_string(), name: Some("second".to_string()), + path: None, }; println!("{}", serde_json::to_string(&adapted_two).unwrap()); std::process::exit(0); }, + "Adapted/Three" => { + let adapted_three = AdaptedOne { + one: "first3".to_string(), + name: Some("first".to_string()), + path: None, + }; + println!("{}", serde_json::to_string(&adapted_three).unwrap()); + let adapted_three = AdaptedOne { + one: "second3".to_string(), + name: Some("second".to_string()), + path: None, + }; + println!("{}", serde_json::to_string(&adapted_three).unwrap()); + std::process::exit(0); + }, _ => Err(format!("Unknown resource type: {resource_type}")), } }, diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 6bc62da6e..4f28ff0f8 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -46,8 +46,10 @@ pub enum SubCommand { Adapter { #[clap(name = "input", short, long, help = "The input to the adapter command as JSON")] input: String, - #[clap(name = "resource-type", short, long, help = "The resource type to adapt to")] + #[clap(name = "resource-type", long, help = "The resource type to adapt to")] resource_type: String, + #[clap(name = "resource-path", long, help = "The path to the adapted resource")] + resource_path: Option, #[clap(name = "operation", short, long, help = "The operation to perform")] operation: AdapterOperation, }, diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 23bdf1fe9..ae5610f8b 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -42,8 +42,8 @@ use std::{thread, time::Duration}; fn main() { let args = Args::parse(); let json = match args.subcommand { - SubCommand::Adapter { input , resource_type, operation } => { - match adapter::adapt(&resource_type, &input, &operation) { + SubCommand::Adapter { input , resource_type, resource_path, operation } => { + match adapter::adapt(&resource_type, &input, &operation, &resource_path) { Ok(result) => result, Err(err) => { eprintln!("Error adapting resource: {err}"); diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index f685eab94..77d0e21bd 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -3,34 +3,33 @@ mod args; +use std::path::PathBuf; + use args::{Args, SubCommand}; use clap::Parser; +use dsc_lib::dscresources::resource_manifest::{ResourceManifest, GetMethod, Kind}; use dsc_lib::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; -use dsc_lib::dscresources::resource_manifest::{GetMethod, Kind, ResourceManifest}; -use dsc_lib::schemas::dsc_repo::DscRepoSchema; -use std::path::PathBuf; +use dsc_lib::schemas::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}; +use dsc_lib::types::FullyQualifiedTypeName; fn main() { let args = Args::parse(); match args.subcommand { SubCommand::List => { - let resource1 = DscResource { - type_name: "Test/TestResource1".parse().unwrap(), - kind: Kind::Resource, - version: "1.0.0".to_string(), - capabilities: vec![Capability::Get, Capability::Set], + let mut resource1 = DscResource::new(); + resource1.type_name = FullyQualifiedTypeName::new("Test/TestResource1").unwrap(); + resource1.kind = Kind::Resource; + resource1.version = "1.0.0".to_string(); + resource1.capabilities = vec![Capability::Get, Capability::Set]; + resource1.description = Some("This is a test resource.".to_string()); + resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); + resource1.author = Some("Microsoft".to_string()); + resource1.require_adapter = Some(FullyQualifiedTypeName::new("Test/TestGroup").unwrap()); + resource1.target_resource = None; + resource1.manifest = Some(ResourceManifest { description: Some("This is a test resource.".to_string()), - implemented_as: ImplementedAs::Custom("TestResource".to_string()), - path: PathBuf::from("test_resource1"), - directory: PathBuf::from("test_directory"), - author: Some("Microsoft".to_string()), - properties: vec!["Property1".to_string(), "Property2".to_string()], - require_adapter: Some("Test/TestGroup".parse().unwrap()), - target_resource: None, - manifest: Some(serde_json::to_value(ResourceManifest { - description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource1".parse().unwrap(), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + resource_type: FullyQualifiedTypeName::new("Test/TestResource1").unwrap(), kind: Some(Kind::Resource), version: "1.0.0".to_string(), get: Some(GetMethod { @@ -38,25 +37,24 @@ fn main() { ..Default::default() }), ..Default::default() - }).unwrap()), - }; - let resource2 = DscResource { - type_name: "Test/TestResource2".parse().unwrap(), - kind: Kind::Resource, - version: "1.0.1".to_string(), - capabilities: vec![Capability::Get, Capability::Set], - description: Some("This is a test resource.".to_string()), - implemented_as: ImplementedAs::Custom("TestResource".to_string()), - path: PathBuf::from("test_resource2"), - directory: PathBuf::from("test_directory"), - author: Some("Microsoft".to_string()), - properties: vec!["Property1".to_string(), "Property2".to_string()], - require_adapter: Some("Test/TestGroup".parse().unwrap()), - target_resource: None, - manifest: Some(serde_json::to_value(ResourceManifest { + } + ); + resource1.set_path(PathBuf::from("test_resource1")); + resource1.set_directory(PathBuf::from("test_directory")); + let mut resource2 = DscResource::new(); + resource2.type_name = FullyQualifiedTypeName::new("Test/TestResource2").unwrap(); + resource2.kind = Kind::Resource; + resource2.version = "1.0.1".to_string(); + resource2.capabilities = vec![Capability::Get, Capability::Set]; + resource2.description = Some("This is a test resource.".to_string()); + resource2.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); + resource2.author = Some("Microsoft".to_string()); + resource2.require_adapter = Some(FullyQualifiedTypeName::new("Test/TestGroup").unwrap()); + resource2.target_resource = None; + resource2.manifest = Some(ResourceManifest { description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource2".parse().unwrap(), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + resource_type: FullyQualifiedTypeName::new("Test/TestResource2").unwrap(), kind: Some(Kind::Resource), version: "1.0.1".to_string(), get: Some(GetMethod { @@ -64,27 +62,27 @@ fn main() { ..Default::default() }), ..Default::default() - }).unwrap()), - }; + } + ); + resource2.set_path(PathBuf::from("test_resource2")); + resource2.set_directory(PathBuf::from("test_directory")); println!("{}", serde_json::to_string(&resource1).unwrap()); println!("{}", serde_json::to_string(&resource2).unwrap()); }, SubCommand::ListMissingRequires => { - let resource1 = DscResource { - type_name: "Test/InvalidResource".parse().unwrap(), - kind: Kind::Resource, - version: "1.0.0".to_string(), - capabilities: vec![Capability::Get], - description: Some("This is a test resource.".to_string()), - implemented_as: ImplementedAs::Custom("TestResource".to_string()), - path: PathBuf::from("test_resource1"), - directory: PathBuf::from("test_directory"), - author: Some("Microsoft".to_string()), - properties: vec!["Property1".to_string(), "Property2".to_string()], - require_adapter: None, - target_resource: None, - manifest: None, - }; + let mut resource1 = DscResource::new(); + resource1.type_name = FullyQualifiedTypeName::new("InvalidResource").unwrap(); + resource1.kind = Kind::Resource; + resource1.version = "1.0.0".to_string(); + resource1.capabilities = vec![Capability::Get]; + resource1.description = Some("This is a test resource.".to_string()); + resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); + resource1.author = Some("Microsoft".to_string()); + resource1.require_adapter = None; + resource1.target_resource = None; + resource1.manifest = None; + resource1.set_path(PathBuf::from("test_resource1")); + resource1.set_directory(PathBuf::from("test_directory")); println!("{}", serde_json::to_string(&resource1).unwrap()); } } From b9992d217f97ef964c50b9199ef1773bd0158e20 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Feb 2026 16:12:36 -0800 Subject: [PATCH 02/11] fix copilot feedback --- lib/dsc-lib/locales/en-us.toml | 1 - lib/dsc-lib/src/discovery/command_discovery.rs | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 24e328a95..56ef7e0be 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -130,7 +130,6 @@ conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean" conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}' for resource '%{resource}'" adaptedMissingRequireAdapter = "Adapted resource manifest '%{resource}' missing required 'requireAdapter' property" adaptedResourcePathNotFound = "Adapted resource '%{resource}' path not found: %{path}" -failedToConvertOsStr = "Failed to convert OsStr to str: %{path}" [dscresources.commandResource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 80e7e6719..64f5b3e08 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -661,7 +661,10 @@ fn evaluate_condition(condition: Option<&str>) -> Result { /// * Returns a `DscError` if the manifest could not be loaded or parsed. pub fn load_manifest(path: &Path) -> Result, DscError> { let contents = read_to_string(path)?; - let file_name_lowercase = path.file_name().and_then(OsStr::to_str).expect(t!("discovery.commandDiscovery.failedToConvertOsStr", path = path.to_string_lossy()).to_string().as_str()).to_lowercase(); let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); + let Some(file_name_lowercase) = path.file_name().and_then(OsStr::to_str).map(|s| s.to_lowercase()) else { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidManifestFileName", resource = path.to_string_lossy()).to_string())); + }; + let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); if DSC_ADAPTED_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { let mut resource = if extension_is_json { match serde_json::from_str::(&contents) { @@ -846,7 +849,7 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result Date: Mon, 2 Feb 2026 16:23:36 -0800 Subject: [PATCH 03/11] test fixes --- .../discover/resources/testDiscoveredOne.dsc.resource.json | 2 +- .../discover/resources/testDiscoveredTwo.dsc.resource.json | 2 +- resources/osinfo/osinfo.dsc.resource.json | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/extensions/test/discover/resources/testDiscoveredOne.dsc.resource.json b/extensions/test/discover/resources/testDiscoveredOne.dsc.resource.json index d564c7337..f07b7c5dc 100644 --- a/extensions/test/discover/resources/testDiscoveredOne.dsc.resource.json +++ b/extensions/test/discover/resources/testDiscoveredOne.dsc.resource.json @@ -21,7 +21,7 @@ "description": "First discovered resource", "type": "object", "required": [], - "additionalProperties": false + "additionalProperties": true } }, "exitCodes": { diff --git a/extensions/test/discover/resources/testDiscoveredTwo.dsc.resource.json b/extensions/test/discover/resources/testDiscoveredTwo.dsc.resource.json index 60ddd2da3..26543f840 100644 --- a/extensions/test/discover/resources/testDiscoveredTwo.dsc.resource.json +++ b/extensions/test/discover/resources/testDiscoveredTwo.dsc.resource.json @@ -21,7 +21,7 @@ "description": "Second discovered resource", "type": "object", "required": [], - "additionalProperties": false + "additionalProperties": true } }, "exitCodes": { diff --git a/resources/osinfo/osinfo.dsc.resource.json b/resources/osinfo/osinfo.dsc.resource.json index b38aabc16..25cb6464d 100644 --- a/resources/osinfo/osinfo.dsc.resource.json +++ b/resources/osinfo/osinfo.dsc.resource.json @@ -36,6 +36,13 @@ "description": "Returns the unique ID for the OSInfo instance data type.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource#id\n", "markdownDescription": "Returns the unique ID for the OSInfo instance data type.\n\n[Online documentation][01]\n\n[01]: https://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource#id\n" }, + "_name": { + "type": "string", + "readOnly": true, + "title": "Instance name", + "description": "Returns the name of the OSInfo instance.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource#name\n", + "markdownDescription": "Returns the name of the OSInfo instance.\n\n[Online documentation][01]\n\n[01]: https://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource#name\n" + }, "architecture": { "type": "string", "title": "Processor architecture", From 787a3b382b1330aebf5107528afd9380142fe6ba Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Feb 2026 16:46:05 -0800 Subject: [PATCH 04/11] fix tests --- dsc/tests/dsc_resource_input.tests.ps1 | 16 +++++++++------- lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/discovery/command_discovery.rs | 2 +- resources/apt/apt.dsc.resource.sh | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/dsc/tests/dsc_resource_input.tests.ps1 b/dsc/tests/dsc_resource_input.tests.ps1 index 74f33bf79..7ecc7340b 100644 --- a/dsc/tests/dsc_resource_input.tests.ps1 +++ b/dsc/tests/dsc_resource_input.tests.ps1 @@ -16,7 +16,7 @@ Describe 'tests for resource input' { "-NonInteractive", "-NoProfile", "-Command", - "\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\"" + "\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": $env:World, `\"Boolean`\": $env:Boolean, `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\"" ] }, "set": { @@ -27,7 +27,7 @@ Describe 'tests for resource input' { "-NonInteractive", "-NoProfile", "-Command", - "\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\"" + "\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": $env:World, `\"Boolean`\": $env:Boolean, `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\"" ], "return": "state", "implementsPretest": true @@ -40,7 +40,7 @@ Describe 'tests for resource input' { "-NonInteractive", "-NoProfile", "-Command", - "\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\"" + "\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": $env:World, `\"Boolean`\": $env:Boolean, `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\"" ] }, "schema": { @@ -66,14 +66,14 @@ Describe 'tests for resource input' { "description": "test" }, "StringArray": { - "type": "array", + "type": ["array", "string"], "description": "test", "items": { "type": "string" } }, "NumberArray": { - "type": "array", + "type": ["array", "string"], "description": "test", "items": { "type": "number" @@ -110,8 +110,10 @@ Describe 'tests for resource input' { } "@ - $result = $json | dsc resource $operation -r Test/EnvVarInput -f - | ConvertFrom-Json - $result.$member.Hello | Should -BeExactly 'foo' + $out = dsc -l trace resource $operation -r Test/EnvVarInput -i $json 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $result = $out | ConvertFrom-Json + $result.$member.Hello | Should -BeExactly 'foo' -Because $out $result.$member.World | Should -Be 2 $result.$member.Boolean | Should -Be 'true' $result.$member.StringArray | Should -BeExactly 'foo,bar' diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 56ef7e0be..7da4c7e14 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -130,6 +130,7 @@ conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean" conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}' for resource '%{resource}'" adaptedMissingRequireAdapter = "Adapted resource manifest '%{resource}' missing required 'requireAdapter' property" adaptedResourcePathNotFound = "Adapted resource '%{resource}' path not found: %{path}" +invalidManifestFileName = "Invalid manifest file name '%{path}'" [dscresources.commandResource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 64f5b3e08..f86317181 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -662,7 +662,7 @@ fn evaluate_condition(condition: Option<&str>) -> Result { pub fn load_manifest(path: &Path) -> Result, DscError> { let contents = read_to_string(path)?; let Some(file_name_lowercase) = path.file_name().and_then(OsStr::to_str).map(|s| s.to_lowercase()) else { - return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidManifestFileName", resource = path.to_string_lossy()).to_string())); + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidManifestFileName", path = path.to_string_lossy()).to_string())); }; let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); if DSC_ADAPTED_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { diff --git a/resources/apt/apt.dsc.resource.sh b/resources/apt/apt.dsc.resource.sh index d2cd5581d..fa95fc499 100644 --- a/resources/apt/apt.dsc.resource.sh +++ b/resources/apt/apt.dsc.resource.sh @@ -25,7 +25,7 @@ get_apt() { echo $line | awk '{ split($0, a, " "); split(a[1], pn, "/"); - printf("{ \"_exist\": \"%s\", \"packageName\": \"%s\", \"version\": \"%s\", \"source\": \"%s\" }\n", ENVIRON["exist"], pn[1], a[2], pn[2]); + printf("{ \"_exist\": %s, \"packageName\": \"%s\", \"version\": \"%s\", \"source\": \"%s\" }\n", ENVIRON["exist"], pn[1], a[2], pn[2]); }' fi done From ca502ff964ef42f36f5cd980247cffaa9995be2d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Feb 2026 16:55:21 -0800 Subject: [PATCH 05/11] fix apt test --- resources/apt/test/apt.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/apt/test/apt.tests.ps1 b/resources/apt/test/apt.tests.ps1 index 9142d2110..5f1dfe06f 100644 --- a/resources/apt/test/apt.tests.ps1 +++ b/resources/apt/test/apt.tests.ps1 @@ -41,7 +41,7 @@ Describe 'Apt resource tests' { $out = dsc config test -f $yamlPath| ConvertFrom-Json -Depth 10 $LASTEXITCODE | Should -Be 0 - $exists = $null -ne (Get-Command pkgName -CommandType Application -ErrorAction Ignore) + $exists = $null -ne (Get-Command $pkgName -CommandType Application -ErrorAction Ignore) $out.results[1].result.inDesiredState | Should -Be $exists } } From b0862dd27c23a5bce590af339020d6ddbc2b7aae Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Feb 2026 18:28:16 -0800 Subject: [PATCH 06/11] fix tests --- dsc/tests/dsc_args.tests.ps1 | 3 --- dsc/tests/dsc_mcp.tests.ps1 | 6 +++--- tools/dsctest/dsctest.dsc.manifests.json | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/dsc/tests/dsc_args.tests.ps1 b/dsc/tests/dsc_args.tests.ps1 index de88b3db2..b51308c37 100644 --- a/dsc/tests/dsc_args.tests.ps1 +++ b/dsc/tests/dsc_args.tests.ps1 @@ -210,9 +210,6 @@ resources: $a_obj = $a[$_] | ConvertFrom-Json $b_obj = $b[$_] | ConvertFrom-Json $a_obj.type | Should -Be $b_obj.type - # adapter-based resources should Not be in the results - $a_obj.requireAdapter | Should -BeNullOrEmpty - $b_obj.requireAdapter | Should -BeNullOrEmpty } } diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 336dcea3b..ab51b52d0 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -164,7 +164,7 @@ Describe 'Tests for MCP server' { } It 'Calling show_dsc_resource works' { - $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) + $resource = (dsc resource list 'Microsoft/OSInfo' | ConvertFrom-Json -Depth 20) $mcpRequest = @{ jsonrpc = "2.0" @@ -227,10 +227,10 @@ Describe 'Tests for MCP server' { $response.id | Should -Be 8 $functions = dsc function list --output-format json | ConvertFrom-Json $response.result.structuredContent.functions.Count | Should -Be $functions.Count - + $mcpFunctions = $response.result.structuredContent.functions | Sort-Object name $dscFunctions = $functions | Sort-Object name - + for ($i = 0; $i -lt $dscFunctions.Count; $i++) { ($mcpFunctions[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 8 $mcpFunctions[$i].name | Should -BeExactly $dscFunctions[$i].name -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 13f8a02ea..b621e4913 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -13,7 +13,7 @@ "description": "An adapted resource for testing.", "author": "DSC Team", "requireAdapter": "Test/Adapter", - "path": "dsctest", + "path": "adaptedTest.dsc.adaptedResource.json", "schema": { "embedded": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -51,7 +51,7 @@ "description": "An adapted resource for testing.", "author": "DSC Team", "requireAdapter": "Test/Adapter", - "path": "dsctest", + "path": "adaptedTest.dsc.adaptedResource.json", "condition": "[false()]", "schema": { "embedded": { From b381c8dcd6cba1afeb34e53e19bbcb105a222134 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Feb 2026 18:40:12 -0800 Subject: [PATCH 07/11] fix test --- dsc/tests/dsc_adapter.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index 47d4980f1..e8cb8035b 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -175,7 +175,7 @@ Describe 'Tests for adapter support' { $out.kind | Should -BeExactly 'resource' $out.capabilities | Should -Be @('get', 'set', 'test', 'export') $parent = (Split-Path -Path (Get-Command dsc).Source -Parent) - $expectedPath = Join-Path -Path $parent -ChildPath 'dsctest' + $expectedPath = Join-Path -Path $parent -ChildPath 'adaptedTest.dsc.adaptedResource.json' $out.path | Should -BeExactly $expectedPath $out.directory | Should -BeExactly $parent $out.requireAdapter | Should -BeExactly 'Test/Adapter' From 6eb784af6312b6c0ebaa32ae33652a294e2c0aa1 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 4 Feb 2026 12:48:00 -0800 Subject: [PATCH 08/11] fix merge issue --- .../src/dscresources/command_resource.rs | 50 ++++++++++++------- .../src/dscresources/resource_manifest.rs | 10 ++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 9a13f117d..1835246db 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -43,8 +43,6 @@ pub fn invoke_get(resource: &DscResource, cwd: &Path, filter: &str, target_resou Some(r) => r.type_name.clone(), None => resource.type_name.clone(), }; - debug!("Resource requires adapter: {:?}", resource.require_adapter); - debug!("Resource path: {:?}", resource.get_directory()?); let path = if let Some(target_resource) = target_resource { Some(target_resource.get_path()?.clone()) } else { @@ -104,6 +102,19 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: }; let operation_type: String; let mut is_synthetic_what_if = false; + let resource_type = match target_resource { + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + let path = if let Some(target_resource) = target_resource { + Some(target_resource.get_path()?.clone()) + } else { + None + }; + let command_resource_info = CommandResourceInfo { + type_name: resource_type.clone(), + path, + }; let set_method = match execution_type { ExecutionKind::Actual => { @@ -113,21 +124,21 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: ExecutionKind::WhatIf => { operation_type = "whatif".to_string(); // Check if set supports native what-if - let has_native_whatif = resource.set.as_ref() + let has_native_whatif = manifest.set.as_ref() .map_or(false, |set| { - let (_, supports_whatif) = process_set_delete_args(set.args.as_ref(), "", &resource.resource_type, execution_type); + let (_, supports_whatif) = process_set_delete_args(set.args.as_ref(), "", &command_resource_info, execution_type); supports_whatif }); if has_native_whatif { - &resource.set + &manifest.set } else { - if resource.what_if.is_some() { - warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource.resource_type)); - &resource.what_if + if manifest.what_if.is_some() { + warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource_type)); + &manifest.what_if } else { is_synthetic_what_if = true; - &resource.set + &manifest.set } } } @@ -220,7 +231,7 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let args = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info); + let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -233,7 +244,6 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: }, } - info!("Invoking {} '{}' using '{}'", operation_type, &resource.type_name, &set.executable); let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env, manifest.exit_codes.as_ref())?; match set.returns { @@ -494,7 +504,7 @@ pub fn invoke_delete(resource: &DscResource, cwd: &Path, filter: &str, target_re type_name: resource_type.clone(), path, }; - let args = process_set_delete_args(delete.args.as_ref(), filter, &command_resource_info, &ExecutionKind::Actual); + let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, &command_resource_info, &ExecutionKind::Actual); let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); @@ -913,7 +923,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo) -> Option> { +pub fn process_get_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -937,9 +947,7 @@ pub fn process_args(args: Option<&Vec>, input: &str, command_resourc processed_args.push(resource_type_arg.clone()); processed_args.push(command_resource_info.type_name.to_string()); }, - ArgKind::ResourcePath { resource_path_arg } => { - debug!("ResourcePath Arg: {:?}", resource_path_arg); - debug!("Command Resource Info Path: {:?}", command_resource_info.path); + GetArgKind::ResourcePath { resource_path_arg } => { if let Some(path) = &command_resource_info.path { processed_args.push(resource_path_arg.clone()); processed_args.push(path.to_string_lossy().to_string()); @@ -961,7 +969,7 @@ pub fn process_args(args: Option<&Vec>, input: &str, command_resourc /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_set_delete_args(args: Option<&Vec>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> (Option>, bool) { +pub fn process_set_delete_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo, execution_type: &ExecutionKind) -> (Option>, bool) { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return (None, false); @@ -982,9 +990,15 @@ pub fn process_set_delete_args(args: Option<&Vec>, input: &str processed_args.push(json_input_arg.clone()); processed_args.push(input.to_string()); }, + SetDeleteArgKind::ResourcePath { resource_path_arg } => { + if let Some(path) = &command_resource_info.path { + processed_args.push(resource_path_arg.clone()); + processed_args.push(path.to_string_lossy().to_string()); + } + }, SetDeleteArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(resource_type.to_string()); + processed_args.push(command_resource_info.type_name.to_string()); }, SetDeleteArgKind::WhatIf { what_if_arg } => { supports_whatif = true; diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index d299933f0..aeb35b814 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -117,7 +117,6 @@ pub enum GetArgKind { #[serde(rename = "resourceTypeArg")] resource_type_arg: String, }, -<<<<<<< HEAD } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -134,6 +133,11 @@ pub enum SetDeleteArgKind { /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. mandatory: Option, }, + ResourcePath { + /// The argument that accepts the resource path. + #[serde(rename = "resourcePathArg")] + resource_path_arg: String, + }, ResourceType { /// The argument that accepts the resource type name. #[serde(rename = "resourceTypeArg")] @@ -145,10 +149,6 @@ pub enum SetDeleteArgKind { #[serde(rename = "whatIfArg")] what_if_arg: String, } -||||||| parent of 89f901ff (Enable support for adapted resource manifests) - } -======= ->>>>>>> 89f901ff (Enable support for adapted resource manifests) } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] From 09b59fae9a38e7fab9ab58f59177b7ae13603675 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 4 Feb 2026 14:37:40 -0800 Subject: [PATCH 09/11] Separate adapted resource manifest into own schema --- lib/dsc-lib/locales/en-us.toml | 3 +- .../src/discovery/command_discovery.rs | 94 ++++++++------- .../dscresources/adapted_resource_manifest.rs | 57 +++++++++ .../src/dscresources/command_resource.rs | 88 +++++++------- lib/dsc-lib/src/dscresources/dscresource.rs | 55 +++------ lib/dsc-lib/src/dscresources/mod.rs | 1 + .../adaptedTest.dsc.adaptedResource.json | 1 + tools/dsctest/dsctest.dsc.manifests.json | 2 + tools/test_group_resource/src/main.rs | 109 +++++++++--------- 9 files changed, 233 insertions(+), 177 deletions(-) create mode 100644 lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 7da4c7e14..8fb58b2da 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -128,7 +128,6 @@ resourceMissingRequireAdapter = "Resource '%{resource}' is missing 'require_adap extensionDiscoverFailed = "Extension '%{extension}' failed to discover resources: %{error}" conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean" conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}' for resource '%{resource}'" -adaptedMissingRequireAdapter = "Adapted resource manifest '%{resource}' missing required 'requireAdapter' property" adaptedResourcePathNotFound = "Adapted resource '%{resource}' path not found: %{path}" invalidManifestFileName = "Invalid manifest file name '%{path}'" @@ -225,6 +224,8 @@ validatingAgainstSchema = "Validating against resource schema" [dscresources.resource_manifest] resourceManifestSchemaTitle = "Resource manifest schema URI" resourceManifestSchemaDescription = "Defines the JSON Schema the resource manifest adheres to." +adaptedResourceManifestSchemaTitle = "Adapted resource manifest schema URI" +adaptedResourceManifestSchemaDescription = "Defines the JSON Schema the adapted resource manifest adheres to." [extensions.dscextension] discoverNoResults = "No results returned for discovery extension '%{extension}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index f86317181..50eb15991 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{discovery::{discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, matches_adapter_requirement}, parser::Statement}; +use crate::{discovery::{discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, matches_adapter_requirement}, dscresources::adapted_resource_manifest::AdaptedDscResourceManifest, parser::Statement}; use crate::{locked_clear, locked_is_empty, locked_extend, locked_clone, locked_get}; use crate::configure::{config_doc::ResourceDiscoveryMode, context::Context}; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; @@ -43,7 +43,7 @@ static ADAPTED_RESOURCES: LazyLock>>> = #[derive(Deserialize, JsonSchema)] pub struct ManifestList { #[serde(rename = "adaptedResources")] - pub adapted_resources: Option>, + pub adapted_resources: Option>, pub resources: Option>, pub extensions: Option>, } @@ -313,12 +313,6 @@ impl ResourceDiscovery for CommandDiscovery { } if let Some(_adapter) = &resource.require_adapter { trace!("{}", t!("discovery.commandDiscovery.adaptedResourceFound", resource = resource.type_name, version = resource.version)); - let mut resource = resource.clone(); - let mut directory = path.clone(); - directory.pop(); - let resource_path = directory.join(resource.get_path()?.clone()); - resource.set_path(resource_path); - resource.set_directory(directory); insert_resource(&mut resources, &resource); } } @@ -417,7 +411,7 @@ impl ResourceDiscovery for CommandDiscovery { let mut adapter_resources_count = 0; // invoke the list command let list_command = &manifest.adapter.clone().unwrap().list; - let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.get_directory()?), None, manifest.exit_codes.as_ref()) + let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.directory), None, manifest.exit_codes.as_ref()) { Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), Err(e) => { @@ -666,35 +660,26 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { }; let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); if DSC_ADAPTED_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { - let mut resource = if extension_is_json { - match serde_json::from_str::(&contents) { + let resource = if extension_is_json { + match serde_json::from_str::(&contents) { Ok(resource) => resource, Err(err) => { return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidAdaptedResourceManifest", resource = path.to_string_lossy(), err = err).to_string())); } } } else { - match serde_yaml::from_str::(&contents) { + match serde_yaml::from_str::(&contents) { Ok(resource) => resource, Err(err) => { return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidAdaptedResourceManifest", resource = path.to_string_lossy(), err = err).to_string())); } } }; - if resource.require_adapter.is_none() { - return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedMissingRequireAdapter", resource = path.to_string_lossy()).to_string())); - } - let directory = path.parent().unwrap(); - let resource_path = directory.join(resource.get_path()?.clone()); - if !resource_path.exists() { - return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = resource.type_name).to_string())); - } if !evaluate_condition(resource.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = resource_path.to_string_lossy(), condition = resource.condition.unwrap_or_default(), resource = resource.type_name)); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = resource.condition.unwrap_or_default(), resource = resource.type_name)); return Ok(vec![]); } - resource.set_path(resource_path); - resource.set_directory(directory.to_path_buf()); + let resource = load_adapted_resource_manifest(&path, &resource)?; return Ok(vec![ImportedManifest::Resource(resource)]); } if DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { @@ -762,20 +747,12 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { }; if let Some(adapted_resources) = &manifest_list.adapted_resources { for resource in adapted_resources { - let directory = path.parent().unwrap(); - let resource_path = directory.join(resource.get_path()?); - if !resource_path.exists() { - warn!("{}", t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = resource.type_name).to_string()); - continue; - } if !evaluate_condition(resource.condition.as_deref())? { debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = resource.condition.as_ref() : {:?}, resource = resource.type_name)); continue; } - let mut resource = resource.clone(); - resource.set_path(resource_path); - resource.set_directory(directory.to_path_buf()); - resources.push(ImportedManifest::Resource(resource.clone())); + let resource = load_adapted_resource_manifest(&path, resource)?; + resources.push(ImportedManifest::Resource(resource)); } } if let Some(resource_manifests) = &manifest_list.resources { @@ -803,6 +780,35 @@ 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 { + if let Err(err) = validate_semver(&manifest.version) { + warn!("{}", t!("discovery.commandDiscovery.invalidManifestVersion", path = path.to_string_lossy(), err = err).to_string()); + } + + let directory = path.parent().unwrap(); + let resource_path = directory.join(&manifest.path); + if !resource_path.exists() { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = manifest.type_name).to_string())); + } + + let resource = DscResource { + type_name: manifest.type_name.clone(), + kind: Kind::Resource, + implemented_as: None, + description: manifest.description.clone(), + version: manifest.version.clone(), + capabilities: manifest.capabilities.clone(), + require_adapter: Some(manifest.require_adapter.clone()), + path: resource_path, + directory: directory.to_path_buf(), + manifest: None, + schema: Some(manifest.schema.clone()), + ..Default::default() + }; + + Ok(resource) +} + fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result { if let Err(err) = validate_semver(&manifest.version) { warn!("{}", t!("discovery.commandDiscovery.invalidManifestVersion", path = path.to_string_lossy(), err = err).to_string()); @@ -848,16 +854,18 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result, + /// An optional condition for the resource to be active. + pub condition: Option, + /// The file path to the resource. + pub path: PathBuf, + /// The description of the resource. + pub description: Option, + /// The author of the resource. + pub author: Option, + /// The required resource adapter for the resource. + #[serde(rename="requireAdapter")] + pub require_adapter: FullyQualifiedTypeName, + /// The JSON Schema of the resource. + pub schema: Map, +} diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 1835246db..ea5f704c7 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -30,7 +30,7 @@ pub struct CommandResourceInfo { /// # Errors /// /// Error returned if the resource does not successfully get the current state -pub fn invoke_get(resource: &DscResource, cwd: &Path, filter: &str, target_resource: Option<&DscResource>) -> Result { +pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option<&DscResource>) -> Result { debug!("{}", t!("dscresources.commandResource.invokeGet", resource = &resource.type_name)); let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); @@ -44,7 +44,7 @@ pub fn invoke_get(resource: &DscResource, cwd: &Path, filter: &str, target_resou None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -54,15 +54,15 @@ pub fn invoke_get(resource: &DscResource, cwd: &Path, filter: &str, target_resou }; let args = process_get_args(get.args.as_ref(), filter, &command_resource_info); if !filter.is_empty() { - verify_json_from_manifest(&resource, cwd, filter)?; + verify_json_from_manifest(&resource, filter)?; command_input = get_command_input(get.input.as_ref(), filter)?; } info!("{}", t!("dscresources.commandResource.invokeGetUsing", resource = &resource.type_name, executable = &get.executable)); - let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.verifyOutputUsing", resource = &resource.type_name, executable = &get.executable)); - verify_json_from_manifest(&resource, cwd, &stdout)?; + verify_json_from_manifest(&resource, &stdout)?; } let result: GetResult = if let Ok(group_response) = serde_json::from_str::>(&stdout) { @@ -95,7 +95,7 @@ pub fn invoke_get(resource: &DscResource, cwd: &Path, filter: &str, target_resou /// /// Error returned if the resource does not successfully set the desired state #[allow(clippy::too_many_lines)] -pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option<&DscResource>) -> Result { +pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option<&DscResource>) -> Result { debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.type_name)); let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); @@ -107,7 +107,7 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -146,12 +146,12 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: let Some(set) = set_method.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; - verify_json_from_manifest(&resource, cwd, desired)?; + verify_json_from_manifest(&resource, desired)?; // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && set.pre_test != Some(true) { info!("{}", t!("dscresources.commandResource.noPretest", resource = &resource.type_name)); - let test_result = invoke_test(resource, cwd, desired, target_resource)?; + let test_result = invoke_test(resource, desired, target_resource)?; if is_synthetic_what_if { return Ok(test_result.into()); } @@ -191,7 +191,7 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -203,11 +203,11 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.type_name, executable = &get.executable)); - let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.setVerifyGet", resource = &resource.type_name, executable = &get.executable)); - verify_json_from_manifest(&resource, cwd, &stdout)?; + verify_json_from_manifest(&resource, &stdout)?; } let pre_state_value: Value = if exit_code == 0 { @@ -244,14 +244,14 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: }, } - let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), env, manifest.exit_codes.as_ref())?; match set.returns { Some(ReturnKind::State) => { if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.type_name, executable = &set.executable)); - verify_json_from_manifest(&resource, cwd, &stdout)?; + verify_json_from_manifest(&resource, &stdout)?; } let actual_value: Value = match serde_json::from_str(&stdout){ @@ -290,7 +290,7 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: }, None => { // perform a get and compare the result to the expected state - let get_result = invoke_get(resource, cwd, desired, target_resource)?; + let get_result = invoke_get(resource, desired, target_resource)?; // for changed_properties, we compare post state to pre state let actual_state = match get_result { GetResult::Group(results) => { @@ -325,24 +325,24 @@ pub fn invoke_set(resource: &DscResource, cwd: &Path, desired: &str, skip_test: /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_test(resource: &DscResource, cwd: &Path, expected: &str, target_resource: Option<&DscResource>) -> Result { +pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Option<&DscResource>) -> Result { debug!("{}", t!("dscresources.commandResource.invokeTest", resource = &resource.type_name)); let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; let Some(test) = &manifest.test else { info!("{}", t!("dscresources.commandResource.testSyntheticTest", resource = &resource.type_name)); - return invoke_synthetic_test(resource, cwd, expected, target_resource); + return invoke_synthetic_test(resource, expected, target_resource); }; - verify_json_from_manifest(&resource, cwd, expected)?; + verify_json_from_manifest(&resource, expected)?; let resource_type = match target_resource.clone() { Some(r) => r.type_name.clone(), None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -354,11 +354,11 @@ pub fn invoke_test(resource: &DscResource, cwd: &Path, expected: &str, target_re let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.type_name, executable = &test.executable)); - let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &resource.type_name, executable = &test.executable)); - verify_json_from_manifest(&resource, cwd, &stdout)?; + verify_json_from_manifest(&resource, &stdout)?; } if resource.kind == Kind::Importer { @@ -408,7 +408,7 @@ pub fn invoke_test(resource: &DscResource, cwd: &Path, expected: &str, target_re }, None => { // perform a get and compare the result to the expected state - let get_result = invoke_get(resource, cwd, expected, target_resource)?; + let get_result = invoke_get(resource, expected, target_resource)?; let actual_state = match get_result { GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); @@ -446,7 +446,7 @@ fn get_desired_state(actual: &Value) -> Result, DscError> { Ok(in_desired_state) } -fn invoke_synthetic_test(resource: &DscResource, cwd: &Path, expected: &str, target_resource: Option<&DscResource>) -> Result { let get_result = invoke_get(resource, cwd, expected, target_resource)?; +fn invoke_synthetic_test(resource: &DscResource, expected: &str, target_resource: Option<&DscResource>) -> Result { let get_result = invoke_get(resource, expected, target_resource)?; let actual_state = match get_result { GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); @@ -481,7 +481,7 @@ fn invoke_synthetic_test(resource: &DscResource, cwd: &Path, expected: &str, tar /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_delete(resource: &DscResource, cwd: &Path, filter: &str, target_resource: Option<&DscResource>) -> Result<(), DscError> { +pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Option<&DscResource>) -> Result<(), DscError> { let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; @@ -489,14 +489,14 @@ pub fn invoke_delete(resource: &DscResource, cwd: &Path, filter: &str, target_re return Err(DscError::NotImplemented("delete".to_string())); }; - verify_json_from_manifest(&resource, cwd, filter)?; + verify_json_from_manifest(&resource, filter)?; let resource_type = match target_resource { Some(r) => r.type_name.clone(), None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -508,7 +508,7 @@ pub fn invoke_delete(resource: &DscResource, cwd: &Path, filter: &str, target_re let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); - let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; Ok(()) } @@ -528,7 +528,7 @@ pub fn invoke_delete(resource: &DscResource, cwd: &Path, filter: &str, target_re /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_validate(resource: &DscResource, cwd: &Path, config: &str, target_resource: Option<&DscResource>) -> Result { +pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Option<&DscResource>) -> Result { trace!("{}", t!("dscresources.commandResource.invokeValidateConfig", resource = &resource.type_name, config = &config)); let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); @@ -543,7 +543,7 @@ pub fn invoke_validate(resource: &DscResource, cwd: &Path, config: &str, target_ None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -555,7 +555,7 @@ pub fn invoke_validate(resource: &DscResource, cwd: &Path, config: &str, target_ let command_input = get_command_input(validate.input.as_ref(), config)?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) } @@ -569,7 +569,7 @@ pub fn invoke_validate(resource: &DscResource, cwd: &Path, config: &str, target_ /// # Errors /// /// Error if schema is not available or if there is an error getting the schema -pub fn get_schema(resource: &DscResource, cwd: &Path) -> Result { +pub fn get_schema(resource: &DscResource) -> Result { let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; @@ -579,7 +579,7 @@ pub fn get_schema(resource: &DscResource, cwd: &Path) -> Result { - let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?; Ok(stdout) }, SchemaKind::Embedded(ref schema) => { @@ -604,7 +604,7 @@ pub fn get_schema(resource: &DscResource, cwd: &Path) -> Result, target_resource: Option<&DscResource>) -> Result { +pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resource: Option<&DscResource>) -> Result { let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; @@ -612,7 +612,7 @@ pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, ta // see if get is supported and use that instead if manifest.get.is_some() { info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.type_name)); - let get_result = invoke_get(resource, cwd, input.unwrap_or(""), target_resource)?; + let get_result = invoke_get(resource, input.unwrap_or(""), target_resource)?; let mut instances: Vec = Vec::new(); match get_result { GetResult::Group(group_response) => { @@ -639,7 +639,7 @@ pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, ta None => resource.type_name.clone(), }; let path = if let Some(target_resource) = target_resource { - Some(target_resource.get_path()?.clone()) + Some(target_resource.path.clone()) } else { None }; @@ -649,7 +649,7 @@ pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, ta }; if let Some(input) = input { if !input.is_empty() { - verify_json_from_manifest(&resource, cwd, input)?; + verify_json_from_manifest(&resource, input)?; command_input = get_command_input(export.input.as_ref(), input)?; } @@ -659,7 +659,7 @@ pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, ta args = process_get_args(export.args.as_ref(), "", &command_resource_info); } - let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let mut instances: Vec = Vec::new(); for line in stdout.lines() { @@ -671,7 +671,7 @@ pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, ta }; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.exportVerifyOutput", resource = &resource.type_name, executable = &export.executable)); - verify_json_from_manifest(&resource, cwd, line)?; + verify_json_from_manifest(&resource, line)?; } instances.push(instance); } @@ -696,7 +696,7 @@ pub fn invoke_export(resource: &DscResource, cwd: &Path, input: Option<&str>, ta /// # Errors /// /// Error returned if the resource does not successfully resolve the input -pub fn invoke_resolve(resource: &DscResource, cwd: &Path, input: &str) -> Result { +pub fn invoke_resolve(resource: &DscResource, input: &str) -> Result { let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; @@ -706,14 +706,14 @@ pub fn invoke_resolve(resource: &DscResource, cwd: &Path, input: &str) -> Result let command_resource_info = CommandResourceInfo { type_name: resource.type_name.clone(), - path: if resource.require_adapter.is_some() { Some(resource.get_path()?.clone()) } else { None }, + path: if resource.require_adapter.is_some() { Some(resource.path.clone()) } else { None }, }; let args = process_get_args(resolve.args.as_ref(), input, &command_resource_info); let command_input = get_command_input(resolve.input.as_ref(), input)?; info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.type_name, executable = &resolve.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result: ResolveResult = serde_json::from_str(&stdout)?; Ok(result) } @@ -1041,7 +1041,7 @@ fn get_command_input(input_kind: Option<&InputKind>, input: &str) -> Result Result<(), DscError> { +fn verify_json_from_manifest(resource: &DscResource, json: &str) -> Result<(), DscError> { debug!("{}", t!("dscresources.commandResource.verifyJson", resource = resource.type_name)); let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); @@ -1050,7 +1050,7 @@ fn verify_json_from_manifest(resource: &DscResource, cwd: &Path, json: &str) -> // see if resource implements validate if manifest.validate.is_some() { trace!("{}", t!("dscresources.commandResource.validateJson", json = json)); - let result = invoke_validate(resource, cwd, json, None)?; + let result = invoke_validate(resource, json, None)?; if result.valid { return Ok(()); } @@ -1059,7 +1059,7 @@ fn verify_json_from_manifest(resource: &DscResource, cwd: &Path, json: &str) -> } // otherwise, use schema validation - let schema = get_schema(resource, cwd)?; + let schema = get_schema(resource)?; let schema: Value = serde_json::from_str(&schema)?; let compiled_schema = match Validator::new(&schema) { Ok(schema) => schema, diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index 6b848c57a..3c20c80cd 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -39,14 +39,12 @@ pub struct DscResource { pub version: String, /// The capabilities of the resource. pub capabilities: Vec, - /// An optional condition for the resource to be active. - pub condition: Option, /// The file path to the resource. - path: Option, + pub path: PathBuf, /// The description of the resource. pub description: Option, // The directory path to the resource. - directory: Option, + pub directory: PathBuf, /// The implementation of the resource. #[serde(rename="implementedAs")] pub implemented_as: Option, @@ -105,10 +103,9 @@ impl DscResource { kind: Kind::Resource, version: String::new(), capabilities: Vec::new(), - condition: None, description: None, - path: None, - directory: None, + path: PathBuf::new(), + directory: PathBuf::new(), implemented_as: Some(ImplementedAs::Command), author: None, properties: None, @@ -119,28 +116,6 @@ impl DscResource { } } - pub fn get_path(&self) -> Result<&PathBuf, DscError> { - match &self.path { - Some(path) => Ok(path), - None => Err(DscError::ResourceMissingPath(self.type_name.to_string())), - } - } - - pub fn set_path(&mut self, path: PathBuf) { - self.path = Some(path); - } - - pub fn get_directory(&self) -> Result<&PathBuf, DscError> { - match &self.directory { - Some(directory) => Ok(directory), - None => Err(DscError::ResourceMissingDirectory(self.type_name.to_string())), - } - } - - pub fn set_directory(&mut self, directory: PathBuf) { - self.directory = Some(directory); - } - fn create_config_for_adapter(self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { // create new configuration with adapter and use this as the resource let mut configuration = Configuration::new(); @@ -311,6 +286,12 @@ impl DscResource { } } +impl Default for DscResource { + fn default() -> Self { + DscResource::new() + } +} + /// The interface for a DSC resource. pub trait Invoke { /// Invoke the get operation on the resource. @@ -408,7 +389,7 @@ impl Invoke for DscResource { match &self.implemented_as { Some(ImplementedAs::Command) => { - command_resource::invoke_get(&self, &self.get_directory()?, filter, self.target_resource.as_deref()) + command_resource::invoke_get(&self, filter, self.target_resource.as_deref()) }, _ => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -424,7 +405,7 @@ impl Invoke for DscResource { match &self.implemented_as { Some(ImplementedAs::Command) => { - command_resource::invoke_set(&self, &self.get_directory()?, desired, skip_test, execution_type, self.target_resource.as_deref()) + command_resource::invoke_set(&self, desired, skip_test, execution_type, self.target_resource.as_deref()) }, _ => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -471,7 +452,7 @@ impl Invoke for DscResource { Ok(test_result) } else { - command_resource::invoke_test(&self, &self.get_directory()?, expected, self.target_resource.as_deref()) + command_resource::invoke_test(&self, expected, self.target_resource.as_deref()) } }, _ => { @@ -488,7 +469,7 @@ impl Invoke for DscResource { match &self.implemented_as { Some(ImplementedAs::Command) => { - command_resource::invoke_delete(&self, &self.get_directory()?, filter, self.target_resource.as_deref()) + command_resource::invoke_delete(&self, filter, self.target_resource.as_deref()) }, _ => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -504,7 +485,7 @@ impl Invoke for DscResource { match &self.implemented_as { Some(ImplementedAs::Command) => { - command_resource::invoke_validate(&self, &self.get_directory()?, config, self.target_resource.as_deref()) + command_resource::invoke_validate(&self, config, self.target_resource.as_deref()) }, _ => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -520,7 +501,7 @@ impl Invoke for DscResource { match &self.implemented_as { Some(ImplementedAs::Command) => { - command_resource::get_schema(&self, &self.get_directory()?) + command_resource::get_schema(&self) }, _ => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -534,7 +515,7 @@ impl Invoke for DscResource { return self.invoke_export_with_adapter(adapter, &self, input); } - command_resource::invoke_export(&self, &self.get_directory()?, Some(input), self.target_resource.as_deref()) + command_resource::invoke_export(&self, Some(input), self.target_resource.as_deref()) } fn resolve(&self, input: &str) -> Result { @@ -543,7 +524,7 @@ impl Invoke for DscResource { return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeResolveNotSupported", resource = self.type_name).to_string())); } - command_resource::invoke_resolve(&self, &self.get_directory()?, input) + command_resource::invoke_resolve(&self, input) } } diff --git a/lib/dsc-lib/src/dscresources/mod.rs b/lib/dsc-lib/src/dscresources/mod.rs index e3a9e6896..95c2a76ca 100644 --- a/lib/dsc-lib/src/dscresources/mod.rs +++ b/lib/dsc-lib/src/dscresources/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +pub mod adapted_resource_manifest; pub mod command_resource; pub mod dscresource; pub mod invoke_result; diff --git a/tools/dsctest/adaptedTest.dsc.adaptedResource.json b/tools/dsctest/adaptedTest.dsc.adaptedResource.json index 46fa470dd..61839507d 100644 --- a/tools/dsctest/adaptedTest.dsc.adaptedResource.json +++ b/tools/dsctest/adaptedTest.dsc.adaptedResource.json @@ -1,4 +1,5 @@ { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", "type": "Adapted/Three", "kind": "resource", "version": "1.0.0", diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index b621e4913..6ffede0ac 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -1,6 +1,7 @@ { "adaptedResources": [ { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", "type": "Adapted/Two", "kind": "resource", "version": "1.0.0", @@ -39,6 +40,7 @@ } }, { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", "type": "Adapted/Four", "kind": "resource", "version": "1.0.0", diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index 77d0e21bd..219357038 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -3,33 +3,35 @@ mod args; -use std::path::PathBuf; - use args::{Args, SubCommand}; use clap::Parser; -use dsc_lib::dscresources::resource_manifest::{ResourceManifest, GetMethod, Kind}; use dsc_lib::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; -use dsc_lib::schemas::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}; -use dsc_lib::types::FullyQualifiedTypeName; +use dsc_lib::dscresources::resource_manifest::{GetMethod, Kind, ResourceManifest}; +use dsc_lib::schemas::dsc_repo::DscRepoSchema; +use std::path::PathBuf; fn main() { let args = Args::parse(); match args.subcommand { SubCommand::List => { - let mut resource1 = DscResource::new(); - resource1.type_name = FullyQualifiedTypeName::new("Test/TestResource1").unwrap(); - resource1.kind = Kind::Resource; - resource1.version = "1.0.0".to_string(); - resource1.capabilities = vec![Capability::Get, Capability::Set]; - resource1.description = Some("This is a test resource.".to_string()); - resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); - resource1.author = Some("Microsoft".to_string()); - resource1.require_adapter = Some(FullyQualifiedTypeName::new("Test/TestGroup").unwrap()); - resource1.target_resource = None; - resource1.manifest = Some(ResourceManifest { + let resource1 = DscResource { + type_name: "Test/TestResource1".parse().unwrap(), + kind: Kind::Resource, + version: "1.0.0".to_string(), + capabilities: vec![Capability::Get, Capability::Set], description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), - resource_type: FullyQualifiedTypeName::new("Test/TestResource1").unwrap(), + implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), + path: PathBuf::from("test_resource1"), + directory: PathBuf::from("test_directory"), + author: Some("Microsoft".to_string()), + properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), + require_adapter: Some("Test/TestGroup".parse().unwrap()), + target_resource: None, + schema: None, + manifest: Some(ResourceManifest { + description: Some("This is a test resource.".to_string()), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), + resource_type: "Test/TestResource1".parse().unwrap(), kind: Some(Kind::Resource), version: "1.0.0".to_string(), get: Some(GetMethod { @@ -37,24 +39,26 @@ fn main() { ..Default::default() }), ..Default::default() - } - ); - resource1.set_path(PathBuf::from("test_resource1")); - resource1.set_directory(PathBuf::from("test_directory")); - let mut resource2 = DscResource::new(); - resource2.type_name = FullyQualifiedTypeName::new("Test/TestResource2").unwrap(); - resource2.kind = Kind::Resource; - resource2.version = "1.0.1".to_string(); - resource2.capabilities = vec![Capability::Get, Capability::Set]; - resource2.description = Some("This is a test resource.".to_string()); - resource2.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); - resource2.author = Some("Microsoft".to_string()); - resource2.require_adapter = Some(FullyQualifiedTypeName::new("Test/TestGroup").unwrap()); - resource2.target_resource = None; - resource2.manifest = Some(ResourceManifest { + }), + }; + let resource2 = DscResource { + type_name: "Test/TestResource2".parse().unwrap(), + kind: Kind::Resource, + version: "1.0.1".to_string(), + capabilities: vec![Capability::Get, Capability::Set], + description: Some("This is a test resource.".to_string()), + implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), + path: PathBuf::from("test_resource2"), + directory: PathBuf::from("test_directory"), + author: Some("Microsoft".to_string()), + properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), + require_adapter: Some("Test/TestGroup".parse().unwrap()), + target_resource: None, + schema: None, + manifest: Some(ResourceManifest { description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), - resource_type: FullyQualifiedTypeName::new("Test/TestResource2").unwrap(), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), + resource_type: "Test/TestResource2".parse().unwrap(), kind: Some(Kind::Resource), version: "1.0.1".to_string(), get: Some(GetMethod { @@ -62,27 +66,28 @@ fn main() { ..Default::default() }), ..Default::default() - } - ); - resource2.set_path(PathBuf::from("test_resource2")); - resource2.set_directory(PathBuf::from("test_directory")); + }), + }; println!("{}", serde_json::to_string(&resource1).unwrap()); println!("{}", serde_json::to_string(&resource2).unwrap()); }, SubCommand::ListMissingRequires => { - let mut resource1 = DscResource::new(); - resource1.type_name = FullyQualifiedTypeName::new("InvalidResource").unwrap(); - resource1.kind = Kind::Resource; - resource1.version = "1.0.0".to_string(); - resource1.capabilities = vec![Capability::Get]; - resource1.description = Some("This is a test resource.".to_string()); - resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); - resource1.author = Some("Microsoft".to_string()); - resource1.require_adapter = None; - resource1.target_resource = None; - resource1.manifest = None; - resource1.set_path(PathBuf::from("test_resource1")); - resource1.set_directory(PathBuf::from("test_directory")); + let resource1 = DscResource { + type_name: "Test/InvalidResource".parse().unwrap(), + kind: Kind::Resource, + version: "1.0.0".to_string(), + capabilities: vec![Capability::Get], + description: Some("This is a test resource.".to_string()), + implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), + path: PathBuf::from("test_resource1"), + directory: PathBuf::from("test_directory"), + author: Some("Microsoft".to_string()), + properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), + require_adapter: None, + target_resource: None, + manifest: None, + schema: None, + }; println!("{}", serde_json::to_string(&resource1).unwrap()); } } From 13519eb3fcc30035d76b55b270bbfefd36a15b3d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 5 Feb 2026 16:26:42 -0800 Subject: [PATCH 10/11] Update lib/dsc-lib/src/dscresources/command_resource.rs Co-authored-by: Tess Gauthier --- lib/dsc-lib/src/dscresources/command_resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index ea5f704c7..48f454e75 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -1048,7 +1048,7 @@ fn verify_json_from_manifest(resource: &DscResource, json: &str) -> Result<(), D }; // see if resource implements validate - if manifest.validate.is_some() { + if manifest.validate.is_some() { trace!("{}", t!("dscresources.commandResource.validateJson", json = json)); let result = invoke_validate(resource, json, None)?; if result.valid { From 9e43b0998c9d064456148a8af54a4feb8bafe3cf Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 5 Feb 2026 16:29:00 -0800 Subject: [PATCH 11/11] fix title/description of test resource --- tools/dsctest/adaptedTest.dsc.adaptedResource.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/dsctest/adaptedTest.dsc.adaptedResource.json b/tools/dsctest/adaptedTest.dsc.adaptedResource.json index 61839507d..f674d5f2d 100644 --- a/tools/dsctest/adaptedTest.dsc.adaptedResource.json +++ b/tools/dsctest/adaptedTest.dsc.adaptedResource.json @@ -16,9 +16,9 @@ "schema": { "embedded": { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Microsoft/OSInfo/v0.1.0/schema.json", - "title": "OsInfo", - "description": "Returns information about the operating system.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource\n", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Adapted/Three/v0.1.0/schema.json", + "title": "Adapted/Three", + "description": "An adapted resource for testing.", "type": "object", "required": [], "additionalProperties": false,