Skip to content

Commit 4ec8a9a

Browse files
committed
Enable resources to return metadata
1 parent 9dee199 commit 4ec8a9a

File tree

7 files changed

+291
-76
lines changed

7 files changed

+291
-76
lines changed

dsc/tests/dsc_metadata.tests.ps1

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'metadata tests' {
5+
It 'resource can provide metadata for <operation>' -TestCases @(
6+
@{ operation = 'get' }
7+
@{ operation = 'set' }
8+
@{ operation = 'test' }
9+
) {
10+
param($operation)
11+
12+
$configYaml = @'
13+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
14+
resources:
15+
- name: test
16+
type: Test/Metadata
17+
properties:
18+
_metadata:
19+
hello: world
20+
myNumber: 42
21+
'@
22+
23+
$out = dsc config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
24+
$LASTEXITCODE | Should -Be 0
25+
$out.results.count | Should -Be 1
26+
$out.results[0].metadata.hello | Should -BeExactly 'world'
27+
$out.results[0].metadata.myNumber | Should -Be 42
28+
}
29+
30+
It 'resource can provide metadata for export' {
31+
$configYaml = @'
32+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
33+
resources:
34+
- name: test
35+
type: Test/Metadata
36+
properties:
37+
_metadata:
38+
hello: There
39+
myNumber: 16
40+
'@
41+
$out = dsc config export -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
42+
$LASTEXITCODE | Should -Be 0
43+
$out.resources.count | Should -Be 3
44+
$out.resources[0].metadata.hello | Should -BeExactly 'There'
45+
$out.resources[0].metadata.myNumber | Should -Be 16
46+
$out.resources[0].name | Should -BeExactly 'Metadata example 1'
47+
$out.resources[1].metadata.hello | Should -BeExactly 'There'
48+
$out.resources[1].metadata.myNumber | Should -Be 16
49+
$out.resources[1].name | Should -BeExactly 'Metadata example 2'
50+
$out.resources[2].metadata.hello | Should -BeExactly 'There'
51+
$out.resources[2].metadata.myNumber | Should -Be 16
52+
$out.resources[2].name | Should -BeExactly 'Metadata example 3'
53+
}
54+
55+
It 'resource returning Microsoft.DSC metadata is ignored' {
56+
$configYaml = @'
57+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
58+
resources:
59+
- name: test
60+
type: Test/Metadata
61+
properties:
62+
_metadata:
63+
Microsoft.DSC:
64+
hello: world
65+
validOne: true
66+
'@
67+
$out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
68+
$LASTEXITCODE | Should -Be 0
69+
$out.results.count | Should -Be 1
70+
$out.results[0].metadata.validOne | Should -BeTrue
71+
$out.results[0].metadata.Microsoft.DSC | Should -BeNullOrEmpty
72+
(Get-Content $TestDrive/error.log) | Should -BeLike "*Resource returned metadata property 'Microsoft.DSC' which is ignored*"
73+
}
74+
}

dsc_lib/locales/en-us.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ parameterNotObject = "Parameter '%{name}' is not an object"
6767
invokePropertyExpressions = "Invoke property expressions"
6868
invokeExpression = "Invoke property expression for %{name}: %{value}"
6969
propertyNotString = "Property '%{name}' with value '%{value}' is not a string"
70+
metadataMicrosoftDscIgnored = "Resource returned metadata property 'Microsoft.DSC' which is ignored"
71+
metadataNotObject = "Resource returned 'metadata' property which is not an object"
7072

7173
[discovery.commandDiscovery]
7274
couldNotReadSetting = "Could not read 'resourcePath' setting"

dsc_lib/src/configure/mod.rs

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use security_context_lib::{SecurityContext, get_security_context};
2424
use serde_json::{Map, Value};
2525
use std::path::PathBuf;
2626
use std::collections::HashMap;
27-
use tracing::{debug, info, trace};
27+
use tracing::{debug, info, trace, warn};
2828
pub mod context;
2929
pub mod config_doc;
3030
pub mod config_result;
@@ -75,27 +75,35 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf
7575
}
7676
r.kind = kind.as_str().map(std::string::ToString::to_string);
7777
}
78+
r.name = if let Some(name) = props.remove("_name") {
79+
name.as_str()
80+
.map(std::string::ToString::to_string)
81+
.ok_or_else(|| DscError::Parser(t!("configure.mod.propertyNotString", name = "_name", value = name).to_string()))?
82+
} else {
83+
format!("{}-{i}", r.resource_type)
84+
};
85+
r.properties = escape_property_values(&props)?;
86+
let mut properties = serde_json::to_value(&r.properties)?;
87+
let mut metadata = Metadata {
88+
microsoft: None,
89+
other: Map::new(),
90+
};
91+
get_metadata_from_result(&mut properties, &mut metadata)?;
7892
if let Some(security_context) = props.remove("_securityContext") {
7993
let context: SecurityContextKind = serde_json::from_value(security_context)?;
80-
let metadata = Metadata {
81-
microsoft: Some(
94+
metadata.microsoft = Some(
8295
MicrosoftDscMetadata {
8396
security_context: Some(context),
8497
..Default::default()
8598
}
86-
),
87-
other: Map::new(),
88-
};
89-
r.metadata = Some(metadata);
99+
);
90100
}
91-
r.name = if let Some(name) = props.remove("_name") {
92-
name.as_str()
93-
.map(std::string::ToString::to_string)
94-
.ok_or_else(|| DscError::Parser(t!("configure.mod.propertyNotString", name = "_name", value = name).to_string()))?
101+
r.properties = Some(properties.as_object().cloned().unwrap_or_default());
102+
r.metadata = if metadata.microsoft.is_some() || !metadata.other.is_empty() {
103+
Some(metadata)
95104
} else {
96-
format!("{}-{i}", r.resource_type)
105+
None
97106
};
98-
r.properties = escape_property_values(&props)?;
99107

100108
conf.resources.push(r);
101109
}
@@ -217,6 +225,26 @@ fn check_security_context(metadata: Option<&Metadata>) -> Result<(), DscError> {
217225
Ok(())
218226
}
219227

228+
fn get_metadata_from_result(result: &mut Value, metadata: &mut Metadata) -> Result<(), DscError> {
229+
if let Some(metadata_value) = result.get("_metadata") {
230+
if let Some(metadata_map) = metadata_value.as_object() {
231+
for (key, value) in metadata_map {
232+
if key.starts_with("Microsoft.DSC") {
233+
warn!("{}", t!("configure.mod.metadataMicrosoftDscIgnored", key = key));
234+
continue;
235+
}
236+
metadata.other.insert(key.clone(), value.clone());
237+
}
238+
} else {
239+
return Err(DscError::Parser(t!("configure.mod.metadataNotObject", value = metadata_value).to_string()));
240+
}
241+
if let Some(value_map) = result.as_object_mut() {
242+
value_map.remove("_metadata");
243+
}
244+
}
245+
Ok(())
246+
}
247+
220248
impl Configurator {
221249
/// Create a new `Configurator` instance.
222250
///
@@ -288,7 +316,7 @@ impl Configurator {
288316
let filter = add_metadata(&dsc_resource.kind, properties)?;
289317
trace!("filter: {filter}");
290318
let start_datetime = chrono::Local::now();
291-
let get_result = match dsc_resource.get(&filter) {
319+
let mut get_result = match dsc_resource.get(&filter) {
292320
Ok(result) => result,
293321
Err(e) => {
294322
progress.set_failure(get_failure_from_error(&e));
@@ -297,9 +325,20 @@ impl Configurator {
297325
},
298326
};
299327
let end_datetime = chrono::Local::now();
300-
match &get_result {
301-
GetResult::Resource(resource_result) => {
328+
let mut metadata = Metadata {
329+
microsoft: Some(
330+
MicrosoftDscMetadata {
331+
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
332+
..Default::default()
333+
}
334+
),
335+
other: Map::new(),
336+
};
337+
338+
match &mut get_result {
339+
GetResult::Resource(ref mut resource_result) => {
302340
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_result.actual_state)?);
341+
get_metadata_from_result(&mut resource_result.actual_state, &mut metadata)?;
303342
},
304343
GetResult::Group(group) => {
305344
let mut results = Vec::<Value>::new();
@@ -310,17 +349,7 @@ impl Configurator {
310349
},
311350
}
312351
let resource_result = config_result::ResourceGetResult {
313-
metadata: Some(
314-
Metadata {
315-
microsoft: Some(
316-
MicrosoftDscMetadata {
317-
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
318-
..Default::default()
319-
}
320-
),
321-
other: Map::new(),
322-
}
323-
),
352+
metadata: Some(metadata),
324353
name: resource.name.clone(),
325354
resource_type: resource.resource_type.clone(),
326355
result: get_result.clone(),
@@ -383,7 +412,7 @@ impl Configurator {
383412

384413
let start_datetime;
385414
let end_datetime;
386-
let set_result;
415+
let mut set_result;
387416
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
388417
debug!("{}", t!("configure.mod.handlesExist"));
389418
start_datetime = chrono::Local::now();
@@ -453,9 +482,19 @@ impl Configurator {
453482
return Err(DscError::NotImplemented(t!("configure.mod.deleteNotSupported", resource = resource.resource_type).to_string()));
454483
}
455484

456-
match &set_result {
485+
let mut metadata = Metadata {
486+
microsoft: Some(
487+
MicrosoftDscMetadata {
488+
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
489+
..Default::default()
490+
}
491+
),
492+
other: Map::new(),
493+
};
494+
match &mut set_result {
457495
SetResult::Resource(resource_result) => {
458496
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_result.after_state)?);
497+
get_metadata_from_result(&mut resource_result.after_state, &mut metadata)?;
459498
},
460499
SetResult::Group(group) => {
461500
let mut results = Vec::<Value>::new();
@@ -466,17 +505,7 @@ impl Configurator {
466505
},
467506
}
468507
let resource_result = config_result::ResourceSetResult {
469-
metadata: Some(
470-
Metadata {
471-
microsoft: Some(
472-
MicrosoftDscMetadata {
473-
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
474-
..Default::default()
475-
}
476-
),
477-
other: Map::new(),
478-
}
479-
),
508+
metadata: Some(metadata),
480509
name: resource.name.clone(),
481510
resource_type: resource.resource_type.clone(),
482511
result: set_result.clone(),
@@ -517,7 +546,7 @@ impl Configurator {
517546
let expected = add_metadata(&dsc_resource.kind, properties)?;
518547
trace!("{}", t!("configure.mod.expectedState", state = expected));
519548
let start_datetime = chrono::Local::now();
520-
let test_result = match dsc_resource.test(&expected) {
549+
let mut test_result = match dsc_resource.test(&expected) {
521550
Ok(result) => result,
522551
Err(e) => {
523552
progress.set_failure(get_failure_from_error(&e));
@@ -526,9 +555,19 @@ impl Configurator {
526555
},
527556
};
528557
let end_datetime = chrono::Local::now();
529-
match &test_result {
558+
let mut metadata = Metadata {
559+
microsoft: Some(
560+
MicrosoftDscMetadata {
561+
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
562+
..Default::default()
563+
}
564+
),
565+
other: Map::new(),
566+
};
567+
match &mut test_result {
530568
TestResult::Resource(resource_test_result) => {
531569
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_test_result.actual_state)?);
570+
get_metadata_from_result(&mut resource_test_result.actual_state, &mut metadata)?;
532571
},
533572
TestResult::Group(group) => {
534573
let mut results = Vec::<Value>::new();
@@ -539,17 +578,7 @@ impl Configurator {
539578
},
540579
}
541580
let resource_result = config_result::ResourceTestResult {
542-
metadata: Some(
543-
Metadata {
544-
microsoft: Some(
545-
MicrosoftDscMetadata {
546-
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
547-
..Default::default()
548-
}
549-
),
550-
other: Map::new(),
551-
}
552-
),
581+
metadata: Some(metadata),
553582
name: resource.name.clone(),
554583
resource_type: resource.resource_type.clone(),
555584
result: test_result.clone(),
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
3+
"type": "Test/Metadata",
4+
"version": "0.1.0",
5+
"get": {
6+
"executable": "dsctest",
7+
"args": [
8+
"metadata",
9+
{
10+
"jsonInputArg": "--input",
11+
"mandatory": true
12+
}
13+
]
14+
},
15+
"set": {
16+
"executable": "dsctest",
17+
"args": [
18+
"metadata",
19+
{
20+
"jsonInputArg": "--input",
21+
"mandatory": true
22+
}
23+
],
24+
"return": "state"
25+
},
26+
"test": {
27+
"executable": "dsctest",
28+
"args": [
29+
"metadata",
30+
{
31+
"jsonInputArg": "--input",
32+
"mandatory": true
33+
}
34+
]
35+
},
36+
"export": {
37+
"executable": "dsctest",
38+
"args": [
39+
"metadata",
40+
{
41+
"jsonInputArg": "--input",
42+
"mandatory": true
43+
},
44+
"--export"
45+
]
46+
},
47+
"schema": {
48+
"command": {
49+
"executable": "dsctest",
50+
"args": [
51+
"schema",
52+
"-s",
53+
"metadata"
54+
]
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)