diff --git a/Cargo.lock b/Cargo.lock index 1f2f37d97..46262c465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -979,6 +979,7 @@ name = "dsctest" version = "0.1.0" dependencies = [ "clap", + "regex", "registry", "schemars", "serde", @@ -2624,9 +2625,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -2647,9 +2648,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "registry" @@ -4239,15 +4240,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_feature" -version = "0.1.0" -dependencies = [ - "rust-i18n", - "serde", - "serde_json", -] - [[package]] name = "windows_firewall" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b792f8307..9826d8efa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ default-members = [ "lib/dsc-lib-registry", "resources/runcommandonset", "lib/dsc-lib-security_context", + "resources/dism_dsc", "resources/sshdconfig", "resources/WindowsUpdate", "resources/windows_service", @@ -57,8 +58,7 @@ default-members = [ "grammars/tree-sitter-dscexpression", "grammars/tree-sitter-ssh-server-config", "y2j", - "xtask", - "resources/dism_dsc" + "xtask" ] [workspace.metadata.groups] @@ -197,7 +197,7 @@ quote = { version = "1.0" } # used by other crates rand = { version = "0.10.1" } # dsc, dsc-lib -regex = { version = "1.12.3" } +regex = { version = "1.12.4" } # registry, dsc-lib, dsc-lib-registry, dsctest registry = { version = "1.3" } # dsc diff --git a/dsc/src/main.rs b/dsc/src/main.rs index b66b0ca2e..b7264a7be 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -29,6 +29,21 @@ pub mod util; i18n!("locales", fallback = "en-us"); fn main() { + #[cfg(windows)] + { + let mut builder = std::thread::Builder::new(); + builder = builder.stack_size(2 * 1024 * 1024); // Default stack is too small on Windows causing overflow + builder.spawn(|| { + dsc_main(); + }).unwrap().join().unwrap(); + } + #[cfg(not(windows))] + { + dsc_main(); + } +} + +fn dsc_main() { #[cfg(debug_assertions)] check_debug(); diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index 57ab35f1b..95a22a968 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -109,7 +109,6 @@ Describe 'Discover extension tests' { $out.type | Should -Be 'Test/DiscoverRelative' $out = dsc resource list 2> $TestDrive/error.log $LASTEXITCODE | Should -Be 0 - $out | Should -BeNullOrEmpty $errorMessage = Get-Content -Path "$TestDrive/error.log" -Raw $errorMessage | Should -BeLike '*is not an absolute path*' } finally { diff --git a/dsc/tests/dsc_resource_export.tests.ps1 b/dsc/tests/dsc_resource_export.tests.ps1 new file mode 100644 index 000000000..f780f87f6 --- /dev/null +++ b/dsc/tests/dsc_resource_export.tests.ps1 @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Resource export tests' { + It "Export with accepts input '' 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) + } +} diff --git a/lib/dsc-lib-jsonschema/.versions.json b/lib/dsc-lib-jsonschema/.versions.json index ccb9af183..15f6ca427 100644 --- a/lib/dsc-lib-jsonschema/.versions.json +++ b/lib/dsc-lib-jsonschema/.versions.json @@ -1,10 +1,11 @@ { "latestMajor": "V3", "latestMinor": "V3_2", - "latestPatch": "V3_2_1", + "latestPatch": "V3_2_2", "all": [ "V3", "V3_2", + "V3_2_2", "V3_2_1", "V3_2_0", "V3_1", diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 59f1e6f43..1622d9156 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -8,7 +8,7 @@ use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; use std::{collections::HashMap, env, path::Path, process::Stdio}; -use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::{resource_manifest::SchemaArgKind}, types::ExitCodesMap, util::canonicalize_which}; +use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::{ExportSchemaKind, SchemaArgKind}, types::{ExitCodesMap}, util::canonicalize_which}; use crate::dscerror::DscError; use super::{ dscresource::{get_diff, redact, DscResource}, @@ -586,6 +586,64 @@ 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())); + }; + + let command_resource = match target_resource { + Some(r) => r, + None => resource, + }; + + let schema = match export.schema { + Some(ExportSchemaKind::Command(ref command)) => { + let args = process_schema_args(command.args.as_ref(), command_resource); + 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)? + }, + Some(ExportSchemaKind::NoFiltering) => { + return Ok(()); + }, + None => { + if 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)); + } + + 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 @@ -636,9 +694,15 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc None => resource, }; validate_security_context(&export.require_security_context, &command_resource.type_name, "export")?; + 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)?; } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 93d558712..704f50e13 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -206,6 +206,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. @@ -321,6 +333,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, + pub schema: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] diff --git a/tools/dsctest/Cargo.toml b/tools/dsctest/Cargo.toml index b339babbb..f502c610e 100644 --- a/tools/dsctest/Cargo.toml +++ b/tools/dsctest/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" [dependencies] clap = { workspace = true } +regex = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 681dbbf4d..446c28655 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -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" + ] + } + } } ] } diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 0e681e27e..a0b28ec59 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -11,6 +11,8 @@ pub enum Schemas { Exist, ExitCode, Export, + ExportGetSchema, + ExportSchema, Exporter, Get, InDesiredState, @@ -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")] diff --git a/tools/dsctest/src/export_schema.rs b/tools/dsctest/src/export_schema.rs new file mode 100644 index 000000000..3318dbf07 --- /dev/null +++ b/tools/dsctest/src/export_schema.rs @@ -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 = if filter.name.contains("*") { + // convert the wildcard to a regex + let regex = filter.name.replace("*", ".*"); + let regex = regex::Regex::new(®ex).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 +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 060d07969..39decf60e 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -7,6 +7,7 @@ mod delete; mod exist; mod exit_code; mod export; +mod export_schema; mod exporter; mod get; mod in_desired_state; @@ -31,6 +32,7 @@ use crate::delete::Delete; use crate::exist::{Exist, State}; use crate::exit_code::ExitCode; use crate::export::Export; +use crate::export_schema::{ExportSchema, invoke_export_schema}; use crate::exporter::{Exporter, Resource}; use crate::get::Get; use crate::in_desired_state::InDesiredState; @@ -132,6 +134,9 @@ fn main() { } String::new() }, + SubCommand::ExportSchema { input } => { + invoke_export_schema(&input) + }, SubCommand::Exporter { input } => { let exporter = match serde_json::from_str::(&input) { Ok(exporter) => exporter, @@ -300,6 +305,12 @@ fn main() { Schemas::Export => { schema_for!(Export) }, + Schemas::ExportGetSchema => { + schema_for!(export_schema::Schema) + }, + Schemas::ExportSchema => { + schema_for!(ExportSchema) + }, Schemas::Exporter => { schema_for!(Exporter) },