Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions dsc/tests/dsc_resource_export.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Resource export tests' {
It "Export with <resource> accepts input '<json>' and returns filtered results" -TestCases @(
@{ resource = 'Test/ExportSchemaCommand'; json = '{ "name": "Gijs" }'; expected = @('Gijs') },
@{ resource = 'Test/ExportSchemaCommand'; json = '{ "name": "*e*" }'; expected = @('Steve', 'Tess') },
@{ resource = 'Test/ExportSchemaEmbedded'; json = '{ "name": "Gijs" }'; expected = @('Gijs') },
@{ resource = 'Test/ExportSchemaEmbedded'; json = '{ "name": "*e*" }'; expected = @('Steve', 'Tess') },
@{ resource = 'Test/ExportSchemaNoFiltering'; json = '{ "name": "Gijs" }'; expected = @('Steve', 'Tess', 'Gijs') }
){
param($resource, $json, $expected)

$output = dsc resource export -r $resource -i $json 2>$TESTDRIVE/error.log | ConvertFrom-Json
$errorlog = Get-Content "$TESTDRIVE/error.log" -Raw
$LASTEXITCODE | Should -Be 0 -Because $errorlog
$output.resources.count | Should -Be $expected.Count -Because ($output | ConvertTo-Json -Depth 4)
$output.resources.properties.name | Should -Be $expected -Because ($output | ConvertTo-Json -Depth 4)
}
}
4 changes: 3 additions & 1 deletion lib/dsc-lib-jsonschema/.versions.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"latestMajor": "V3",
"latestMinor": "V3_2",
"latestPatch": "V3_2_0",
"latestPatch": "V3_2_2",
"all": [
"V3",
"V3_2",
"V3_2_2",
"V3_2_1",
"V3_2_0",
"V3_1",
"V3_1_3",
Expand Down
64 changes: 62 additions & 2 deletions lib/dsc-lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rust_i18n::t;
use serde::Deserialize;
use serde_json::{Map, Value};
use std::{collections::HashMap, env, path::{Path, PathBuf}, process::Stdio};
use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which};
use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::{ExportSchemaKind, SchemaArgKind}, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which};
use crate::dscerror::DscError;
use super::{
dscresource::{get_diff, redact, DscResource},
Expand Down Expand Up @@ -616,6 +616,60 @@ pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>)
}
}

fn verify_with_export_schema(input: &str, resource: &DscResource, target_resource: Option<&DscResource>) -> Result<(), DscError> {
let Some(manifest) = &resource.manifest else {
return Err(DscError::MissingManifest(resource.type_name.to_string()));
};

let Some(export) = manifest.export.as_ref() else {
return Err(DscError::SchemaNotAvailable(resource.type_name.to_string()));
};

if export.schema.is_none() && manifest.validate.is_some() {
let result = invoke_validate(resource, input, target_resource)?;
if result.valid {
return Ok(());
}

let reason = result
.reason
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| t!("dscresources.commandResource.resourceInvalidJson").to_string());
return Err(DscError::Validation(reason));
}

let schema = match export.schema {
Some(ExportSchemaKind::Command(ref command)) => {
let resource_type = match target_resource {
Some(r) => r.type_name.clone(),
None => resource.type_name.clone(),
};
let args = process_schema_args(command.args.as_ref(), &CommandResourceInfo { type_name: resource_type, path: None });
let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?;
stdout
},
Some(ExportSchemaKind::Embedded(ref schema)) => {
serde_json::to_string(schema)?
},
_ => {
get_schema(resource, target_resource)?
}
};
let schema = serde_json::from_str(&schema)?;
let compiled_schema = match Validator::new(&schema) {
Ok(schema) => schema,
Err(e) => {
return Err(DscError::Schema(e.to_string()));
},
};
let json: Value = serde_json::from_str(input)?;
if let Err(err) = compiled_schema.validate(&json) {
return Err(DscError::Schema(err.to_string()));
}
Ok(())
}

/// Invoke the export operation on a resource
///
/// # Arguments
Expand Down Expand Up @@ -671,9 +725,15 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc
type_name: resource_type.clone(),
path,
};

if let Some(input) = input {
let input = if export.schema == Some(ExportSchemaKind::NoFiltering) {
""
} else {
input
};
if !input.is_empty() {
verify_json_from_manifest(resource, input, target_resource)?;
verify_with_export_schema(input, resource, target_resource)?;

command_input = get_command_input(export.input.as_ref(), input)?;
}
Expand Down
13 changes: 13 additions & 0 deletions lib/dsc-lib/src/dscresources/resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ pub enum SchemaKind {
Embedded(Value),
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[dsc_repo_schema(base_name = "manifest.exportSchema", folder_path = "definitions")]
#[serde(rename_all = "camelCase")]
pub enum ExportSchemaKind {
/// The export schema is returned by running a command.
Command(SchemaCommand),
/// The export schema is embedded in the manifest.
Embedded(Value),
/// The export operation does not support filtering.
NoFiltering,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct SchemaCommand {
/// The command to run to get the schema.
Expand Down Expand Up @@ -313,6 +325,7 @@ pub struct ExportMethod {
/// The security context required to run the Export method. Default if not specified is `current`.
#[serde(rename = "requireSecurityContext", skip_serializing_if = "Option::is_none")]
pub require_security_context: Option<SecurityContextKind>,
pub schema: Option<ExportSchemaKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand Down
1 change: 1 addition & 0 deletions tools/dsctest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2024"

[dependencies]
clap = { workspace = true }
regex = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
Expand Down
108 changes: 108 additions & 0 deletions tools/dsctest/dsctest.dsc.manifests.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,114 @@
]
}
}
},
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"type": "Test/ExportSchemaCommand",
"version": "0.1.0",
"description": "Test resource for export specific schema",
"export": {
"executable": "dsctest",
"args": [
"export-schema",
{
"jsonInputArg": "--input",
"mandatory": true
}
],
"schema": {
"command":{
"executable": "dsctest",
"args": [
"schema",
"-s",
"export-schema"
]
}
}
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"schema",
"-s",
"export-get-schema"
]
}
}
},
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"type": "Test/ExportSchemaEmbedded",
"version": "0.1.0",
"description": "Test resource for export specific schema",
"export": {
"executable": "dsctest",
"args": [
"export-schema",
{
"jsonInputArg": "--input",
"mandatory": true
}
],
"schema": {
"embedded": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/export-schema",
"title": "Export Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"title": "Export Property",
"description": "Can contain wildcards for export filtering."
}
},
"required": [
"name"
]
}
}
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"schema",
"-s",
"export-get-schema"
]
}
}
},
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"type": "Test/ExportSchemaNoFiltering",
"version": "0.1.0",
"description": "Test resource for export specific schema",
"export": {
"executable": "dsctest",
"args": [
"export-schema",
{
"jsonInputArg": "--input",
"mandatory": true
}
],
"schema": "noFiltering"
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"schema",
"-s",
"export-get-schema"
]
}
}
}
]
}
8 changes: 8 additions & 0 deletions tools/dsctest/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub enum Schemas {
Exist,
ExitCode,
Export,
ExportGetSchema,
ExportSchema,
Exporter,
Get,
InDesiredState,
Expand Down Expand Up @@ -95,6 +97,12 @@ pub enum SubCommand {
input: String,
},

#[clap(name = "export-schema", about = "Test export specific schema")]
ExportSchema {
#[clap(name = "input", short, long, help = "The input to the export schema command as JSON")]
input: String,
},

#[clap(name = "exporter", about = "Exports different types of resources")]
Exporter {
#[clap(name = "input", short, long, help = "The input to the exporter command as JSON")]
Expand Down
86 changes: 86 additions & 0 deletions tools/dsctest/src/export_schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Display;

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum Names {
Gijs,
Steve,
Tess,
}

impl Display for Names {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Names::Gijs => write!(f, "Gijs"),
Names::Steve => write!(f, "Steve"),
Names::Tess => write!(f, "Tess"),
}
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Schema {
pub name: Names,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ExportSchema {
pub name: String,
}

pub fn invoke_export_schema(input: &str) -> String {
let instances = vec![
Schema {
name: Names::Steve,
},
Schema {
name: Names::Tess,
},
Schema {
name: Names::Gijs,
},
];
let filter: ExportSchema = if !input.is_empty() {
match serde_json::from_str(input) {
Ok(filter) => filter,
Err(err) => {
eprintln!("Error JSON does not match schema: {err}");
std::process::exit(1);
}
}
} else {
ExportSchema {
name: "*".to_string(),
}
};
let filtered_instances: Vec<Schema> = if filter.name.contains("*") {
// convert the wildcard to a regex
let regex = filter.name.replace("*", ".*");
let regex = regex::Regex::new(&regex).unwrap();
instances
.into_iter()
.filter(|instance| regex.is_match(&instance.name.to_string()))
.collect()
} else {
instances
.into_iter()
.filter(|instance| instance.name.to_string() == filter.name)
.collect()
};
let mut output = String::new();
let mut count = filtered_instances.len();
for instance in &filtered_instances {
output.push_str(serde_json::to_string(instance).unwrap().as_str());
if count > 1 {
output.push('\n');
}
count -= 1;
}
output
}
Loading
Loading