Skip to content

Commit beb05fb

Browse files
authored
Merge pull request #947 from SteveL-MSFT/resource-metadata
Enable resources to return metadata
2 parents 9dee199 + 766da94 commit beb05fb

File tree

8 files changed

+303
-76
lines changed

8 files changed

+303
-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 "*WARN*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/config_doc.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
use chrono::{DateTime, Local};
45
use rust_i18n::t;
56
use schemars::JsonSchema;
67
use serde::{Deserialize, Serialize};
@@ -58,6 +59,26 @@ pub struct MicrosoftDscMetadata {
5859
pub security_context: Option<SecurityContextKind>,
5960
}
6061

62+
impl MicrosoftDscMetadata {
63+
/// Creates a new instance of `MicrosoftDscMetadata` with the duration
64+
///
65+
/// # Arguments
66+
///
67+
/// * `start` - The start time of the configuration operation
68+
/// * `end` - The end time of the configuration operation
69+
///
70+
/// # Returns
71+
///
72+
/// A new instance of `MicrosoftDscMetadata` with the duration calculated from the start and end times.
73+
#[must_use]
74+
pub fn new_with_duration(start: &DateTime<Local>, end: &DateTime<Local>) -> Self {
75+
Self {
76+
duration: Some(end.signed_duration_since(*start).to_string()),
77+
..Default::default()
78+
}
79+
}
80+
}
81+
6182
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
6283
pub struct Metadata {
6384
#[serde(rename = "Microsoft.DSC", skip_serializing_if = "Option::is_none")]

dsc_lib/src/configure/mod.rs

Lines changed: 73 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+
let mut metadata = Metadata {
86+
microsoft: None,
87+
other: Map::new(),
88+
};
7889
if let Some(security_context) = props.remove("_securityContext") {
7990
let context: SecurityContextKind = serde_json::from_value(security_context)?;
80-
let metadata = Metadata {
81-
microsoft: Some(
91+
metadata.microsoft = Some(
8292
MicrosoftDscMetadata {
8393
security_context: Some(context),
8494
..Default::default()
8595
}
86-
),
87-
other: Map::new(),
88-
};
89-
r.metadata = Some(metadata);
96+
);
9097
}
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()))?
98+
r.properties = escape_property_values(&props)?;
99+
let mut properties = serde_json::to_value(&r.properties)?;
100+
get_metadata_from_result(&mut properties, &mut metadata)?;
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,17 @@ 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::new_with_duration(&start_datetime, &end_datetime)
331+
),
332+
other: Map::new(),
333+
};
334+
335+
match &mut get_result {
336+
GetResult::Resource(ref mut resource_result) => {
302337
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_result.actual_state)?);
338+
get_metadata_from_result(&mut resource_result.actual_state, &mut metadata)?;
303339
},
304340
GetResult::Group(group) => {
305341
let mut results = Vec::<Value>::new();
@@ -310,17 +346,7 @@ impl Configurator {
310346
},
311347
}
312348
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-
),
349+
metadata: Some(metadata),
324350
name: resource.name.clone(),
325351
resource_type: resource.resource_type.clone(),
326352
result: get_result.clone(),
@@ -383,7 +409,7 @@ impl Configurator {
383409

384410
let start_datetime;
385411
let end_datetime;
386-
let set_result;
412+
let mut set_result;
387413
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
388414
debug!("{}", t!("configure.mod.handlesExist"));
389415
start_datetime = chrono::Local::now();
@@ -453,9 +479,16 @@ impl Configurator {
453479
return Err(DscError::NotImplemented(t!("configure.mod.deleteNotSupported", resource = resource.resource_type).to_string()));
454480
}
455481

456-
match &set_result {
482+
let mut metadata = Metadata {
483+
microsoft: Some(
484+
MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime)
485+
),
486+
other: Map::new(),
487+
};
488+
match &mut set_result {
457489
SetResult::Resource(resource_result) => {
458490
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_result.after_state)?);
491+
get_metadata_from_result(&mut resource_result.after_state, &mut metadata)?;
459492
},
460493
SetResult::Group(group) => {
461494
let mut results = Vec::<Value>::new();
@@ -466,17 +499,7 @@ impl Configurator {
466499
},
467500
}
468501
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-
),
502+
metadata: Some(metadata),
480503
name: resource.name.clone(),
481504
resource_type: resource.resource_type.clone(),
482505
result: set_result.clone(),
@@ -517,7 +540,7 @@ impl Configurator {
517540
let expected = add_metadata(&dsc_resource.kind, properties)?;
518541
trace!("{}", t!("configure.mod.expectedState", state = expected));
519542
let start_datetime = chrono::Local::now();
520-
let test_result = match dsc_resource.test(&expected) {
543+
let mut test_result = match dsc_resource.test(&expected) {
521544
Ok(result) => result,
522545
Err(e) => {
523546
progress.set_failure(get_failure_from_error(&e));
@@ -526,9 +549,16 @@ impl Configurator {
526549
},
527550
};
528551
let end_datetime = chrono::Local::now();
529-
match &test_result {
552+
let mut metadata = Metadata {
553+
microsoft: Some(
554+
MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime)
555+
),
556+
other: Map::new(),
557+
};
558+
match &mut test_result {
530559
TestResult::Resource(resource_test_result) => {
531560
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_test_result.actual_state)?);
561+
get_metadata_from_result(&mut resource_test_result.actual_state, &mut metadata)?;
532562
},
533563
TestResult::Group(group) => {
534564
let mut results = Vec::<Value>::new();
@@ -539,17 +569,7 @@ impl Configurator {
539569
},
540570
}
541571
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-
),
572+
metadata: Some(metadata),
553573
name: resource.name.clone(),
554574
resource_type: resource.resource_type.clone(),
555575
result: test_result.clone(),

0 commit comments

Comments
 (0)