Skip to content

Commit 93a9428

Browse files
committed
Add Microsoft.DSC/Include resource with way to have resource accept trace level
1 parent 37a6f44 commit 93a9428

File tree

10 files changed

+244
-22
lines changed

10 files changed

+244
-22
lines changed

dsc/examples/include.dsc.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This is a simple example of how to Include another configuration into this one
2+
3+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
4+
resources:
5+
- name: get os info
6+
type: Microsoft.DSC/Include
7+
properties:
8+
configurationFile: osinfo_parameters.dsc.yaml

dsc/include.dsc.resource.json

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
3+
"type": "Microsoft.DSC/Include",
4+
"version": "0.1.0",
5+
"description": "Allows including a configuration file into current configuration.",
6+
"kind": "Group",
7+
"get": {
8+
"executable": "dsc",
9+
"args": [
10+
{
11+
"traceLevelArg": "--trace-level"
12+
},
13+
"config",
14+
"--as-group",
15+
"--as-include",
16+
"get"
17+
],
18+
"input": "stdin"
19+
},
20+
"set": {
21+
"executable": "dsc",
22+
"args": [
23+
{
24+
"traceLevelArg": "--trace-level"
25+
},
26+
"config",
27+
"--as-group",
28+
"--as-include",
29+
"set"
30+
],
31+
"input": "stdin",
32+
"implementsPretest": true,
33+
"return": "state"
34+
},
35+
"test": {
36+
"executable": "dsc",
37+
"args": [
38+
{
39+
"traceLevelArg": "--trace-level"
40+
},
41+
"config",
42+
"--as-group",
43+
"--as-include",
44+
"test"
45+
],
46+
"input": "stdin",
47+
"return": "state"
48+
},
49+
"exitCodes": {
50+
"0": "Success",
51+
"1": "Invalid argument",
52+
"2": "Resource error",
53+
"3": "JSON Serialization error",
54+
"4": "Invalid input format",
55+
"5": "Resource instance failed schema validation",
56+
"6": "Command cancelled"
57+
},
58+
"schema": {
59+
"command": {
60+
"executable": "dsc",
61+
"args": [
62+
"schema",
63+
"--type",
64+
"include"
65+
]
66+
}
67+
}
68+
}

dsc/src/args.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,12 @@ pub enum SubCommand {
5454
parameters: Option<String>,
5555
#[clap(short = 'f', long, help = "Parameters to pass to the configuration as a JSON or YAML file", conflicts_with = "parameters")]
5656
parameters_file: Option<String>,
57+
// Used to inform when DSC is used as a group resource to modify it's output
5758
#[clap(long, hide = true)]
5859
as_group: bool,
5960
// Used for the Microsoft.DSC/Include resource
6061
#[clap(long, hide = true)]
61-
include: Option<String>,
62+
as_include: bool,
6263
},
6364
#[clap(name = "resource", about = "Invoke a specific DSC resource")]
6465
Resource {
@@ -208,6 +209,7 @@ pub enum DscType {
208209
TestResult,
209210
DscResource,
210211
ResourceManifest,
212+
Include,
211213
Configuration,
212214
ConfigurationGetResult,
213215
ConfigurationSetResult,

dsc/src/include.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
8+
pub struct Include {
9+
/// The path to the file to include. Path is relative to the file containing the include
10+
/// and not allowed to reference parent directories. If a configuration document is used
11+
/// instead of a file, then the path is relative to the current working directory.
12+
#[serde(rename = "configurationFile")]
13+
pub configuration_file: String,
14+
pub parameters_file: Option<String>,
15+
}

dsc/src/main.rs

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ use args::{Args, SubCommand};
55
use atty::Stream;
66
use clap::{CommandFactory, Parser};
77
use clap_complete::generate;
8+
use dsc_lib::configure::config_doc::Configuration;
9+
use crate::include::Include;
810
use std::io::{self, Read};
11+
use std::fs::File;
12+
use std::path::Path;
913
use std::process::exit;
1014
use sysinfo::{Process, ProcessExt, RefreshKind, System, SystemExt, get_current_pid, ProcessRefreshKind};
1115
use tracing::{error, info, warn, debug};
@@ -36,7 +40,7 @@ fn main() {
3640

3741
debug!("Running dsc {}", env!("CARGO_PKG_VERSION"));
3842

39-
let input = if atty::is(Stream::Stdin) {
43+
let mut input = if atty::is(Stream::Stdin) {
4044
None
4145
} else {
4246
info!("Reading input from STDIN");
@@ -66,19 +70,23 @@ fn main() {
6670
let mut cmd = Args::command();
6771
generate(shell, &mut cmd, "dsc", &mut io::stdout());
6872
},
69-
SubCommand::Config { subcommand, parameters, parameters_file, as_group } => {
73+
SubCommand::Config { subcommand, parameters, parameters_file, as_group, as_include} => {
74+
if as_include {
75+
input = Some(read_include_file(&input));
76+
}
77+
7078
if let Some(file_name) = parameters_file {
7179
info!("Reading parameters from file {}", file_name);
7280
match std::fs::read_to_string(file_name) {
73-
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group),
81+
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group, &as_include),
7482
Err(err) => {
7583
error!("Error: Failed to read parameters file: {err}");
7684
exit(util::EXIT_INVALID_INPUT);
7785
}
7886
}
7987
}
8088
else {
81-
subcommand::config(&subcommand, &parameters, &input, &as_group);
89+
subcommand::config(&subcommand, &parameters, &input, &as_group, &as_include);
8290
}
8391
},
8492
SubCommand::Resource { subcommand } => {
@@ -100,6 +108,96 @@ fn main() {
100108
exit(util::EXIT_SUCCESS);
101109
}
102110

111+
fn read_include_file(input: &Option<String>) -> String {
112+
let Some(include) = input else {
113+
error!("Error: Include requires input from STDIN");
114+
exit(util::EXIT_INVALID_INPUT);
115+
};
116+
117+
// deserialize the Include input
118+
let include: Include = match serde_json::from_str(include) {
119+
Ok(include) => include,
120+
Err(err) => {
121+
error!("Error: Failed to deserialize Include input: {err}");
122+
exit(util::EXIT_INVALID_INPUT);
123+
}
124+
};
125+
126+
let path = Path::new(&include.configuration_file);
127+
if path.is_absolute() {
128+
error!("Error: Include path must be relative: {}", include.configuration_file);
129+
exit(util::EXIT_INVALID_INPUT);
130+
}
131+
132+
// check that no components of the path are '..'
133+
if path.components().any(|c| c == std::path::Component::ParentDir) {
134+
error!("Error: Include path must not contain '..': {}", include.configuration_file);
135+
exit(util::EXIT_INVALID_INPUT);
136+
}
137+
138+
// use DSC_CONFIG_ROOT env var as current directory
139+
let current_directory = match std::env::var("DSC_CONFIG_ROOT") {
140+
Ok(current_directory) => current_directory,
141+
Err(err) => {
142+
error!("Error: Could not read DSC_CONFIG_ROOT env var: {err}");
143+
exit(util::EXIT_INVALID_INPUT);
144+
}
145+
};
146+
147+
// combine the current directory with the Include path
148+
let include_path = Path::new(&current_directory).join(&include.configuration_file);
149+
150+
// read the file specified in the Include input
151+
let mut buffer: Vec<u8> = Vec::new();
152+
match File::open(&include_path) {
153+
Ok(mut file) => {
154+
match file.read_to_end(&mut buffer) {
155+
Ok(_) => (),
156+
Err(err) => {
157+
error!("Error: Failed to read file '{include_path:?}': {err}");
158+
exit(util::EXIT_INVALID_INPUT);
159+
}
160+
}
161+
},
162+
Err(err) => {
163+
error!("Error: Failed to included file '{include_path:?}': {err}");
164+
exit(util::EXIT_INVALID_INPUT);
165+
}
166+
}
167+
// convert the buffer to a string
168+
let include_content = match String::from_utf8(buffer) {
169+
Ok(input) => input,
170+
Err(err) => {
171+
error!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}");
172+
exit(util::EXIT_INVALID_INPUT);
173+
}
174+
};
175+
176+
// try to deserialize the Include content as YAML first
177+
let configuration: Configuration = match serde_yaml::from_str(&include_content) {
178+
Ok(configuration) => configuration,
179+
Err(_err) => {
180+
// if that fails, try to deserialize it as JSON
181+
match serde_json::from_str(&include_content) {
182+
Ok(configuration) => configuration,
183+
Err(err) => {
184+
error!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}");
185+
exit(util::EXIT_INVALID_INPUT);
186+
}
187+
}
188+
}
189+
};
190+
191+
// serialize the Configuration as JSON
192+
match serde_json::to_string(&configuration) {
193+
Ok(json) => json,
194+
Err(err) => {
195+
error!("Error: JSON Error: {err}");
196+
exit(util::EXIT_JSON_ERROR);
197+
}
198+
}
199+
}
200+
103201
fn ctrlc_handler() {
104202
warn!("Ctrl-C received");
105203

dsc/src/subcommand.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,22 +198,27 @@ pub fn config_export(configurator: &mut Configurator, format: &Option<OutputForm
198198
}
199199
}
200200

201-
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin: &Option<String>, as_group: &bool) {
201+
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin: &Option<String>, as_group: &bool, use_stdin: &bool) {
202202
let json_string = match subcommand {
203203
ConfigSubCommand::Get { document, path, .. } |
204204
ConfigSubCommand::Set { document, path, .. } |
205205
ConfigSubCommand::Test { document, path, .. } |
206206
ConfigSubCommand::Validate { document, path, .. } |
207207
ConfigSubCommand::Export { document, path, .. } => {
208-
let mut new_path = path;
209-
let opt_new_path;
210-
if path.is_some()
211-
{
208+
let new_path = if path.is_some() {
212209
let config_path = path.clone().unwrap_or_default();
213-
opt_new_path = Some(set_dscconfigroot(&config_path));
214-
new_path = &opt_new_path;
210+
Some(set_dscconfigroot(&config_path))
211+
} else {
212+
// use current working directory
213+
let current_directory = std::env::current_dir().unwrap_or_default();
214+
Some(current_directory.to_string_lossy().to_string())
215+
};
216+
217+
if *use_stdin {
218+
stdin.clone().unwrap_or_default()
219+
} else {
220+
get_input(document, stdin, &new_path)
215221
}
216-
get_input(document, stdin, new_path)
217222
}
218223
};
219224

@@ -226,8 +231,12 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
226231
};
227232

228233
let parameters: Option<serde_json::Value> = match parameters {
229-
None => None,
234+
None => {
235+
debug!("No parameters specified");
236+
None
237+
},
230238
Some(parameters) => {
239+
debug!("Parameters specified");
231240
match serde_json::from_str(parameters) {
232241
Ok(json) => Some(json),
233242
Err(_) => {

dsc/src/util.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use crate::args::{DscType, OutputFormat, TraceFormat, TraceLevel};
55

66
use atty::Stream;
7+
use crate::include::Include;
78
use dsc_lib::{
89
configure::{
910
config_doc::Configuration,
@@ -15,13 +16,11 @@ use dsc_lib::{
1516
},
1617
dscerror::DscError,
1718
dscresources::{
18-
dscresource::DscResource,
19-
invoke_result::{
19+
dscresource::DscResource, invoke_result::{
2020
GetResult,
2121
SetResult,
2222
TestResult,
23-
},
24-
resource_manifest::ResourceManifest
23+
}, resource_manifest::ResourceManifest
2524
}
2625
};
2726
use jsonschema::JSONSchema;
@@ -158,6 +157,9 @@ pub fn get_schema(dsc_type: DscType) -> RootSchema {
158157
DscType::ResourceManifest => {
159158
schema_for!(ResourceManifest)
160159
},
160+
DscType::Include => {
161+
schema_for!(Include)
162+
},
161163
DscType::Configuration => {
162164
schema_for!(Configuration)
163165
},

dsc_lib/src/configure/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,9 @@ impl Configurator {
509509
};
510510

511511
for (name, parameter) in parameters {
512+
debug!("Processing parameter '{name}'");
512513
if let Some(default_value) = &parameter.default_value {
514+
debug!("Set default parameter '{name}'");
513515
// default values can be expressions
514516
let value = if default_value.is_string() {
515517
if let Some(value) = default_value.as_str() {
@@ -526,15 +528,18 @@ impl Configurator {
526528
}
527529

528530
let Some(parameters_input) = parameters_input else {
531+
debug!("No parameters input");
529532
return Ok(());
530533
};
531534

535+
trace!("parameters_input: {parameters_input}");
532536
let parameters: HashMap<String, Value> = serde_json::from_value::<Input>(parameters_input.clone())?.parameters;
533537
let Some(parameters_constraints) = &config.parameters else {
534538
return Err(DscError::Validation("No parameters defined in configuration".to_string()));
535539
};
536540
for (name, value) in parameters {
537541
if let Some(constraint) = parameters_constraints.get(&name) {
542+
debug!("Validating parameter '{name}'");
538543
check_length(&name, &value, constraint)?;
539544
check_allowed_values(&name, &value, constraint)?;
540545
check_number_limits(&name, &value, constraint)?;

0 commit comments

Comments
 (0)