Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dsc/tests/dsc_adapter.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand Down
24 changes: 24 additions & 0 deletions dsc/tests/dsc_resource_schema.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
}
63 changes: 45 additions & 18 deletions lib/dsc-lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -64,15 +64,15 @@ 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)?;
}

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(&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::<Vec<ResourceGetResult>>(&stdout) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<String, DscError> {
pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) -> Result<String, DscError> {
let Some(manifest) = &resource.manifest else {
return Err(DscError::MissingManifest(resource.type_name.to_string()));
};
Expand All @@ -599,7 +599,12 @@ pub fn get_schema(resource: &DscResource) -> Result<String, DscError> {

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) => {
Expand Down Expand Up @@ -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)?;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -844,7 +849,7 @@ async fn run_process_async(executable: &str, args: Option<Vec<String>>, 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) {
Expand Down Expand Up @@ -970,6 +975,28 @@ pub fn process_get_args(args: Option<&Vec<GetArgKind>>, input: &str, command_res
Some(processed_args)
}

fn process_schema_args(args: Option<&Vec<SchemaArgKind>>, command_resource_info: &CommandResourceInfo) -> Option<Vec<String>> {
let Some(arg_values) = args else {
debug!("{}", t!("dscresources.commandResource.noArgs"));
return None;
};

let mut processed_args = Vec::<String>::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
Expand All @@ -980,7 +1007,7 @@ pub fn process_get_args(args: Option<&Vec<GetArgKind>>, input: &str, command_res
/// # Returns
///
/// A vector of strings representing the processed arguments
pub fn process_set_delete_args(args: Option<&Vec<SetDeleteArgKind>>, input: &str, command_resource_info: &CommandResourceInfo, execution_type: &ExecutionKind) -> (Option<Vec<String>>, bool) {
fn process_set_delete_args(args: Option<&Vec<SetDeleteArgKind>>, input: &str, command_resource_info: &CommandResourceInfo, execution_type: &ExecutionKind) -> (Option<Vec<String>>, bool) {
let Some(arg_values) = args else {
debug!("{}", t!("dscresources.commandResource.noArgs"));
return (None, false);
Expand Down Expand Up @@ -1052,7 +1079,7 @@ fn get_command_input(input_kind: Option<&InputKind>, input: &str) -> Result<Comm
})
}

fn verify_json_from_manifest(resource: &DscResource, json: &str) -> 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()));
Expand All @@ -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(());
}
Expand All @@ -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,
Expand Down
20 changes: 17 additions & 3 deletions lib/dsc-lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,17 @@ impl DscResource {
Ok(export_result)
}

fn invoke_schema_with_adapter(&self, adapter: &FullyQualifiedTypeName, target_resource: &DscResource) -> Result<String, DscError> {
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<DscResource, DscError> {
if let Some(adapter_resource) = configurator.discovery().find_resource(&DiscoveryFilter::new(adapter, None, None))? {
return Ok(adapter_resource.clone());
Expand Down Expand Up @@ -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()))
Expand Down
15 changes: 14 additions & 1 deletion lib/dsc-lib/src/dscresources/resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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<Vec<String>>,
pub args: Option<Vec<SchemaArgKind>>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand Down
57 changes: 35 additions & 22 deletions tools/dsctest/dsctest.dsc.manifests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -632,6 +630,21 @@
}
]
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"adapter",
"--operation",
"schema",
"--input",
"{}",
{
"resourceTypeArg": "--resource-type"
}
]
}
},
"validate": {
"executable": "dsctest",
"args": [
Expand Down
Loading