Skip to content

Commit 51ebbe3

Browse files
authored
Merge pull request #589 from SteveL-MSFT/mounted-path
Add `--system-root` parameter, `systemRoot()` and `path()` functions to dsc
2 parents 22dbb57 + 6be82a1 commit 51ebbe3

File tree

10 files changed

+224
-9
lines changed

10 files changed

+224
-9
lines changed

dsc/src/args.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub enum SubCommand {
4949
parameters: Option<String>,
5050
#[clap(short = 'f', long, help = "Parameters to pass to the configuration as a JSON or YAML file", conflicts_with = "parameters")]
5151
parameters_file: Option<String>,
52+
#[clap(short = 'r', long, help = "Specify the operating system root path if not targeting the current running OS")]
53+
system_root: Option<String>,
5254
// Used to inform when DSC is used as a group resource to modify it's output
5355
#[clap(long, hide = true)]
5456
as_group: bool,

dsc/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,19 @@ fn main() {
6868
let mut cmd = Args::command();
6969
generate(shell, &mut cmd, "dsc", &mut io::stdout());
7070
},
71-
SubCommand::Config { subcommand, parameters, parameters_file, as_group, as_include } => {
71+
SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_include } => {
7272
if let Some(file_name) = parameters_file {
7373
info!("Reading parameters from file {file_name}");
7474
match std::fs::read_to_string(&file_name) {
75-
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group, &as_include),
75+
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &system_root, &input, &as_group, &as_include),
7676
Err(err) => {
7777
error!("Error: Failed to read parameters file '{file_name}': {err}");
7878
exit(util::EXIT_INVALID_INPUT);
7979
}
8080
}
8181
}
8282
else {
83-
subcommand::config(&subcommand, &parameters, &input, &as_group, &as_include);
83+
subcommand::config(&subcommand, &parameters, &system_root, &input, &as_group, &as_include);
8484
}
8585
},
8686
SubCommand::Resource { subcommand } => {

dsc/src/subcommand.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand};
55
use crate::resolve::{get_contents, Include};
66
use crate::resource_command::{get_resource, self};
77
use crate::tablewriter::Table;
8-
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};
8+
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};
99
use dsc_lib::configure::{Configurator, config_doc::{Configuration, ExecutionKind}, config_result::ResourceGetResult};
1010
use dsc_lib::dscerror::DscError;
1111
use dsc_lib::dscresources::invoke_result::ResolveResult;
@@ -15,9 +15,12 @@ use dsc_lib::{
1515
dscresources::dscresource::{Capability, ImplementedAs, Invoke},
1616
dscresources::resource_manifest::{import_manifest, ResourceManifest},
1717
};
18-
use std::collections::HashMap;
19-
use std::io::{self, IsTerminal};
20-
use std::process::exit;
18+
use std::{
19+
collections::HashMap,
20+
io::{self, IsTerminal},
21+
path::Path,
22+
process::exit
23+
};
2124
use tracing::{debug, error, trace};
2225

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

188191
#[allow(clippy::too_many_lines)]
189-
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin: &Option<String>, as_group: &bool, as_include: &bool) {
192+
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, mounted_path: &Option<String>, stdin: &Option<String>, as_group: &bool, as_include: &bool) {
190193
let (new_parameters, json_string) = match subcommand {
191194
ConfigSubCommand::Get { document, path, .. } |
192195
ConfigSubCommand::Set { document, path, .. } |
@@ -270,6 +273,15 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
270273
}
271274
};
272275

276+
if let Some(path) = mounted_path {
277+
if !Path::new(&path).exists() {
278+
error!("Error: Target path '{path}' does not exist");
279+
exit(EXIT_INVALID_ARGS);
280+
}
281+
282+
configurator.set_system_root(path);
283+
}
284+
273285
if let Err(err) = configurator.set_context(&parameters) {
274286
error!("Error: Parameter input failure: {err}");
275287
exit(EXIT_INVALID_INPUT);

dsc/tests/dsc_args.tests.ps1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,10 @@ resources:
308308
$LASTEXITCODE | Should -Be 2
309309
"$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Can not perform this operation on the adapter'
310310
}
311+
312+
It 'Invalid --system-root' {
313+
dsc config --system-root /invalid/path get -p "$PSScriptRoot/../examples/groups.dsc.yaml" 2> $TestDrive/tracing.txt
314+
$LASTEXITCODE | Should -Be 1
315+
"$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Target path '/invalid/path' does not exist"
316+
}
311317
}

dsc/tests/dsc_functions.tests.ps1

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,42 @@ Describe 'tests for function expressions' {
2424
$out = $config_yaml | dsc config get | ConvertFrom-Json
2525
$out.results[0].result.actualState.output | Should -Be $expected
2626
}
27+
28+
It 'path(<path>) works' -TestCases @(
29+
@{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" }
30+
@{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" }
31+
) {
32+
param($path, $expected)
33+
34+
$config_yaml = @"
35+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
36+
resources:
37+
- name: Echo
38+
type: Microsoft.DSC.Debug/Echo
39+
properties:
40+
output: "[path($path)]"
41+
"@
42+
$out = $config_yaml | dsc config --system-root $PSHOME get | ConvertFrom-Json
43+
$out.results[0].result.actualState.output | Should -BeExactly $expected
44+
}
45+
46+
It 'default systemRoot() is correct for the OS' {
47+
$config_yaml = @'
48+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
49+
resources:
50+
- name: Echo
51+
type: Microsoft.DSC.Debug/Echo
52+
properties:
53+
output: "[systemRoot()]"
54+
'@
55+
56+
$expected = if ($IsWindows) {
57+
$env:SYSTEMDRIVE
58+
} else {
59+
'/'
60+
}
61+
$out = $config_yaml | dsc config get | ConvertFrom-Json
62+
$LASTEXITCODE | Should -Be 0
63+
$out.results[0].result.actualState.output | Should -BeExactly $expected
64+
}
2765
}

dsc_lib/src/configure/context.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ use chrono::{DateTime, Local};
55
use crate::configure::config_doc::ExecutionKind;
66
use security_context_lib::{get_security_context, SecurityContext};
77
use serde_json::Value;
8-
use std::collections::HashMap;
8+
use std::{collections::HashMap, path::PathBuf};
99

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

1212
pub struct Context {
1313
pub execution_type: ExecutionKind,
1414
pub outputs: HashMap<String, Value>, // this is used by the `reference()` function to retrieve output
15+
pub system_root: PathBuf,
1516
pub parameters: HashMap<String, (Value, DataType)>,
1617
pub security_context: SecurityContextKind,
1718
pub variables: HashMap<String, Value>,
@@ -24,6 +25,7 @@ impl Context {
2425
Self {
2526
execution_type: ExecutionKind::Actual,
2627
outputs: HashMap::new(),
28+
system_root: get_default_os_system_root(),
2729
parameters: HashMap::new(),
2830
security_context: match get_security_context() {
2931
SecurityContext::Admin => SecurityContextKind::Elevated,
@@ -40,3 +42,15 @@ impl Default for Context {
4042
Self::new()
4143
}
4244
}
45+
46+
#[cfg(target_os = "windows")]
47+
fn get_default_os_system_root() -> PathBuf {
48+
// use SYSTEMDRIVE env var to get the default target path
49+
let system_drive = std::env::var("SYSTEMDRIVE").unwrap_or_else(|_| "C:".to_string());
50+
PathBuf::from(system_drive)
51+
}
52+
53+
#[cfg(not(target_os = "windows"))]
54+
fn get_default_os_system_root() -> PathBuf {
55+
PathBuf::from("/")
56+
}

dsc_lib/src/configure/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use self::contraints::{check_length, check_number_limits, check_allowed_values};
2020
use indicatif::ProgressStyle;
2121
use security_context_lib::{SecurityContext, get_security_context};
2222
use serde_json::{Map, Value};
23+
use std::path::PathBuf;
2324
use std::{collections::HashMap, mem};
2425
use tracing::{debug, info, trace, warn_span, Span};
2526
use tracing_indicatif::span_ext::IndicatifSpanExt;
@@ -479,6 +480,15 @@ impl Configurator {
479480
Ok(result)
480481
}
481482

483+
/// Set the mounted path for the configuration.
484+
///
485+
/// # Arguments
486+
///
487+
/// * `system_root` - The system root to set.
488+
pub fn set_system_root(&mut self, system_root: &str) {
489+
self.context.system_root = PathBuf::from(system_root);
490+
}
491+
482492
/// Set the parameters and variables context for the configuration.
483493
///
484494
/// # Arguments

dsc_lib/src/functions/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ pub mod min;
1919
pub mod mod_function;
2020
pub mod mul;
2121
pub mod parameters;
22+
pub mod path;
2223
pub mod reference;
2324
pub mod resource_id;
2425
pub mod sub;
26+
pub mod system_root;
2527
pub mod variables;
2628

2729
/// The kind of argument that a function accepts.
@@ -76,9 +78,11 @@ impl FunctionDispatcher {
7678
functions.insert("mod".to_string(), Box::new(mod_function::Mod{}));
7779
functions.insert("mul".to_string(), Box::new(mul::Mul{}));
7880
functions.insert("parameters".to_string(), Box::new(parameters::Parameters{}));
81+
functions.insert("path".to_string(), Box::new(path::Path{}));
7982
functions.insert("reference".to_string(), Box::new(reference::Reference{}));
8083
functions.insert("resourceId".to_string(), Box::new(resource_id::ResourceId{}));
8184
functions.insert("sub".to_string(), Box::new(sub::Sub{}));
85+
functions.insert("systemRoot".to_string(), Box::new(system_root::SystemRoot{}));
8286
functions.insert("variables".to_string(), Box::new(variables::Variables{}));
8387
Self {
8488
functions,

dsc_lib/src/functions/path.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{AcceptedArgKind, Function};
7+
use serde_json::Value;
8+
use std::path::PathBuf;
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct Path {}
13+
14+
/// Implements the `path` function.
15+
/// Accepts a variable number of arguments, each of which is a string.
16+
/// Returns a string that is the concatenation of the arguments, separated by the platform's path separator.
17+
impl Function for Path {
18+
fn min_args(&self) -> usize {
19+
2
20+
}
21+
22+
fn max_args(&self) -> usize {
23+
usize::MAX
24+
}
25+
26+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
27+
vec![AcceptedArgKind::String]
28+
}
29+
30+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
31+
debug!("Executing path function with args: {:?}", args);
32+
33+
let mut path = PathBuf::new();
34+
for arg in args {
35+
if let Value::String(s) = arg {
36+
path.push(s);
37+
} else {
38+
return Err(DscError::Parser("Arguments must all be strings".to_string()));
39+
}
40+
}
41+
42+
Ok(Value::String(path.to_string_lossy().to_string()))
43+
}
44+
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use crate::configure::context::Context;
49+
use crate::parser::Statement;
50+
51+
#[test]
52+
fn two_args() {
53+
let mut parser = Statement::new().unwrap();
54+
let separator = std::path::MAIN_SEPARATOR;
55+
let result = parser.parse_and_execute("[path('a','b')]", &Context::new()).unwrap();
56+
assert_eq!(result, format!("a{separator}b"));
57+
}
58+
59+
#[test]
60+
fn three_args() {
61+
let mut parser = Statement::new().unwrap();
62+
let separator = std::path::MAIN_SEPARATOR;
63+
let result = parser.parse_and_execute("[path('a','b','c')]", &Context::new()).unwrap();
64+
assert_eq!(result, format!("a{separator}b{separator}c"));
65+
}
66+
}

dsc_lib/src/functions/system_root.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{AcceptedArgKind, Function};
7+
use serde_json::Value;
8+
use tracing::debug;
9+
10+
#[derive(Debug, Default)]
11+
pub struct SystemRoot {}
12+
13+
/// Implements the `systemRoot` function.
14+
/// This function returns the value of the specified system root path.
15+
impl Function for SystemRoot {
16+
fn min_args(&self) -> usize {
17+
0
18+
}
19+
20+
fn max_args(&self) -> usize {
21+
0
22+
}
23+
24+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
25+
vec![AcceptedArgKind::String]
26+
}
27+
28+
fn invoke(&self, _args: &[Value], context: &Context) -> Result<Value, DscError> {
29+
debug!("Executing targetPath function");
30+
31+
Ok(Value::String(context.system_root.to_string_lossy().to_string()))
32+
}
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use crate::configure::context::Context;
38+
use crate::parser::Statement;
39+
use std::path::PathBuf;
40+
41+
#[test]
42+
fn init() {
43+
let mut parser = Statement::new().unwrap();
44+
let result = parser.parse_and_execute("[systemRoot()]", &Context::new()).unwrap();
45+
// on windows, the default is SYSTEMDRIVE env var
46+
#[cfg(target_os = "windows")]
47+
assert_eq!(result, std::env::var("SYSTEMDRIVE").unwrap());
48+
// on linux/macOS, the default is /
49+
#[cfg(not(target_os = "windows"))]
50+
assert_eq!(result, "/");
51+
}
52+
53+
#[test]
54+
fn simple() {
55+
let mut parser = Statement::new().unwrap();
56+
let mut context = Context::new();
57+
let separator = std::path::MAIN_SEPARATOR;
58+
context.system_root = PathBuf::from(format!("{separator}mnt"));
59+
let result = parser.parse_and_execute("[systemRoot()]", &context).unwrap();
60+
assert_eq!(result, format!("{separator}mnt"));
61+
}
62+
63+
}

0 commit comments

Comments
 (0)