Skip to content

Commit

Permalink
Enable Include resource to take configuration and parameters as str…
Browse files Browse the repository at this point in the history
…ing content
  • Loading branch information
SteveL-MSFT committed Jan 16, 2025
1 parent 1d8398a commit 0812245
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 57 deletions.
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')]"
}
}
]
}
158 changes: 102 additions & 56 deletions dsc/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,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,
FilePath(String),
#[serde(rename = "configurationContent")]
Content(String),
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum IncludeParametersKind {
#[serde(rename = "parametersFile")]
pub parameters_file: Option<String>,
FilePath(String),
#[serde(rename = "parametersContent")]
Content(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 +67,104 @@ 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::FilePath(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")));
}
}
}
};
// // 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,
// Err(err) => {
// return Err(format!("{} '{include_path:?}': {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}"));
// // serialize the Configuration as JSON
// match serde_json::to_string(&configuration) {
// Ok(json) => json,
// Err(err) => {
// return Err(format!("JSON: {err}"));
// }
// }
},
IncludeKind::Content(text) => {
match parse_input_to_json(&text) {
Ok(json) => json,
Err(err) => {
return Err(format!("{}: {err}", t!("resolve.invalidFile")));
}
}
}
};

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::FilePath(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::Content(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

0 comments on commit 0812245

Please sign in to comment.