Skip to content

Commit fcbaa32

Browse files
committed
refactor resource schema validation and use before inserting metadata
1 parent 362ff1c commit fcbaa32

File tree

6 files changed

+128
-84
lines changed

6 files changed

+128
-84
lines changed

dsc/src/subcommand.rs

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, SchemaType, ExtensionSubCommand, FunctionSub
55
use crate::resolve::{get_contents, Include};
66
use crate::resource_command::{get_resource, self};
77
use crate::tablewriter::Table;
8-
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};
8+
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};
99
use dsc_lib::functions::FunctionArgKind;
1010
use dsc_lib::{
1111
configure::{
@@ -26,8 +26,8 @@ use dsc_lib::{
2626
TestResult,
2727
ValidateResult,
2828
},
29-
dscresources::dscresource::{Capability, ImplementedAs, Invoke},
30-
dscresources::resource_manifest::{import_manifest, ResourceManifest},
29+
dscresources::dscresource::{Capability, ImplementedAs, validate_json, validate_properties},
30+
dscresources::resource_manifest::import_manifest,
3131
extensions::dscextension::Capability as ExtensionCapability,
3232
functions::FunctionDispatcher,
3333
progress::ProgressFormat,
@@ -516,34 +516,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
516516

517517
// see if the resource is command based
518518
if resource.implemented_as == ImplementedAs::Command {
519-
// if so, see if it implements validate via the resource manifest
520-
if let Some(manifest) = resource.manifest.clone() {
521-
// convert to resource_manifest
522-
let manifest: ResourceManifest = serde_json::from_value(manifest)?;
523-
if manifest.validate.is_some() {
524-
debug!("{}: {type_name} ", t!("subcommand.resourceImplementsValidate"));
525-
// get the resource's part of the config
526-
let resource_config = resource_block["properties"].to_string();
527-
let result = resource.validate(&resource_config)?;
528-
if !result.valid {
529-
let reason = result.reason.unwrap_or(t!("subcommand.noReason").to_string());
530-
let type_name = resource.type_name.clone();
531-
return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("subcommand.resourceValidationFailed"))));
532-
}
533-
}
534-
else {
535-
// use schema validation
536-
trace!("{}: {type_name}", t!("subcommand.resourceDoesNotImplementValidate"));
537-
let Ok(schema) = resource.schema() else {
538-
return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noSchemaOrValidate"))));
539-
};
540-
let schema = serde_json::from_str(&schema)?;
541-
542-
validate_json(&resource.type_name, &schema, &resource_block["properties"])?;
543-
}
544-
} else {
545-
return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noManifest"))));
546-
}
519+
validate_properties(resource, &resource_block["properties"])?;
547520
}
548521
}
549522

dsc/src/util.rs

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@ use dsc_lib::{
4141
parse_input_to_json,
4242
},
4343
};
44-
use jsonschema::Validator;
4544
use path_absolutize::Absolutize;
4645
use rust_i18n::t;
4746
use schemars::{Schema, schema_for};
4847
use serde::Deserialize;
49-
use serde_json::Value;
5048
use std::collections::HashMap;
5149
use std::env;
5250
use std::io::{IsTerminal, Read};
@@ -423,39 +421,6 @@ pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Op
423421
info!("Trace-level is {:?}", tracing_setting.level);
424422
}
425423

426-
/// Validate the JSON against the schema.
427-
///
428-
/// # Arguments
429-
///
430-
/// * `source` - The source of the JSON
431-
/// * `schema` - The schema to validate against
432-
/// * `json` - The JSON to validate
433-
///
434-
/// # Returns
435-
///
436-
/// Nothing on success.
437-
///
438-
/// # Errors
439-
///
440-
/// * `DscError` - The JSON is invalid
441-
pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> {
442-
debug!("{}: {source}", t!("util.validatingSchema"));
443-
trace!("JSON: {json}");
444-
trace!("Schema: {schema}");
445-
let compiled_schema = match Validator::new(schema) {
446-
Ok(compiled_schema) => compiled_schema,
447-
Err(err) => {
448-
return Err(DscError::Validation(format!("{}: {err}", t!("util.failedToCompileSchema"))));
449-
}
450-
};
451-
452-
if let Err(err) = compiled_schema.validate(json) {
453-
return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("util.validationFailed"))));
454-
}
455-
456-
Ok(())
457-
}
458-
459424
pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_stdin: bool) -> String {
460425
trace!("Input: {input:?}, File: {file:?}");
461426
let value = if let Some(input) = input {

dsc/tests/dsc_metadata.tests.ps1

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22
# Licensed under the MIT License.
33

44
Describe 'metadata tests' {
5+
It 'metadata not provided if not declared in resource schema' {
6+
$configYaml = @'
7+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
8+
resources:
9+
- name: test
10+
type: Microsoft.DSC.Debug/Echo
11+
metadata:
12+
ignoreKey: true
13+
properties:
14+
output: hello world
15+
'@
16+
$out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
17+
$LASTEXITCODE | Should -Be 0
18+
(Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*"
19+
$out.results.result.actualState.output | Should -BeExactly 'hello world'
20+
}
21+
522
It 'resource can provide high-level metadata for <operation>' -TestCases @(
623
@{ operation = 'get' }
724
@{ operation = 'set' }

dsc_lib/locales/en-us.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ propertyNotString = "Property '%{name}' with value '%{value}' is not a string"
7171
metadataMicrosoftDscIgnored = "Resource returned '_metadata' property 'Microsoft.DSC' which is ignored"
7272
metadataNotObject = "Resource returned '_metadata' property which is not an object"
7373
metadataRestartRequiredInvalid = "Resource returned '_metadata' property '_restartRequired' which contains invalid value: %{value}"
74+
schemaExcludesMetadata = "Will not add '_metadata' to properties because resource schema does not support it"
7475

7576
[discovery.commandDiscovery]
7677
couldNotReadSetting = "Could not read 'resourcePath' setting"

dsc_lib/src/configure/mod.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::configure::{config_doc::RestartRequired, parameters::Input};
66
use crate::dscerror::DscError;
77
use crate::dscresources::invoke_result::ExportResult;
88
use crate::dscresources::{
9-
{dscresource::{Capability, Invoke, get_diff},
9+
{dscresource::{Capability, Invoke, get_diff, validate_properties},
1010
invoke_result::{GetResult, SetResult, TestResult, ResourceSetResponse}},
1111
resource_manifest::Kind,
1212
};
@@ -170,8 +170,8 @@ fn escape_property_values(properties: &Map<String, Value>) -> Result<Option<Map<
170170
Ok(Some(result))
171171
}
172172

173-
fn add_metadata(kind: &Kind, mut properties: Option<Map<String, Value>>, resource_metadata: Option<Metadata> ) -> Result<String, DscError> {
174-
if *kind == Kind::Adapter {
173+
fn add_metadata(dsc_resource: &DscResource, mut properties: Option<Map<String, Value>>, resource_metadata: Option<Metadata> ) -> Result<String, DscError> {
174+
if dsc_resource.kind == Kind::Adapter {
175175
// add metadata to the properties so the adapter knows this is a config
176176
let mut metadata: Map<String, Value> = Map::new();
177177
if let Some(resource_metadata) = resource_metadata {
@@ -191,15 +191,19 @@ fn add_metadata(kind: &Kind, mut properties: Option<Map<String, Value>>, resourc
191191
}
192192

193193
if let Some(resource_metadata) = resource_metadata {
194-
let other_metadata = resource_metadata.other.clone();
195-
if let Some(mut properties) = properties {
196-
properties.insert("_metadata".to_string(), Value::Object(other_metadata));
197-
return Ok(serde_json::to_string(&properties)?);
198-
}
199-
let mut props = Map::new();
194+
let other_metadata = resource_metadata.other;
195+
let mut props = if let Some(props) = properties {
196+
props
197+
} else {
198+
Map::new()
199+
};
200200
props.insert("_metadata".to_string(), Value::Object(other_metadata));
201-
properties = Some(props);
202-
return Ok(serde_json::to_string(&properties)?);
201+
let modified_props = Value::from(props.clone());
202+
if let Ok(()) = validate_properties(dsc_resource, &modified_props) {} else {
203+
warn!("{}", t!("configure.mod.schemaExcludesMetadata"));
204+
props.remove("_metadata");
205+
}
206+
return Ok(serde_json::to_string(&props)?);
203207
}
204208

205209
match properties {
@@ -347,7 +351,7 @@ impl Configurator {
347351
};
348352
let properties = self.get_properties(&resource, &dsc_resource.kind)?;
349353
debug!("resource_type {}", &resource.resource_type);
350-
let filter = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?;
354+
let filter = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
351355
trace!("filter: {filter}");
352356
let start_datetime = chrono::Local::now();
353357
let mut get_result = match dsc_resource.get(&filter) {
@@ -442,7 +446,7 @@ impl Configurator {
442446
}
443447
};
444448

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

448452
let start_datetime;
@@ -579,7 +583,7 @@ impl Configurator {
579583
};
580584
let properties = self.get_properties(&resource, &dsc_resource.kind)?;
581585
debug!("resource_type {}", &resource.resource_type);
582-
let expected = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?;
586+
let expected = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
583587
trace!("{}", t!("configure.mod.expectedState", state = expected));
584588
let start_datetime = chrono::Local::now();
585589
let mut test_result = match dsc_resource.test(&expected) {
@@ -655,7 +659,7 @@ impl Configurator {
655659
return Err(DscError::ResourceNotFound(resource.resource_type.clone()));
656660
};
657661
let properties = self.get_properties(resource, &dsc_resource.kind)?;
658-
let input = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?;
662+
let input = add_metadata(dsc_resource, properties, resource.metadata.clone())?;
659663
trace!("{}", t!("configure.mod.exportInput", input = input));
660664
let export_result = match add_resource_export_results_to_configuration(dsc_resource, &mut conf, input.as_str()) {
661665
Ok(result) => result,

dsc_lib/src/dscresources/dscresource.rs

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@
44
use crate::{configure::{config_doc::{Configuration, ExecutionKind, Resource}, Configurator}, dscresources::resource_manifest::Kind};
55
use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse};
66
use dscerror::DscError;
7+
use jsonschema::Validator;
78
use rust_i18n::t;
89
use schemars::JsonSchema;
910
use serde::{Deserialize, Serialize};
1011
use serde_json::{Map, Value};
1112
use std::collections::HashMap;
12-
use tracing::{debug, info};
13-
14-
use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest};
13+
use tracing::{debug, info, trace};
14+
15+
use super::{
16+
command_resource,
17+
dscerror,
18+
invoke_result::{
19+
ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult
20+
},
21+
resource_manifest::{
22+
import_manifest, ResourceManifest
23+
}
24+
};
1525

1626
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1727
#[serde(deny_unknown_fields)]
@@ -557,6 +567,80 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec<String> {
557567
diff_properties
558568
}
559569

570+
/// Validates the properties of a resource against its schema.
571+
///
572+
/// # Arguments
573+
///
574+
/// * `properties` - The properties of the resource to validate.
575+
/// * `schema` - The schema to validate against.
576+
///
577+
/// # Returns
578+
///
579+
/// * `Result<(), DscError>` - Ok if valid, Err with message if invalid.
580+
///
581+
/// # Errors
582+
///
583+
/// * `DscError` - Error if the schema is invalid
584+
pub fn validate_properties(resource: &DscResource, properties: &Value) -> Result<(), DscError> {
585+
// if so, see if it implements validate via the resource manifest
586+
let type_name = resource.type_name.clone();
587+
if let Some(manifest) = resource.manifest.clone() {
588+
// convert to resource_manifest``
589+
let manifest: ResourceManifest = serde_json::from_value(manifest)?;
590+
if manifest.validate.is_some() {
591+
debug!("{}: {type_name} ", t!("subcommand.resourceImplementsValidate"));
592+
let resource_config = properties.to_string();
593+
let result = resource.validate(&resource_config)?;
594+
if !result.valid {
595+
let reason = result.reason.unwrap_or(t!("subcommand.noReason").to_string());
596+
return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("subcommand.resourceValidationFailed"))));
597+
}
598+
return Ok(())
599+
}
600+
// use schema validation
601+
trace!("{}: {type_name}", t!("subcommand.resourceDoesNotImplementValidate"));
602+
let Ok(schema) = resource.schema() else {
603+
return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noSchemaOrValidate"))));
604+
};
605+
let schema = serde_json::from_str(&schema)?;
606+
return validate_json(&resource.type_name, &schema, properties)
607+
}
608+
Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noManifest"))))
609+
}
610+
611+
/// Validate the JSON against the schema.
612+
///
613+
/// # Arguments
614+
///
615+
/// * `source` - The source of the JSON
616+
/// * `schema` - The schema to validate against
617+
/// * `json` - The JSON to validate
618+
///
619+
/// # Returns
620+
///
621+
/// Nothing on success.
622+
///
623+
/// # Errors
624+
///
625+
/// * `DscError` - The JSON is invalid
626+
pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> {
627+
debug!("{}: {source}", t!("util.validatingSchema"));
628+
trace!("JSON: {json}");
629+
trace!("Schema: {schema}");
630+
let compiled_schema = match Validator::new(schema) {
631+
Ok(compiled_schema) => compiled_schema,
632+
Err(err) => {
633+
return Err(DscError::Validation(format!("{}: {err}", t!("util.failedToCompileSchema"))));
634+
}
635+
};
636+
637+
if let Err(err) = compiled_schema.validate(json) {
638+
return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("util.validationFailed"))));
639+
}
640+
641+
Ok(())
642+
}
643+
560644
/// Compares two arrays independent of order
561645
fn is_same_array(expected: &Vec<Value>, actual: &Vec<Value>) -> bool {
562646
if expected.len() != actual.len() {

0 commit comments

Comments
 (0)