Skip to content

Commit 2fc0525

Browse files
authored
Merge pull request #429 from SteveL-MSFT/include
Add `Include` resource via new `Import` resource kind and `resolve` operation
2 parents 9f92b0d + a523997 commit 2fc0525

38 files changed

+983
-246
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"./dsc_lib/Cargo.toml",
55
"./osinfo/Cargo.toml",
66
"./registry/Cargo.toml",
7+
"./runcommandonset/Cargo.toml",
78
"./tools/test_group_resource/Cargo.toml",
89
"./tools/dsctest/Cargo.toml",
910
"./tree-sitter-dscexpression/Cargo.toml",

dsc/examples/include.dsc.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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
9+
parametersFile: osinfo.parameters.yaml

dsc/include.dsc.resource.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 contents into current configuration.",
6+
"kind": "Import",
7+
"resolve": {
8+
"executable": "dsc",
9+
"args": [
10+
"config",
11+
"resolve"
12+
],
13+
"input": "stdin"
14+
},
15+
"exitCodes": {
16+
"0": "Success",
17+
"1": "Invalid argument",
18+
"2": "Resource error",
19+
"3": "JSON Serialization error",
20+
"4": "Invalid input format",
21+
"5": "Resource instance failed schema validation",
22+
"6": "Command cancelled"
23+
},
24+
"schema": {
25+
"command": {
26+
"executable": "dsc",
27+
"args": [
28+
"schema",
29+
"--type",
30+
"include"
31+
]
32+
}
33+
}
34+
}

dsc/src/args.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub enum TraceFormat {
2121
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
2222
pub enum TraceLevel {
2323
Error,
24-
Warning,
24+
Warn,
2525
Info,
2626
Debug,
2727
Trace
@@ -33,8 +33,8 @@ pub struct Args {
3333
/// The subcommand to run
3434
#[clap(subcommand)]
3535
pub subcommand: SubCommand,
36-
#[clap(short = 'l', long, help = "Trace level to use", value_enum, default_value = "warning")]
37-
pub trace_level: TraceLevel,
36+
#[clap(short = 'l', long, help = "Trace level to use", value_enum)]
37+
pub trace_level: Option<TraceLevel>,
3838
#[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "default")]
3939
pub trace_format: TraceFormat,
4040
}
@@ -54,6 +54,7 @@ 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
},
@@ -119,6 +120,15 @@ pub enum ConfigSubCommand {
119120
path: Option<String>,
120121
#[clap(short = 'f', long, help = "The output format to use")]
121122
format: Option<OutputFormat>,
123+
},
124+
#[clap(name = "resolve", about = "Resolve the current configuration", hide = true)]
125+
Resolve {
126+
#[clap(short = 'd', long, help = "The document to pass to the configuration or resource", conflicts_with = "path")]
127+
document: Option<String>,
128+
#[clap(short = 'p', long, help = "The path to a file used as input to the configuration or resource", conflicts_with = "document")]
129+
path: Option<String>,
130+
#[clap(short = 'f', long, help = "The output format to use")]
131+
format: Option<OutputFormat>,
122132
}
123133
}
124134

@@ -203,8 +213,10 @@ pub enum DscType {
203213
GetResult,
204214
SetResult,
205215
TestResult,
216+
ResolveResult,
206217
DscResource,
207218
ResourceManifest,
219+
Include,
208220
Configuration,
209221
ConfigurationGetResult,
210222
ConfigurationSetResult,

dsc/src/main.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crossterm::event;
1616
use std::env;
1717

1818
pub mod args;
19+
pub mod resolve;
1920
pub mod resource_command;
2021
pub mod subcommand;
2122
pub mod tablewriter;
@@ -67,11 +68,11 @@ fn main() {
6768
},
6869
SubCommand::Config { subcommand, parameters, parameters_file, as_group } => {
6970
if let Some(file_name) = parameters_file {
70-
info!("Reading parameters from file {}", file_name);
71-
match std::fs::read_to_string(file_name) {
71+
info!("Reading parameters from file {file_name}");
72+
match std::fs::read_to_string(&file_name) {
7273
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group),
7374
Err(err) => {
74-
error!("Error: Failed to read parameters file: {err}");
75+
error!("Error: Failed to read parameters file '{file_name}': {err}");
7576
exit(util::EXIT_INVALID_INPUT);
7677
}
7778
}

dsc/src/resolve.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use dsc_lib::configure::config_doc::Configuration;
5+
use dsc_lib::util::parse_input_to_json;
6+
use schemars::JsonSchema;
7+
use serde::{Deserialize, Serialize};
8+
use std::io::Read;
9+
use std::fs::File;
10+
use std::path::{Path, PathBuf};
11+
use tracing::{debug, info};
12+
13+
use crate::util::DSC_CONFIG_ROOT;
14+
15+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
16+
pub struct Include {
17+
/// The path to the file to include. Path is relative to the file containing the include
18+
/// and not allowed to reference parent directories. If a configuration document is used
19+
/// instead of a file, then the path is relative to the current working directory.
20+
#[serde(rename = "configurationFile")]
21+
pub configuration_file: String,
22+
#[serde(rename = "parametersFile")]
23+
pub parameters_file: Option<String>,
24+
}
25+
26+
/// Read the file specified in the Include input and return the content as a JSON string.
27+
///
28+
/// # Arguments
29+
///
30+
/// * `input` - The Include input as a JSON string.
31+
///
32+
/// # Returns
33+
///
34+
/// A tuple containing the contents of the parameters file as JSON and the configuration content
35+
/// as a JSON string.
36+
///
37+
/// # Errors
38+
///
39+
/// This function will return an error if the Include input is not valid JSON, if the file
40+
/// specified in the Include input cannot be read, or if the content of the file cannot be
41+
/// deserialized as YAML or JSON.
42+
pub fn get_contents(input: &str) -> Result<(Option<String>, String), String> {
43+
debug!("Processing Include input");
44+
45+
// deserialize the Include input
46+
let include = match serde_json::from_str::<Include>(input) {
47+
Ok(include) => include,
48+
Err(err) => {
49+
return Err(format!("Error: Failed to deserialize Include input: {err}"));
50+
}
51+
};
52+
53+
let include_path = normalize_path(Path::new(&include.configuration_file))?;
54+
55+
// read the file specified in the Include input
56+
let mut buffer: Vec<u8> = Vec::new();
57+
match File::open(&include_path) {
58+
Ok(mut file) => {
59+
match file.read_to_end(&mut buffer) {
60+
Ok(_) => (),
61+
Err(err) => {
62+
return Err(format!("Error: Failed to read file '{include_path:?}': {err}"));
63+
}
64+
}
65+
},
66+
Err(err) => {
67+
return Err(format!("Error: Failed to open included file '{include_path:?}': {err}"));
68+
}
69+
}
70+
// convert the buffer to a string
71+
let include_content = match String::from_utf8(buffer) {
72+
Ok(input) => input,
73+
Err(err) => {
74+
return Err(format!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}"));
75+
}
76+
};
77+
78+
// try to deserialize the Include content as YAML first
79+
let configuration: Configuration = match serde_yaml::from_str(&include_content) {
80+
Ok(configuration) => configuration,
81+
Err(_err) => {
82+
// if that fails, try to deserialize it as JSON
83+
match serde_json::from_str(&include_content) {
84+
Ok(configuration) => configuration,
85+
Err(err) => {
86+
return Err(format!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}"));
87+
}
88+
}
89+
}
90+
};
91+
92+
// serialize the Configuration as JSON
93+
let config_json = match serde_json::to_string(&configuration) {
94+
Ok(json) => json,
95+
Err(err) => {
96+
return Err(format!("Error: JSON Error: {err}"));
97+
}
98+
};
99+
100+
let parameters = if let Some(parameters_file) = include.parameters_file {
101+
// combine the path with DSC_CONFIG_ROOT
102+
let parameters_file = normalize_path(Path::new(&parameters_file))?;
103+
info!("Resolving parameters from file '{parameters_file:?}'");
104+
match std::fs::read_to_string(&parameters_file) {
105+
Ok(parameters) => {
106+
let parameters_json = match parse_input_to_json(&parameters) {
107+
Ok(json) => json,
108+
Err(err) => {
109+
return Err(format!("Failed to parse parameters file '{parameters_file:?}' to JSON: {err}"));
110+
}
111+
};
112+
Some(parameters_json)
113+
},
114+
Err(err) => {
115+
return Err(format!("Failed to resolve parameters file '{parameters_file:?}': {err}"));
116+
}
117+
}
118+
} else {
119+
debug!("No parameters file found");
120+
None
121+
};
122+
123+
Ok((parameters, config_json))
124+
}
125+
126+
fn normalize_path(path: &Path) -> Result<PathBuf, String> {
127+
if path.is_absolute() {
128+
Ok(path.to_path_buf())
129+
} else {
130+
// check that no components of the path are '..'
131+
if path.components().any(|c| c == std::path::Component::ParentDir) {
132+
return Err(format!("Error: Include path must not contain '..': {path:?}"));
133+
}
134+
135+
// use DSC_CONFIG_ROOT env var as current directory
136+
let current_directory = match std::env::var(DSC_CONFIG_ROOT) {
137+
Ok(current_directory) => current_directory,
138+
Err(_err) => {
139+
// use current working directory
140+
match std::env::current_dir() {
141+
Ok(current_directory) => current_directory.to_string_lossy().into_owned(),
142+
Err(err) => {
143+
return Err(format!("Error: Failed to get current directory: {err}"));
144+
}
145+
}
146+
}
147+
};
148+
149+
// combine the current directory with the Include path
150+
Ok(Path::new(&current_directory).join(path))
151+
}
152+
}

dsc/src/resource_command.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,6 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, format: &Option<OutputF
261261

262262
#[must_use]
263263
pub fn get_resource<'a>(dsc: &'a DscManager, resource: &str) -> Option<&'a DscResource> {
264-
//TODO: add dinamically generated resource to dsc
265-
dsc.find_resource(String::from(resource).to_lowercase().as_str())
264+
//TODO: add dynamically generated resource to dsc
265+
dsc.find_resource(resource)
266266
}

0 commit comments

Comments
 (0)