diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index 63381dc3c..25508b0db 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -179,7 +179,7 @@ Describe 'Tests for adapter support' { $out.path | Should -BeExactly $expectedPath $out.directory | Should -BeExactly $parent $out.requireAdapter | Should -BeExactly 'Test/Adapter' - $out.schema.embedded | Should -Not -BeNullOrEmpty + $out.schema | Should -Not -BeNullOrEmpty } It 'Adapted resource with condition false should not be returned' { diff --git a/dsc/tests/dsc_resource_schema.tests.ps1 b/dsc/tests/dsc_resource_schema.tests.ps1 index e40b78d36..91cc8942b 100644 --- a/dsc/tests/dsc_resource_schema.tests.ps1 +++ b/dsc/tests/dsc_resource_schema.tests.ps1 @@ -27,4 +27,28 @@ Describe 'tests for the Resource schema within a configuration' { $schema.title | Should -BeExactly 'Resource' $schema.additionalProperties | Should -Be $false } + + It 'adapted resource manifest can return schema' { + $schema = dsc resource schema -r Adapted/Two | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 + $schema.'$id' | Should -BeExactly 'https://github.com/powershell/dsc' + $schema.title | Should -BeExactly 'AdaptedTwo' + $schema.description | Should -BeExactly 'Adapted test resource number two' + $schema.type | Should -BeExactly 'object' + $schema.additionalProperties | Should -Be $false + $schema.properties.two.type | Should -BeExactly 'string' + $schema.properties.name.title | Should -BeExactly 'Name' + } + + It 'resource schema can be returned from adapter' { + $schema = dsc resource schema -r Adapted/One | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 + $schema.title | Should -BeExactly 'AdaptedOne' + $schema.type | Should -BeExactly 'object' + $schema.properties.one.type | Should -BeExactly 'string' + $schema.properties._name.type | Should -BeExactly @('string', 'null') + $schema.properties.path.type | Should -BeExactly @('string', 'null') + $schema.additionalProperties | Should -Be $false + $schema.required | Should -BeExactly @('one') + } } diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 24b6be91a..b2906c5bc 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -7,7 +7,7 @@ use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; use std::{collections::HashMap, env, path::{Path, PathBuf}, process::Stdio}; -use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; +use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; use crate::dscerror::DscError; use super::{ dscresource::{get_diff, redact, DscResource}, @@ -64,7 +64,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< }; let args = process_get_args(get.args.as_ref(), filter, &command_resource_info); if !filter.is_empty() { - verify_json_from_manifest(&resource, filter)?; + verify_json_from_manifest(&resource, filter, target_resource)?; command_input = get_command_input(get.input.as_ref(), filter)?; } @@ -72,7 +72,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< 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, &stdout)?; + verify_json_from_manifest(&resource, &stdout, target_resource)?; } let result: GetResult = if let Ok(group_response) = serde_json::from_str::>(&stdout) { @@ -156,7 +156,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let Some(set) = set_method.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; - verify_json_from_manifest(&resource, desired)?; + verify_json_from_manifest(&resource, desired, target_resource)?; // 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) { @@ -217,7 +217,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.setVerifyGet", resource = &resource.type_name, executable = &get.executable)); - verify_json_from_manifest(&resource, &stdout)?; + verify_json_from_manifest(&resource, &stdout, target_resource)?; } let pre_state_value: Value = if exit_code == 0 { @@ -261,7 +261,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut 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, &stdout)?; + verify_json_from_manifest(&resource, &stdout, target_resource)?; } let actual_value: Value = match serde_json::from_str(&stdout){ @@ -345,7 +345,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti return invoke_synthetic_test(resource, expected, target_resource); }; - verify_json_from_manifest(&resource, expected)?; + verify_json_from_manifest(&resource, expected, target_resource)?; let resource_type = match target_resource.clone() { Some(r) => r.type_name.clone(), @@ -368,7 +368,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &resource.type_name, executable = &test.executable)); - verify_json_from_manifest(&resource, &stdout)?; + verify_json_from_manifest(&resource, &stdout, target_resource)?; } if resource.kind == Kind::Importer { @@ -499,7 +499,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti return Err(DscError::NotImplemented("delete".to_string())); }; - verify_json_from_manifest(&resource, filter)?; + verify_json_from_manifest(&resource, filter, target_resource)?; let resource_type = match target_resource { Some(r) => r.type_name.clone(), @@ -589,7 +589,7 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op /// # Errors /// /// Error if schema is not available or if there is an error getting the schema -pub fn get_schema(resource: &DscResource) -> Result { +pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) -> Result { let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; @@ -599,7 +599,12 @@ pub fn get_schema(resource: &DscResource) -> Result { match schema_kind { SchemaKind::Command(ref command) => { - let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?; + let resource_type = match target_resource { + Some(r) => r.type_name.clone(), + None => resource.type_name.clone(), + }; + let args = process_schema_args(command.args.as_ref(), &CommandResourceInfo { type_name: resource_type, path: None }); + let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?; Ok(stdout) }, SchemaKind::Embedded(ref schema) => { @@ -669,7 +674,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc }; if let Some(input) = input { if !input.is_empty() { - verify_json_from_manifest(&resource, input)?; + verify_json_from_manifest(&resource, input, target_resource)?; command_input = get_command_input(export.input.as_ref(), input)?; } @@ -691,7 +696,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc }; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.exportVerifyOutput", resource = &resource.type_name, executable = &export.executable)); - verify_json_from_manifest(&resource, line)?; + verify_json_from_manifest(&resource, line, target_resource)?; } instances.push(instance); } @@ -844,7 +849,7 @@ async fn run_process_async(executable: &str, args: Option>, input: O debug!("{}", t!("dscresources.commandResource.processChildExit", executable = executable, id = child_id, code = code)); if code != 0 { - // Only use manifest-provided exit code mappings when the map is not empty/default, + // Only use manifest-provided exit code mappings when the map is not empty/default, // so that default mappings do not suppress stderr-based diagnostics. if !exit_codes.is_empty_or_default() { if let Some(error_message) = exit_codes.get_code(code) { @@ -970,6 +975,28 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res Some(processed_args) } +fn process_schema_args(args: Option<&Vec>, command_resource_info: &CommandResourceInfo) -> Option> { + let Some(arg_values) = args else { + debug!("{}", t!("dscresources.commandResource.noArgs")); + return None; + }; + + let mut processed_args = Vec::::new(); + for arg in arg_values { + match arg { + SchemaArgKind::String(s) => { + processed_args.push(s.clone()); + }, + SchemaArgKind::ResourceType { resource_type_arg } => { + processed_args.push(resource_type_arg.clone()); + processed_args.push(command_resource_info.type_name.to_string()); + }, + } + } + + Some(processed_args) +} + /// Process the arguments for a command resource's set or delete operation. /// /// # Arguments @@ -980,7 +1007,7 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_set_delete_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo, execution_type: &ExecutionKind) -> (Option>, bool) { +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); @@ -1052,7 +1079,7 @@ fn get_command_input(input_kind: Option<&InputKind>, input: &str) -> Result Result<(), DscError> { +fn verify_json_from_manifest(resource: &DscResource, json: &str, target_resource: Option<&DscResource>) -> 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())); @@ -1061,7 +1088,7 @@ fn verify_json_from_manifest(resource: &DscResource, json: &str) -> Result<(), D // see if resource implements validate if manifest.validate.is_some() { trace!("{}", t!("dscresources.commandResource.validateJson", json = json)); - let result = invoke_validate(resource, json, None)?; + let result = invoke_validate(resource, json, target_resource)?; if result.valid { return Ok(()); } @@ -1070,7 +1097,7 @@ fn verify_json_from_manifest(resource: &DscResource, json: &str) -> Result<(), D } // otherwise, use schema validation - let schema = get_schema(resource)?; + let schema = get_schema(resource, target_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 e37783321..8c8b747b1 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -279,6 +279,17 @@ impl DscResource { Ok(export_result) } + fn invoke_schema_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource) -> Result { + let mut configurator = self.clone().create_config_for_adapter(adapter, "")?; + let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; + if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + adapter.target_resource = Some(Box::new(target_resource.clone())); + return adapter.schema(); + } + + return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeSchemaNotSupported", resource = self.type_name).to_string())); + } + fn get_adapter_resource(configurator: &mut Configurator, adapter: &FullyQualifiedTypeName) -> Result { if let Some(adapter_resource) = configurator.discovery().find_resource(&DiscoveryFilter::new(adapter, None, None))? { return Ok(adapter_resource.clone()); @@ -514,13 +525,16 @@ impl Invoke for DscResource { if let Some(deprecation_message) = self.deprecation_message.as_ref() { warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); } - if self.require_adapter.is_some() { - return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeSchemaNotSupported", resource = self.type_name).to_string())); + if let Some(schema) = &self.schema { + return Ok(serde_json::to_string(schema)?); + } + if let Some(adapter) = &self.require_adapter { + return self.invoke_schema_with_adapter(adapter, &self); } match &self.implemented_as { Some(ImplementedAs::Command) => { - command_resource::get_schema(&self) + command_resource::get_schema(&self, self.target_resource.as_deref()) }, _ => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index ba0b1d859..886c789f0 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -155,6 +155,19 @@ pub enum SetDeleteArgKind { } } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(untagged)] +#[dsc_repo_schema(base_name = "commandArgs.schema", folder_path = "definitions")] +pub enum SchemaArgKind { + /// The argument is a string. + String(String), + ResourceType { + /// The argument that accepts the resource type name. + #[serde(rename = "resourceTypeArg")] + resource_type_arg: String, + }, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[schemars(transform = idiomaticize_string_enum)] #[dsc_repo_schema(base_name = "inputKind", folder_path = "definitions")] @@ -183,7 +196,7 @@ pub struct SchemaCommand { /// The command to run to get the schema. pub executable: String, /// The arguments to pass to the command. - pub args: Option>, + pub args: Option>, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 94d1e17ca..ea790f7cf 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -16,25 +16,23 @@ "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": { - "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." - } + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/powershell/dsc", + "title": "AdaptedTwo", + "description": "Adapted test resource number two", + "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." } } } @@ -58,9 +56,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://github.com/powershell/dsc", + "title": "AdaptedFour", + "description": "An adapted resource for testing.", "type": "object", "required": [], "additionalProperties": false, @@ -632,6 +630,21 @@ } ] }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "schema", + "--input", + "{}", + { + "resourceTypeArg": "--resource-type" + } + ] + } + }, "validate": { "executable": "dsctest", "args": [ diff --git a/tools/dsctest/src/adapter.rs b/tools/dsctest/src/adapter.rs index 48806d805..9547ea787 100644 --- a/tools/dsctest/src/adapter.rs +++ b/tools/dsctest/src/adapter.rs @@ -188,6 +188,23 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, res _ => Err(format!("Unknown resource type: {resource_type}")), } }, + AdapterOperation::Schema => { + match resource_type { + "Adapted/One" => { + let schema = schemars::schema_for!(AdaptedOne); + Ok(serde_json::to_string(&schema).unwrap()) + }, + "Adapted/Two" => { + let schema = schemars::schema_for!(AdaptedTwo); + Ok(serde_json::to_string(&schema).unwrap()) + }, + "Adapted/Three" => { + let schema = schemars::schema_for!(AdaptedOne); + Ok(serde_json::to_string(&schema).unwrap()) + }, + _ => Err(format!("Unknown resource type: {resource_type}")), + } + }, AdapterOperation::Validate => { Ok("{\"valid\": true}".to_string()) }, diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 21c4f4d1e..fd7b7d56e 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -39,6 +39,7 @@ pub enum AdapterOperation { List, Export, Validate, + Schema, } #[derive(Debug, PartialEq, Eq, Subcommand)]