Skip to content

Commit

Permalink
Merge pull request #429 from SteveL-MSFT/include
Browse files Browse the repository at this point in the history
Add `Include` resource via new `Import` resource kind and `resolve` operation
  • Loading branch information
SteveL-MSFT authored May 10, 2024
2 parents 9f92b0d + a523997 commit 2fc0525
Show file tree
Hide file tree
Showing 38 changed files with 983 additions and 246 deletions.
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://raw.githubusercontent.com/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://raw.githubusercontent.com/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>,
#[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) {
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))
}
}
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

0 comments on commit 2fc0525

Please sign in to comment.