diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 96ada198..07530eec 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -49,6 +49,8 @@ pub enum SubCommand { parameters: Option, #[clap(short = 'f', long, help = "Parameters to pass to the configuration as a JSON or YAML file", conflicts_with = "parameters")] parameters_file: Option, + #[clap(short = 'r', long, help = "Specify the operating system root path if not targeting the current running OS")] + system_root: Option, // Used to inform when DSC is used as a group resource to modify it's output #[clap(long, hide = true)] as_group: bool, diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 77adaa4c..1f578e30 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -68,11 +68,11 @@ 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); @@ -80,7 +80,7 @@ fn main() { } } else { - subcommand::config(&subcommand, ¶meters, &input, &as_group, &as_include); + subcommand::config(&subcommand, ¶meters, &system_root, &input, &as_group, &as_include); } }, SubCommand::Resource { subcommand } => { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 4b1783fe..dacdd457 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -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; @@ -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, as_group: &bool) @@ -186,7 +189,7 @@ fn initialize_config_root(path: &Option) -> Option { } #[allow(clippy::too_many_lines)] -pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, stdin: &Option, as_group: &bool, as_include: &bool) { +pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounted_path: &Option, stdin: &Option, as_group: &bool, as_include: &bool) { let (new_parameters, json_string) = match subcommand { ConfigSubCommand::Get { document, path, .. } | ConfigSubCommand::Set { document, path, .. } | @@ -270,6 +273,15 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, 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(¶meters) { error!("Error: Parameter input failure: {err}"); exit(EXIT_INVALID_INPUT); diff --git a/dsc/tests/dsc_args.tests.ps1 b/dsc/tests/dsc_args.tests.ps1 index 70752a40..5b51b527 100644 --- a/dsc/tests/dsc_args.tests.ps1 +++ b/dsc/tests/dsc_args.tests.ps1 @@ -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" + } } diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 53db473a..7deef03d 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -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() 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 + } } diff --git a/dsc_lib/src/configure/context.rs b/dsc_lib/src/configure/context.rs index 0ce678fe..49e90b65 100644 --- a/dsc_lib/src/configure/context.rs +++ b/dsc_lib/src/configure/context.rs @@ -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, // this is used by the `reference()` function to retrieve output + pub system_root: PathBuf, pub parameters: HashMap, pub security_context: SecurityContextKind, pub variables: HashMap, @@ -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, @@ -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("/") +} diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 3a5d84a3..41bfb6f8 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -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; @@ -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 diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 5d631877..909d73f9 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -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. @@ -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, diff --git a/dsc_lib/src/functions/path.rs b/dsc_lib/src/functions/path.rs new file mode 100644 index 00000000..db439d6c --- /dev/null +++ b/dsc_lib/src/functions/path.rs @@ -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 { + vec![AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + 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")); + } +} diff --git a/dsc_lib/src/functions/system_root.rs b/dsc_lib/src/functions/system_root.rs new file mode 100644 index 00000000..cf885c67 --- /dev/null +++ b/dsc_lib/src/functions/system_root.rs @@ -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 { + vec![AcceptedArgKind::String] + } + + fn invoke(&self, _args: &[Value], context: &Context) -> Result { + 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")); + } + +}