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
9 changes: 0 additions & 9 deletions dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,6 @@ noResources = "Resources not specified"
resourceTypeNotSpecified = "Resource type not specified"
validatingResource = "Validating resource named"
resourceNotFound = "Resource type not found"
resourceImplementsValidate = "Resource implements validation"
noReason = "No reason provided"
resourceValidationFailed = "Resource failed validation"
resourceDoesNotImplementValidate = "Resource does not implement validation, using schema"
noSchemaOrValidate = "Resource does not have a schema nor supports validation"
noManifest = "Resource does not have a manifest"
tableHeader_type = "Type"
tableHeader_kind = "Kind"
tableHeader_version = "Version"
Expand All @@ -127,9 +121,6 @@ failedToConvertJsonToString = "Failed to convert JSON to string"
failedToReadTracingSetting = "Could not read 'tracing' setting"
invalidTraceLevel = "Default to 'warn', invalid DSC_TRACE_LEVEL value"
failedToSetTracing = "Unable to set global default tracing subscriber. Tracing is disabled."
validatingSchema = "Validating against schema"
failedToCompileSchema = "JSON Schema Compilation"
validationFailed = "Failed validation"
readingInput = "Reading input from command line parameter"
inputIsFile = "Document provided is a file path, use '--file' instead"
readingInputFromFile = "Reading input from file"
Expand Down
35 changes: 4 additions & 31 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, SchemaType, ExtensionSubCommand, FunctionSub
use crate::resolve::{get_contents, Include};
use crate::resource_command::{get_resource, self};
use crate::tablewriter::Table;
use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, validate_json, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR};
use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR};
use dsc_lib::functions::FunctionArgKind;
use dsc_lib::{
configure::{
Expand All @@ -26,8 +26,8 @@ use dsc_lib::{
TestResult,
ValidateResult,
},
dscresources::dscresource::{Capability, ImplementedAs, Invoke},
dscresources::resource_manifest::{import_manifest, ResourceManifest},
dscresources::dscresource::{Capability, ImplementedAs, validate_json, validate_properties},
dscresources::resource_manifest::import_manifest,
extensions::dscextension::Capability as ExtensionCapability,
functions::FunctionDispatcher,
progress::ProgressFormat,
Expand Down Expand Up @@ -514,34 +514,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)

// see if the resource is command based
if resource.implemented_as == ImplementedAs::Command {
// if so, see if it implements validate via the resource manifest
if let Some(manifest) = resource.manifest.clone() {
// convert to resource_manifest
let manifest: ResourceManifest = serde_json::from_value(manifest)?;
if manifest.validate.is_some() {
debug!("{}: {type_name} ", t!("subcommand.resourceImplementsValidate"));
// get the resource's part of the config
let resource_config = resource_block["properties"].to_string();
let result = resource.validate(&resource_config)?;
if !result.valid {
let reason = result.reason.unwrap_or(t!("subcommand.noReason").to_string());
let type_name = resource.type_name.clone();
return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("subcommand.resourceValidationFailed"))));
}
}
else {
// use schema validation
trace!("{}: {type_name}", t!("subcommand.resourceDoesNotImplementValidate"));
let Ok(schema) = resource.schema() else {
return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noSchemaOrValidate"))));
};
let schema = serde_json::from_str(&schema)?;

validate_json(&resource.type_name, &schema, &resource_block["properties"])?;
}
} else {
return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noManifest"))));
}
validate_properties(resource, &resource_block["properties"])?;
}
}

Expand Down
35 changes: 0 additions & 35 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,10 @@ use dsc_lib::{
parse_input_to_json,
},
};
use jsonschema::Validator;
use path_absolutize::Absolutize;
use rust_i18n::t;
use schemars::{Schema, schema_for};
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::env;
use std::io::{IsTerminal, Read};
Expand Down Expand Up @@ -423,39 +421,6 @@ pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Op
info!("Trace-level is {:?}", tracing_setting.level);
}

/// Validate the JSON against the schema.
///
/// # Arguments
///
/// * `source` - The source of the JSON
/// * `schema` - The schema to validate against
/// * `json` - The JSON to validate
///
/// # Returns
///
/// Nothing on success.
///
/// # Errors
///
/// * `DscError` - The JSON is invalid
pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> {
debug!("{}: {source}", t!("util.validatingSchema"));
trace!("JSON: {json}");
trace!("Schema: {schema}");
let compiled_schema = match Validator::new(schema) {
Ok(compiled_schema) => compiled_schema,
Err(err) => {
return Err(DscError::Validation(format!("{}: {err}", t!("util.failedToCompileSchema"))));
}
};

if let Err(err) = compiled_schema.validate(json) {
return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("util.validationFailed"))));
}

Ok(())
}

pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_stdin: bool) -> String {
trace!("Input: {input:?}, File: {file:?}");
let value = if let Some(input) = input {
Expand Down
67 changes: 67 additions & 0 deletions dsc/tests/dsc_metadata.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,73 @@
# Licensed under the MIT License.

Describe 'metadata tests' {
It 'metadata not provided if not declared in resource schema' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Microsoft.DSC.Debug/Echo
metadata:
ignoreKey: true
properties:
output: hello world
'@
$out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
(Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*"
$out.results.result.actualState.output | Should -BeExactly 'hello world'
}

It 'resource can provide high-level 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
metadata:
hello: world
myNumber: 42
properties:
'@

$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 high-level metadata for export' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/Metadata
metadata:
hello: There
myNumber: 16
properties:
'@
$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 can provide metadata for <operation>' -TestCases @(
@{ operation = 'get' }
@{ operation = 'set' }
Expand Down
10 changes: 10 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ 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"
metadataRestartRequiredInvalid = "Resource returned '_metadata' property '_restartRequired' which contains invalid value: %{value}"
schemaExcludesMetadata = "Will not add '_metadata' to properties because resource schema does not support it"

[discovery.commandDiscovery]
couldNotReadSetting = "Could not read 'resourcePath' setting"
Expand Down Expand Up @@ -176,6 +177,15 @@ diffKeyMissing = "diff: key '%{key}' missing"
diffKeyNotObject = "diff: key '%{key}' is not an object"
diffArraySize = "diff: arrays have different lengths"
diffMissingItem = "diff: actual array missing expected item"
failedToCompileSchema = "JSON Schema Compilation"
noManifest = "Resource does not have a manifest"
noReason = "No reason provided"
noSchemaOrValidate = "Resource does not have a schema nor supports validation"
resourceDoesNotImplementValidate = "Resource does not implement validation, using schema"
resourceImplementsValidate = "Resource implements validation"
resourceValidationFailed = "Resource failed validation"
validatingSchema = "Validating against schema"
validationFailed = "Failed validation"

[dscresources.resource_manifest]
resourceManifestSchemaTitle = "Resource manifest schema URI"
Expand Down
37 changes: 29 additions & 8 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::discovery::discovery_trait::DiscoveryFilter;
use crate::dscerror::DscError;
use crate::dscresources::invoke_result::ExportResult;
use crate::dscresources::{
{dscresource::{Capability, Invoke, get_diff},
{dscresource::{Capability, Invoke, get_diff, validate_properties},
invoke_result::{GetResult, SetResult, TestResult, ResourceSetResponse}},
resource_manifest::Kind,
};
Expand Down Expand Up @@ -171,10 +171,15 @@ fn escape_property_values(properties: &Map<String, Value>) -> Result<Option<Map<
Ok(Some(result))
}

fn add_metadata(kind: &Kind, mut properties: Option<Map<String, Value>> ) -> Result<String, DscError> {
if *kind == Kind::Adapter {
fn add_metadata(dsc_resource: &DscResource, mut properties: Option<Map<String, Value>>, resource_metadata: Option<Metadata> ) -> Result<String, DscError> {
if dsc_resource.kind == Kind::Adapter {
// add metadata to the properties so the adapter knows this is a config
let mut metadata = Map::new();
let mut metadata: Map<String, Value> = Map::new();
if let Some(resource_metadata) = resource_metadata {
if !resource_metadata.other.is_empty() {
metadata.extend(resource_metadata.other);
}
}
let mut dsc_value = Map::new();
dsc_value.insert("context".to_string(), Value::String("configuration".to_string()));
metadata.insert("Microsoft.DSC".to_string(), Value::Object(dsc_value));
Expand All @@ -186,6 +191,22 @@ fn add_metadata(kind: &Kind, mut properties: Option<Map<String, Value>> ) -> Res
return Ok(serde_json::to_string(&properties)?);
}

if let Some(resource_metadata) = resource_metadata {
let other_metadata = resource_metadata.other;
let mut props = if let Some(props) = properties {
props
} else {
Map::new()
};
props.insert("_metadata".to_string(), Value::Object(other_metadata));
let modified_props = Value::from(props.clone());
if let Ok(()) = validate_properties(dsc_resource, &modified_props) {} else {
warn!("{}", t!("configure.mod.schemaExcludesMetadata"));
props.remove("_metadata");
}
return Ok(serde_json::to_string(&props)?);
}

match properties {
Some(properties) => {
Ok(serde_json::to_string(&properties)?)
Expand Down Expand Up @@ -330,7 +351,7 @@ impl Configurator {
return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string()));
};
let properties = self.get_properties(&resource, &dsc_resource.kind)?;
let filter = add_metadata(&dsc_resource.kind, properties)?;
let filter = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
let start_datetime = chrono::Local::now();
let mut get_result = match dsc_resource.get(&filter) {
Ok(result) => result,
Expand Down Expand Up @@ -424,7 +445,7 @@ impl Configurator {
}
};

let desired = add_metadata(&dsc_resource.kind, properties)?;
let desired = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
trace!("{}", t!("configure.mod.desired", state = desired));

let start_datetime;
Expand Down Expand Up @@ -561,7 +582,7 @@ impl Configurator {
};
let properties = self.get_properties(&resource, &dsc_resource.kind)?;
debug!("resource_type {}", &resource.resource_type);
let expected = add_metadata(&dsc_resource.kind, properties)?;
let expected = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
trace!("{}", t!("configure.mod.expectedState", state = expected));
let start_datetime = chrono::Local::now();
let mut test_result = match dsc_resource.test(&expected) {
Expand Down Expand Up @@ -637,7 +658,7 @@ impl Configurator {
return Err(DscError::ResourceNotFound(resource.resource_type.clone(), resource.api_version.as_deref().unwrap_or("").to_string()));
};
let properties = self.get_properties(resource, &dsc_resource.kind)?;
let input = add_metadata(&dsc_resource.kind, properties)?;
let input = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
trace!("{}", t!("configure.mod.exportInput", input = input));
let export_result = match add_resource_export_results_to_configuration(dsc_resource, &mut conf, input.as_str()) {
Ok(result) => result,
Expand Down
Loading
Loading