diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 new file mode 100644 index 00000000..747202b9 --- /dev/null +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -0,0 +1,41 @@ +Describe 'metadata tests' { + BeforeAll { + $config_yaml = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json + resources: + - name: Message + type: Test/Metadata + properties: + _metadata: + messages: + - hello world +"@ + } + + It 'can pull _metadata from config set' { + $result = $config_yaml | dsc config set | ConvertFrom-Json + $result.results.metadata.messages[0] | Should -BeExactly 'hello world' + $result.results.result.afterState | Should -Be '' + $result.hadErrors | Should -BeFalse + $result.results.Count | Should -Be 1 + $LASTEXITCODE | Should -Be 0 + } + + # It 'can pull _metadata from config get' { + # $result = $config_yaml | dsc config get | ConvertFrom-Json + # $result.results.metadata.messages[0] | Should -BeExactly 'hello world' + # $result.results.result.actualState | Should -Be '' + # $result.hadErrors | Should -BeFalse + # $result.results.Count | Should -Be 1 + # $LASTEXITCODE | Should -Be 0 + # } + + # It 'can pull _metadata from config test' { + # $result = $config_yaml | dsc config test | ConvertFrom-Json + # $result.results.metadata.messages[0] | Should -BeExactly 'hello world' + # $result.results.result.actualState | Should -Be '' + # $result.hadErrors | Should -BeFalse + # $result.results.Count | Should -Be 1 + # $LASTEXITCODE | Should -Be 0 + # } +} diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index ceb4ea84..50a759c7 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -65,6 +65,8 @@ pub struct MicrosoftDscMetadata { pub struct Metadata { #[serde(rename = "Microsoft.DSC", skip_serializing_if = "Option::is_none")] pub microsoft: Option, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub resource: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 3a5d84a3..16ee553f 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -260,7 +260,8 @@ impl Configurator { duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()), ..Default::default() } - ) + ), + resource: None } ), name: resource.name.clone(), @@ -364,6 +365,8 @@ impl Configurator { } self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?); + // Process SetResult by checking for _metadata field + let (set_result_parsed, resource_metadata) = Configurator::parse_metadata_from_set(set_result)?; let resource_result = config_result::ResourceSetResult { metadata: Some( Metadata { @@ -372,12 +375,13 @@ impl Configurator { duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()), ..Default::default() } - ) + ), + resource: resource_metadata } ), name: resource.name.clone(), resource_type: resource.resource_type.clone(), - result: set_result, + result: set_result_parsed, }; result.results.push(resource_result); } @@ -426,7 +430,8 @@ impl Configurator { duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()), ..Default::default() } - ) + ), + resource: None } ), name: resource.name.clone(), @@ -598,7 +603,8 @@ impl Configurator { duration: Some(end_datetime.signed_duration_since(self.context.start_datetime).to_string()), security_context: Some(self.context.security_context.clone()), } - ) + ), + resource: None } } @@ -711,4 +717,30 @@ impl Configurator { } Ok(Some(result)) } + + fn parse_metadata(mut input: Value) -> Result<(Value, Option), DscError> { + let result = input.clone(); + if let Value::Object(mut map) = input.take() { + let metadata = map.remove("_metadata"); + return Ok((Value::Object(map), metadata)); + } + // TODO: if the after_state can't be parsed as a map, it may be because it's nested in a group like - afterState.result.afterState? + // returning original for now + return Ok((result, None)) + } + + fn parse_metadata_from_set(set_result: SetResult) -> Result<(SetResult, Option), DscError> { + match set_result { + SetResult::Resource(result) => { + let mut set_result_parsed = result.clone(); + let (after_state, metadata) = Configurator::parse_metadata(result.after_state)?; + set_result_parsed.after_state = after_state; + return Ok((SetResult::Resource(set_result_parsed), metadata)); + }, + SetResult::Group(_results) => { + //Ok((SetResult::Group(results.clone()), None)) + Err(DscError::NotImplemented("group resources not implemented yet".to_string())) + } + } + } } diff --git a/tools/dsctest/dscmetadata.dsc.resource.json b/tools/dsctest/dscmetadata.dsc.resource.json new file mode 100644 index 00000000..faf34bde --- /dev/null +++ b/tools/dsctest/dscmetadata.dsc.resource.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Test/Metadata", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "metadata", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "metadata", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "metadata" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index e394a8a8..ab5cffb5 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -9,6 +9,7 @@ pub enum Schemas { Echo, Exist, ExitCode, + Metadata, Sleep, Trace, WhatIf, @@ -48,6 +49,12 @@ pub enum SubCommand { input: String, }, + #[clap(name = "metadata", about = "Return the metadata")] + Metadata { + #[clap(name = "input", short, long, help = "The input to the metadata command as JSON")] + input: String, + }, + #[clap(name = "schema", about = "Get the JSON schema for a subcommand")] Schema { #[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")] diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 91760736..a616e579 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -6,6 +6,7 @@ mod delete; mod echo; mod exist; mod exit_code; +mod metadata; mod sleep; mod trace; mod whatif; @@ -17,6 +18,7 @@ use crate::delete::Delete; use crate::echo::Echo; use crate::exist::{Exist, State}; use crate::exit_code::ExitCode; +use crate::metadata::Metadata; use crate::sleep::Sleep; use crate::trace::Trace; use crate::whatif::WhatIf; @@ -77,6 +79,16 @@ fn main() { } input }, + SubCommand::Metadata { input } => { + let metadata = match serde_json::from_str::(&input) { + Ok(metadata) => metadata, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + serde_json::to_string(&metadata).unwrap() + }, SubCommand::Schema { subcommand } => { let schema = match subcommand { Schemas::Delete => { @@ -91,6 +103,9 @@ fn main() { Schemas::ExitCode => { schema_for!(ExitCode) }, + Schemas::Metadata => { + schema_for!(Metadata) + }, Schemas::Sleep => { schema_for!(Sleep) }, diff --git a/tools/dsctest/src/metadata.rs b/tools/dsctest/src/metadata.rs new file mode 100644 index 00000000..ac4fcaa1 --- /dev/null +++ b/tools/dsctest/src/metadata.rs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Metadata { + #[serde(rename="_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SubMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub messages: Option> +}