diff --git a/dsc/examples/osinfo.parameters.json b/dsc/examples/osinfo.parameters.json new file mode 100644 index 00000000..a302ff64 --- /dev/null +++ b/dsc/examples/osinfo.parameters.json @@ -0,0 +1,5 @@ +{ + "parameters": { + "osFamily": "macOS" + } +} diff --git a/dsc/examples/osinfo_parameters.dsc.json b/dsc/examples/osinfo_parameters.dsc.json new file mode 100644 index 00000000..1b9fc84d --- /dev/null +++ b/dsc/examples/osinfo_parameters.dsc.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", + "parameters": { + "osFamily": { + "type": "string", + "defaultValue": "[concat('Win','dows')]", + "allowedValues": [ + "Windows", + "Linux", + "macOS" + ] + } + }, + "resources": [ + { + "name": "os", + "type": "Microsoft/OSInfo", + "properties": { + "family": "[parameters('osFamily')]" + } + }, + { + "name": "another os instance", + "type": "Microsoft/OSInfo", + "properties": { + "family": "macOS" + } + }, + { + "name": "path", + "type": "Microsoft.DSC.Debug/Echo", + "properties": { + "output": "[envvar('PATH')]" + } + } + ] +} diff --git a/dsc/src/main.rs b/dsc/src/main.rs index f3c0d1f2..983804fa 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -102,7 +102,7 @@ fn ctrlc_handler() { fn terminate_subprocesses(sys: &System, process: &Process) { info!("{}: {:?} {}", t!("main.terminatingSubprocess"), process.name(), process.pid()); - for subprocess in sys.processes().values().filter(|p| p.parent().map_or(false, |parent| parent == process.pid())) { + for subprocess in sys.processes().values().filter(|p| p.parent().is_some_and(|parent| parent == process.pid())) { terminate_subprocesses(sys, subprocess); } @@ -157,7 +157,7 @@ fn check_store() { }; // MS Store runs app using `sihost.exe` - if parent_process.name().to_ascii_lowercase() == "sihost.exe" || parent_process.name().to_ascii_lowercase() == "explorer.exe"{ + if parent_process.name().eq_ignore_ascii_case("sihost.exe") || parent_process.name().eq_ignore_ascii_case("explorer.exe") { eprintln!("{}", t!("main.storeMessage")); // wait for keypress let _ = io::stdin().read(&mut [0u8]).unwrap(); diff --git a/dsc/src/resolve.rs b/dsc/src/resolve.rs index 711cf661..fad893df 100644 --- a/dsc/src/resolve.rs +++ b/dsc/src/resolve.rs @@ -1,7 +1,6 @@ // 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 rust_i18n::t; use schemars::JsonSchema; @@ -14,14 +13,30 @@ use tracing::{debug, info}; use crate::util::DSC_CONFIG_ROOT; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -pub struct Include { +pub enum IncludeKind { /// 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, + ConfigurationFile(String), + #[serde(rename = "configurationContent")] + ConfigurationContent(String), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub enum IncludeParametersKind { #[serde(rename = "parametersFile")] - pub parameters_file: Option, + ParametersFile(String), + #[serde(rename = "parametersContent")] + ParametersContent(String), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct Include { + #[serde(flatten)] + pub configuration: IncludeKind, + #[serde(flatten)] + pub parameters: Option, } /// Read the file specified in the Include input and return the content as a JSON string. @@ -51,74 +66,83 @@ pub fn get_contents(input: &str) -> Result<(Option, String), String> { } }; - let include_path = normalize_path(Path::new(&include.configuration_file))?; + let config_json = match include.configuration { + IncludeKind::ConfigurationFile(file_path) => { + let include_path = normalize_path(Path::new(&file_path))?; - // read the file specified in the Include input - let mut buffer: Vec = Vec::new(); - match File::open(&include_path) { - Ok(mut file) => { - match file.read_to_end(&mut buffer) { - Ok(_) => (), + // read the file specified in the Include input + let mut buffer: Vec = Vec::new(); + match File::open(&include_path) { + Ok(mut file) => { + match file.read_to_end(&mut buffer) { + Ok(_) => (), + Err(err) => { + return Err(format!("{} '{include_path:?}': {err}", t!("resolve.failedToReadFile"))); + } + } + }, Err(err) => { - return Err(format!("{} '{include_path:?}': {err}", t!("resolve.failedToReadFile"))); + return Err(format!("{} '{include_path:?}': {err}", t!("resolve.failedToOpenFile"))); } } - }, - Err(err) => { - return Err(format!("{} '{include_path:?}': {err}", t!("resolve.failedToOpenFile"))); - } - } - // convert the buffer to a string - let include_content = match String::from_utf8(buffer) { - Ok(input) => input, - Err(err) => { - return Err(format!("{} '{include_path:?}': {err}", t!("resolve.invalidFileContent"))); - } - }; + // convert the buffer to a string + let include_content = match String::from_utf8(buffer) { + Ok(input) => input, + Err(err) => { + return Err(format!("{} '{include_path:?}': {err}", t!("resolve.invalidFileContent"))); + } + }; - // 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, + match parse_input_to_json(&include_content) { + Ok(json) => json, Err(err) => { return Err(format!("{} '{include_path:?}': {err}", t!("resolve.invalidFile"))); } } + }, + IncludeKind::ConfigurationContent(text) => { + match parse_input_to_json(&text) { + Ok(json) => json, + Err(err) => { + return Err(format!("{}: {err}", t!("resolve.invalidFile"))); + } + } } }; - // serialize the Configuration as JSON - let config_json = match serde_json::to_string(&configuration) { - Ok(json) => json, - Err(err) => { - return Err(format!("JSON: {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(¶meters_file))?; - info!("{} '{parameters_file:?}'", t!("resolve.resolvingParameters")); - match std::fs::read_to_string(¶meters_file) { - Ok(parameters) => { - let parameters_json = match parse_input_to_json(¶meters) { - Ok(json) => json, - Err(err) => { - return Err(format!("{} '{parameters_file:?}': {err}", t!("resolve.failedParseParametersFile"))); - } - }; - Some(parameters_json) - }, - Err(err) => { - return Err(format!("{} '{parameters_file:?}': {err}", t!("resolve.failedResolveParametersFile"))); + let parameters = match include.parameters { + Some(IncludeParametersKind::ParametersFile(file_path)) => { + // combine the path with DSC_CONFIG_ROOT + let parameters_file = normalize_path(Path::new(&file_path))?; + info!("{} '{parameters_file:?}'", t!("resolve.resolvingParameters")); + match std::fs::read_to_string(¶meters_file) { + Ok(parameters) => { + let parameters_json = match parse_input_to_json(¶meters) { + Ok(json) => json, + Err(err) => { + return Err(format!("{} '{parameters_file:?}': {err}", t!("resolve.failedParseParametersFile"))); + } + }; + Some(parameters_json) + }, + Err(err) => { + return Err(format!("{} '{parameters_file:?}': {err}", t!("resolve.failedResolveParametersFile"))); + } } + }, + Some(IncludeParametersKind::ParametersContent(text)) => { + let parameters_json = match parse_input_to_json(&text) { + Ok(json) => json, + Err(err) => { + return Err(format!("{}: {err}", t!("resolve.invalidParametersContent"))); + } + }; + Some(parameters_json) + }, + None => { + debug!("{}", t!("resolve.noParameters")); + None } - } else { - debug!("{}", t!("resolve.noParametersFile")); - None }; Ok((parameters, config_json)) diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index 8c063927..e0f45438 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -12,7 +12,34 @@ Describe 'Include tests' { $logPath = Join-Path $TestDrive 'stderr.log' } - It 'Include config with default parameters' { + It 'Include invalid config file' { + $invalidConfig = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + properties: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: include/non-existing.dsc.yaml +"@ + + $invalidConfigPath = Join-Path $TestDrive 'invalid_config.dsc.yaml' + $invalidConfig | Set-Content -Path $invalidConfigPath + + $config = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: $invalidConfigPath +"@ + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $config | Set-Content -Path $configPath + dsc config get -f $configPath + $LASTEXITCODE | Should -Be 2 + } + + It 'Include config file with default parameters' { $config = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json resources: @@ -35,6 +62,63 @@ Describe 'Include tests' { $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS } + It 'Include config YAML content with default parameters' { + # since this is YAML, we need to ensure correct indentation + $includeContent = (Get-Content $osinfoConfigPath -Raw).Replace("`n", "`n" + (' ' * 20)) + + $config = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationContent: | + $includeContent +"@ + + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $config | Set-Content -Path $configPath + $out = dsc config get -f $configPath | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($IsWindows) { + $expectedOS = 'Windows' + } elseif ($IsLinux) { + $expectedOS = 'Linux' + } else { + $expectedOS = 'macOS' + } + $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS + } + + It 'Include config JSON content with default parameters' { + $osinfoJsonPath = Join-Path $PSScriptRoot '../examples/osinfo_parameters.dsc.json' + + # for JSON, we can just have it as a single line + $includeContent = (Get-Content $osinfoJsonPath -Raw).Replace("`n", "").Replace('"', '\"') + + $config = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationContent: "$includeContent" +"@ + + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $config | Set-Content -Path $configPath + $out = dsc config get -f $configPath | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($IsWindows) { + $expectedOS = 'Windows' + } elseif ($IsLinux) { + $expectedOS = 'Linux' + } else { + $expectedOS = 'macOS' + } + $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS + } + It 'Include config with parameters file' { $config = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json @@ -59,6 +143,33 @@ Describe 'Include tests' { $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS } + It 'Include config with parameters content' { + $parametersContentFile = Join-Path $PSScriptRoot '../examples/osinfo.parameters.json' + $parametersContent = (Get-Content $parametersContentFile -Raw).Replace("`n", "").Replace('"', '\"') + + $config = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + - name: osinfo + type: Microsoft.DSC/Include + properties: + configurationFile: include/osinfo_parameters.dsc.yaml + parametersContent: "$parametersContent" +"@ + $configPath = Join-Path $TestDrive 'config.dsc.yaml' + $config | Set-Content -Path $configPath + $out = dsc config get -f $configPath | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($IsWindows) { + $expectedOS = 'Windows' + } elseif ($IsLinux) { + $expectedOS = 'Linux' + } else { + $expectedOS = 'macOS' + } + $out.results[0].result[0].result.actualState.family | Should -Be $expectedOS + } + It 'Invalid file path: ' -TestCases @( @{ test = 'non-existing configuration'; config = 'include/non-existing.dsc.yaml'; parameters = $null } @{ test = 'non-existing parameters'; config = 'include/osinfo_parameters.dsc.yaml'; parameters = 'include/non-existing.parameters.yaml' }