Skip to content

Commit

Permalink
Merge pull request #589 from SteveL-MSFT/mounted-path
Browse files Browse the repository at this point in the history
Add `--system-root` parameter, `systemRoot()` and `path()` functions  to dsc
  • Loading branch information
SteveL-MSFT authored Nov 17, 2024
2 parents 22dbb57 + 6be82a1 commit 51ebbe3
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 9 deletions.
2 changes: 2 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ 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>,
#[clap(short = 'r', long, help = "Specify the operating system root path if not targeting the current running OS")]
system_root: 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
6 changes: 3 additions & 3 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,19 @@ fn main() {
let mut cmd = Args::command();
generate(shell, &mut cmd, "dsc", &mut io::stdout());
},
SubCommand::Config { subcommand, parameters, parameters_file, as_group, as_include } => {
SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_include } => {
if let Some(file_name) = parameters_file {
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, &as_include),
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &system_root, &input, &as_group, &as_include),
Err(err) => {
error!("Error: Failed to read parameters file '{file_name}': {err}");
exit(util::EXIT_INVALID_INPUT);
}
}
}
else {
subcommand::config(&subcommand, &parameters, &input, &as_group, &as_include);
subcommand::config(&subcommand, &parameters, &system_root, &input, &as_group, &as_include);
}
},
SubCommand::Resource { subcommand } => {
Expand Down
22 changes: 17 additions & 5 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand};
use crate::resolve::{get_contents, Include};
use crate::resource_command::{get_resource, self};
use crate::tablewriter::Table;
use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, get_schema, write_output, get_input, set_dscconfigroot, validate_json};
use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, get_schema, write_output, get_input, set_dscconfigroot, validate_json};
use dsc_lib::configure::{Configurator, config_doc::{Configuration, ExecutionKind}, config_result::ResourceGetResult};
use dsc_lib::dscerror::DscError;
use dsc_lib::dscresources::invoke_result::ResolveResult;
Expand All @@ -15,9 +15,12 @@ use dsc_lib::{
dscresources::dscresource::{Capability, ImplementedAs, Invoke},
dscresources::resource_manifest::{import_manifest, ResourceManifest},
};
use std::collections::HashMap;
use std::io::{self, IsTerminal};
use std::process::exit;
use std::{
collections::HashMap,
io::{self, IsTerminal},
path::Path,
process::exit
};
use tracing::{debug, error, trace};

pub fn config_get(configurator: &mut Configurator, format: &Option<OutputFormat>, as_group: &bool)
Expand Down Expand Up @@ -186,7 +189,7 @@ fn initialize_config_root(path: &Option<String>) -> Option<String> {
}

#[allow(clippy::too_many_lines)]
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin: &Option<String>, as_group: &bool, as_include: &bool) {
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, mounted_path: &Option<String>, stdin: &Option<String>, as_group: &bool, as_include: &bool) {
let (new_parameters, json_string) = match subcommand {
ConfigSubCommand::Get { document, path, .. } |
ConfigSubCommand::Set { document, path, .. } |
Expand Down Expand Up @@ -270,6 +273,15 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
}
};

if let Some(path) = mounted_path {
if !Path::new(&path).exists() {
error!("Error: Target path '{path}' does not exist");
exit(EXIT_INVALID_ARGS);
}

configurator.set_system_root(path);
}

if let Err(err) = configurator.set_context(&parameters) {
error!("Error: Parameter input failure: {err}");
exit(EXIT_INVALID_INPUT);
Expand Down
6 changes: 6 additions & 0 deletions dsc/tests/dsc_args.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,10 @@ resources:
$LASTEXITCODE | Should -Be 2
"$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Can not perform this operation on the adapter'
}

It 'Invalid --system-root' {
dsc config --system-root /invalid/path get -p "$PSScriptRoot/../examples/groups.dsc.yaml" 2> $TestDrive/tracing.txt
$LASTEXITCODE | Should -Be 1
"$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Target path '/invalid/path' does not exist"
}
}
38 changes: 38 additions & 0 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,42 @@ Describe 'tests for function expressions' {
$out = $config_yaml | dsc config get | ConvertFrom-Json
$out.results[0].result.actualState.output | Should -Be $expected
}

It 'path(<path>) works' -TestCases @(
@{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" }
@{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" }
) {
param($path, $expected)

$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[path($path)]"
"@
$out = $config_yaml | dsc config --system-root $PSHOME get | ConvertFrom-Json
$out.results[0].result.actualState.output | Should -BeExactly $expected
}

It 'default systemRoot() is correct for the OS' {
$config_yaml = @'
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[systemRoot()]"
'@

$expected = if ($IsWindows) {
$env:SYSTEMDRIVE
} else {
'/'
}
$out = $config_yaml | dsc config get | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -BeExactly $expected
}
}
16 changes: 15 additions & 1 deletion dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ use chrono::{DateTime, Local};
use crate::configure::config_doc::ExecutionKind;
use security_context_lib::{get_security_context, SecurityContext};
use serde_json::Value;
use std::collections::HashMap;
use std::{collections::HashMap, path::PathBuf};

use super::config_doc::{DataType, SecurityContextKind};

pub struct Context {
pub execution_type: ExecutionKind,
pub outputs: HashMap<String, Value>, // this is used by the `reference()` function to retrieve output
pub system_root: PathBuf,
pub parameters: HashMap<String, (Value, DataType)>,
pub security_context: SecurityContextKind,
pub variables: HashMap<String, Value>,
Expand All @@ -24,6 +25,7 @@ impl Context {
Self {
execution_type: ExecutionKind::Actual,
outputs: HashMap::new(),
system_root: get_default_os_system_root(),
parameters: HashMap::new(),
security_context: match get_security_context() {
SecurityContext::Admin => SecurityContextKind::Elevated,
Expand All @@ -40,3 +42,15 @@ impl Default for Context {
Self::new()
}
}

#[cfg(target_os = "windows")]
fn get_default_os_system_root() -> PathBuf {
// use SYSTEMDRIVE env var to get the default target path
let system_drive = std::env::var("SYSTEMDRIVE").unwrap_or_else(|_| "C:".to_string());
PathBuf::from(system_drive)
}

#[cfg(not(target_os = "windows"))]
fn get_default_os_system_root() -> PathBuf {
PathBuf::from("/")
}
10 changes: 10 additions & 0 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use self::contraints::{check_length, check_number_limits, check_allowed_values};
use indicatif::ProgressStyle;
use security_context_lib::{SecurityContext, get_security_context};
use serde_json::{Map, Value};
use std::path::PathBuf;
use std::{collections::HashMap, mem};
use tracing::{debug, info, trace, warn_span, Span};
use tracing_indicatif::span_ext::IndicatifSpanExt;
Expand Down Expand Up @@ -479,6 +480,15 @@ impl Configurator {
Ok(result)
}

/// Set the mounted path for the configuration.
///
/// # Arguments
///
/// * `system_root` - The system root to set.
pub fn set_system_root(&mut self, system_root: &str) {
self.context.system_root = PathBuf::from(system_root);
}

/// Set the parameters and variables context for the configuration.
///
/// # Arguments
Expand Down
4 changes: 4 additions & 0 deletions dsc_lib/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ pub mod min;
pub mod mod_function;
pub mod mul;
pub mod parameters;
pub mod path;
pub mod reference;
pub mod resource_id;
pub mod sub;
pub mod system_root;
pub mod variables;

/// The kind of argument that a function accepts.
Expand Down Expand Up @@ -76,9 +78,11 @@ impl FunctionDispatcher {
functions.insert("mod".to_string(), Box::new(mod_function::Mod{}));
functions.insert("mul".to_string(), Box::new(mul::Mul{}));
functions.insert("parameters".to_string(), Box::new(parameters::Parameters{}));
functions.insert("path".to_string(), Box::new(path::Path{}));
functions.insert("reference".to_string(), Box::new(reference::Reference{}));
functions.insert("resourceId".to_string(), Box::new(resource_id::ResourceId{}));
functions.insert("sub".to_string(), Box::new(sub::Sub{}));
functions.insert("systemRoot".to_string(), Box::new(system_root::SystemRoot{}));
functions.insert("variables".to_string(), Box::new(variables::Variables{}));
Self {
functions,
Expand Down
66 changes: 66 additions & 0 deletions dsc_lib/src/functions/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{AcceptedArgKind, Function};
use serde_json::Value;
use std::path::PathBuf;
use tracing::debug;

#[derive(Debug, Default)]
pub struct Path {}

/// Implements the `path` function.
/// Accepts a variable number of arguments, each of which is a string.
/// Returns a string that is the concatenation of the arguments, separated by the platform's path separator.
impl Function for Path {
fn min_args(&self) -> usize {
2
}

fn max_args(&self) -> usize {
usize::MAX
}

fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
vec![AcceptedArgKind::String]
}

fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
debug!("Executing path function with args: {:?}", args);

let mut path = PathBuf::new();
for arg in args {
if let Value::String(s) = arg {
path.push(s);
} else {
return Err(DscError::Parser("Arguments must all be strings".to_string()));
}
}

Ok(Value::String(path.to_string_lossy().to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;

#[test]
fn two_args() {
let mut parser = Statement::new().unwrap();
let separator = std::path::MAIN_SEPARATOR;
let result = parser.parse_and_execute("[path('a','b')]", &Context::new()).unwrap();
assert_eq!(result, format!("a{separator}b"));
}

#[test]
fn three_args() {
let mut parser = Statement::new().unwrap();
let separator = std::path::MAIN_SEPARATOR;
let result = parser.parse_and_execute("[path('a','b','c')]", &Context::new()).unwrap();
assert_eq!(result, format!("a{separator}b{separator}c"));
}
}
63 changes: 63 additions & 0 deletions dsc_lib/src/functions/system_root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{AcceptedArgKind, Function};
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct SystemRoot {}

/// Implements the `systemRoot` function.
/// This function returns the value of the specified system root path.
impl Function for SystemRoot {
fn min_args(&self) -> usize {
0
}

fn max_args(&self) -> usize {
0
}

fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
vec![AcceptedArgKind::String]
}

fn invoke(&self, _args: &[Value], context: &Context) -> Result<Value, DscError> {
debug!("Executing targetPath function");

Ok(Value::String(context.system_root.to_string_lossy().to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;
use std::path::PathBuf;

#[test]
fn init() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[systemRoot()]", &Context::new()).unwrap();
// on windows, the default is SYSTEMDRIVE env var
#[cfg(target_os = "windows")]
assert_eq!(result, std::env::var("SYSTEMDRIVE").unwrap());
// on linux/macOS, the default is /
#[cfg(not(target_os = "windows"))]
assert_eq!(result, "/");
}

#[test]
fn simple() {
let mut parser = Statement::new().unwrap();
let mut context = Context::new();
let separator = std::path::MAIN_SEPARATOR;
context.system_root = PathBuf::from(format!("{separator}mnt"));
let result = parser.parse_and_execute("[systemRoot()]", &context).unwrap();
assert_eq!(result, format!("{separator}mnt"));
}

}

0 comments on commit 51ebbe3

Please sign in to comment.