Skip to content
Merged
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
74 changes: 74 additions & 0 deletions dsc/tests/dsc_metadata.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'metadata tests' {
It 'resource can provide metadata for <operation>' -TestCases @(
@{ operation = 'get' }
@{ operation = 'set' }
@{ operation = 'test' }
) {
param($operation)

$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/Metadata
properties:
_metadata:
hello: world
myNumber: 42
'@

$out = dsc config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results.count | Should -Be 1
$out.results[0].metadata.hello | Should -BeExactly 'world'
$out.results[0].metadata.myNumber | Should -Be 42
}

It 'resource can provide metadata for export' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/Metadata
properties:
_metadata:
hello: There
myNumber: 16
'@
$out = dsc config export -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.resources.count | Should -Be 3
$out.resources[0].metadata.hello | Should -BeExactly 'There'
$out.resources[0].metadata.myNumber | Should -Be 16
$out.resources[0].name | Should -BeExactly 'Metadata example 1'
$out.resources[1].metadata.hello | Should -BeExactly 'There'
$out.resources[1].metadata.myNumber | Should -Be 16
$out.resources[1].name | Should -BeExactly 'Metadata example 2'
$out.resources[2].metadata.hello | Should -BeExactly 'There'
$out.resources[2].metadata.myNumber | Should -Be 16
$out.resources[2].name | Should -BeExactly 'Metadata example 3'
}

It 'resource returning Microsoft.DSC metadata is ignored' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/Metadata
properties:
_metadata:
Microsoft.DSC:
hello: world
validOne: true
'@
$out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results.count | Should -Be 1
$out.results[0].metadata.validOne | Should -BeTrue
$out.results[0].metadata.Microsoft.DSC | Should -BeNullOrEmpty
(Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Resource returned '_metadata' property 'Microsoft.DSC' which is ignored*"
}
}
2 changes: 2 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ parameterNotObject = "Parameter '%{name}' is not an object"
invokePropertyExpressions = "Invoke property expressions"
invokeExpression = "Invoke property expression for %{name}: %{value}"
propertyNotString = "Property '%{name}' with value '%{value}' is not a string"
metadataMicrosoftDscIgnored = "Resource returned '_metadata' property 'Microsoft.DSC' which is ignored"
metadataNotObject = "Resource returned '_metadata' property which is not an object"

[discovery.commandDiscovery]
couldNotReadSetting = "Could not read 'resourcePath' setting"
Expand Down
21 changes: 21 additions & 0 deletions dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use chrono::{DateTime, Local};
use rust_i18n::t;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -58,6 +59,26 @@ pub struct MicrosoftDscMetadata {
pub security_context: Option<SecurityContextKind>,
}

impl MicrosoftDscMetadata {
/// Creates a new instance of `MicrosoftDscMetadata` with the duration
///
/// # Arguments
///
/// * `start` - The start time of the configuration operation
/// * `end` - The end time of the configuration operation
///
/// # Returns
///
/// A new instance of `MicrosoftDscMetadata` with the duration calculated from the start and end times.
#[must_use]
pub fn new_with_duration(start: &DateTime<Local>, end: &DateTime<Local>) -> Self {
Self {
duration: Some(end.signed_duration_since(*start).to_string()),
..Default::default()
}
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct Metadata {
#[serde(rename = "Microsoft.DSC", skip_serializing_if = "Option::is_none")]
Expand Down
126 changes: 73 additions & 53 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use security_context_lib::{SecurityContext, get_security_context};
use serde_json::{Map, Value};
use std::path::PathBuf;
use std::collections::HashMap;
use tracing::{debug, info, trace};
use tracing::{debug, info, trace, warn};
pub mod context;
pub mod config_doc;
pub mod config_result;
Expand Down Expand Up @@ -75,27 +75,35 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf
}
r.kind = kind.as_str().map(std::string::ToString::to_string);
}
r.name = if let Some(name) = props.remove("_name") {
name.as_str()
.map(std::string::ToString::to_string)
.ok_or_else(|| DscError::Parser(t!("configure.mod.propertyNotString", name = "_name", value = name).to_string()))?
} else {
format!("{}-{i}", r.resource_type)
};
let mut metadata = Metadata {
microsoft: None,
other: Map::new(),
};
if let Some(security_context) = props.remove("_securityContext") {
let context: SecurityContextKind = serde_json::from_value(security_context)?;
let metadata = Metadata {
microsoft: Some(
metadata.microsoft = Some(
MicrosoftDscMetadata {
security_context: Some(context),
..Default::default()
}
),
other: Map::new(),
};
r.metadata = Some(metadata);
);
}
r.name = if let Some(name) = props.remove("_name") {
name.as_str()
.map(std::string::ToString::to_string)
.ok_or_else(|| DscError::Parser(t!("configure.mod.propertyNotString", name = "_name", value = name).to_string()))?
r.properties = escape_property_values(&props)?;
let mut properties = serde_json::to_value(&r.properties)?;
get_metadata_from_result(&mut properties, &mut metadata)?;
r.properties = Some(properties.as_object().cloned().unwrap_or_default());
r.metadata = if metadata.microsoft.is_some() || !metadata.other.is_empty() {
Some(metadata)
} else {
format!("{}-{i}", r.resource_type)
None
};
r.properties = escape_property_values(&props)?;

conf.resources.push(r);
}
Expand Down Expand Up @@ -217,6 +225,26 @@ fn check_security_context(metadata: Option<&Metadata>) -> Result<(), DscError> {
Ok(())
}

fn get_metadata_from_result(result: &mut Value, metadata: &mut Metadata) -> Result<(), DscError> {
if let Some(metadata_value) = result.get("_metadata") {
if let Some(metadata_map) = metadata_value.as_object() {
for (key, value) in metadata_map {
if key.starts_with("Microsoft.DSC") {
warn!("{}", t!("configure.mod.metadataMicrosoftDscIgnored", key = key));
continue;
}
metadata.other.insert(key.clone(), value.clone());
}
} else {
return Err(DscError::Parser(t!("configure.mod.metadataNotObject", value = metadata_value).to_string()));
}
if let Some(value_map) = result.as_object_mut() {
value_map.remove("_metadata");
}
}
Ok(())
}

impl Configurator {
/// Create a new `Configurator` instance.
///
Expand Down Expand Up @@ -288,7 +316,7 @@ impl Configurator {
let filter = add_metadata(&dsc_resource.kind, properties)?;
trace!("filter: {filter}");
let start_datetime = chrono::Local::now();
let get_result = match dsc_resource.get(&filter) {
let mut get_result = match dsc_resource.get(&filter) {
Ok(result) => result,
Err(e) => {
progress.set_failure(get_failure_from_error(&e));
Expand All @@ -297,9 +325,17 @@ impl Configurator {
},
};
let end_datetime = chrono::Local::now();
match &get_result {
GetResult::Resource(resource_result) => {
let mut metadata = Metadata {
microsoft: Some(
MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime)
),
other: Map::new(),
};

match &mut get_result {
GetResult::Resource(ref mut resource_result) => {
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_result.actual_state)?);
get_metadata_from_result(&mut resource_result.actual_state, &mut metadata)?;
},
GetResult::Group(group) => {
let mut results = Vec::<Value>::new();
Expand All @@ -310,17 +346,7 @@ impl Configurator {
},
}
let resource_result = config_result::ResourceGetResult {
metadata: Some(
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
..Default::default()
}
),
other: Map::new(),
}
),
metadata: Some(metadata),
name: resource.name.clone(),
resource_type: resource.resource_type.clone(),
result: get_result.clone(),
Expand Down Expand Up @@ -383,7 +409,7 @@ impl Configurator {

let start_datetime;
let end_datetime;
let set_result;
let mut set_result;
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
debug!("{}", t!("configure.mod.handlesExist"));
start_datetime = chrono::Local::now();
Expand Down Expand Up @@ -453,9 +479,16 @@ impl Configurator {
return Err(DscError::NotImplemented(t!("configure.mod.deleteNotSupported", resource = resource.resource_type).to_string()));
}

match &set_result {
let mut metadata = Metadata {
microsoft: Some(
MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime)
),
other: Map::new(),
};
match &mut set_result {
SetResult::Resource(resource_result) => {
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_result.after_state)?);
get_metadata_from_result(&mut resource_result.after_state, &mut metadata)?;
},
SetResult::Group(group) => {
let mut results = Vec::<Value>::new();
Expand All @@ -466,17 +499,7 @@ impl Configurator {
},
}
let resource_result = config_result::ResourceSetResult {
metadata: Some(
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
..Default::default()
}
),
other: Map::new(),
}
),
metadata: Some(metadata),
name: resource.name.clone(),
resource_type: resource.resource_type.clone(),
result: set_result.clone(),
Expand Down Expand Up @@ -517,7 +540,7 @@ impl Configurator {
let expected = add_metadata(&dsc_resource.kind, properties)?;
trace!("{}", t!("configure.mod.expectedState", state = expected));
let start_datetime = chrono::Local::now();
let test_result = match dsc_resource.test(&expected) {
let mut test_result = match dsc_resource.test(&expected) {
Ok(result) => result,
Err(e) => {
progress.set_failure(get_failure_from_error(&e));
Expand All @@ -526,9 +549,16 @@ impl Configurator {
},
};
let end_datetime = chrono::Local::now();
match &test_result {
let mut metadata = Metadata {
microsoft: Some(
MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime)
),
other: Map::new(),
};
match &mut test_result {
TestResult::Resource(resource_test_result) => {
self.context.references.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&resource_test_result.actual_state)?);
get_metadata_from_result(&mut resource_test_result.actual_state, &mut metadata)?;
},
TestResult::Group(group) => {
let mut results = Vec::<Value>::new();
Expand All @@ -539,17 +569,7 @@ impl Configurator {
},
}
let resource_result = config_result::ResourceTestResult {
metadata: Some(
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
..Default::default()
}
),
other: Map::new(),
}
),
metadata: Some(metadata),
name: resource.name.clone(),
resource_type: resource.resource_type.clone(),
result: test_result.clone(),
Expand Down
Loading
Loading