Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Include resource to take configuration and parameters as string content #626

Merged
merged 7 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dsc/examples/osinfo.parameters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"parameters": {
"osFamily": "macOS"
}
}
37 changes: 37 additions & 0 deletions dsc/examples/osinfo_parameters.dsc.json
Original file line number Diff line number Diff line change
@@ -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')]"
}
}
]
}
4 changes: 2 additions & 2 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
Expand Down
140 changes: 82 additions & 58 deletions dsc/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String>,
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<IncludeParametersKind>,
}

/// Read the file specified in the Include input and return the content as a JSON string.
Expand Down Expand Up @@ -51,74 +66,83 @@ pub fn get_contents(input: &str) -> Result<(Option<String>, 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<u8> = 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<u8> = 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(&parameters_file))?;
info!("{} '{parameters_file:?}'", t!("resolve.resolvingParameters"));
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!("{} '{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(&parameters_file) {
Ok(parameters) => {
let parameters_json = match parse_input_to_json(&parameters) {
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))
Expand Down
113 changes: 112 additions & 1 deletion dsc/tests/dsc_include.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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: <test>' -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' }
Expand Down
Loading