Skip to content
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
1 change: 0 additions & 1 deletion dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,6 @@ pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adap
(Capability::Get, "g"),
(Capability::Set, "s"),
(Capability::SetHandlesExist, "x"),
(Capability::WhatIf, "w"),
(Capability::Test, "t"),
(Capability::Delete, "d"),
(Capability::Export, "e"),
Expand Down
2 changes: 1 addition & 1 deletion dsc/tests/dsc_discovery.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ Describe 'tests for resource discovery' {
@{ operation = 'delete' }
@{ operation = 'export' }
@{ operation = 'resolve' }
@{ operation = 'whatIf' }
) {
param($operation)

Expand All @@ -212,6 +211,7 @@ Describe 'tests for resource discovery' {
$out.Count | Should -Be 1
$out.Type | Should -BeExactly 'Test/ExecutableNotFound'
$out.Kind | Should -BeExactly 'resource'
(Get-Content -Path "$testdrive/error.txt" -Raw)
(Get-Content -Path "$testdrive/error.txt" -Raw) | Should -Match "INFO.*?Executable 'doesNotExist' not found"
}
finally {
Expand Down
51 changes: 46 additions & 5 deletions dsc/tests/dsc_whatif.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ Describe 'whatif tests' {
output: hello
"@
$what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf'
$what_if_result.results.result.beforeState.output | Should -Be $set_result.results.result.beforeState.output
$what_if_result.results.result.afterState.output | Should -Be $set_result.results.result.afterState.output
$what_if_result.results.result.changedProperties | Should -Be $set_result.results.result.changedProperties
$what_if_result.hadErrors | Should -BeFalse
$what_if_result.results.Count | Should -Be 1
$LASTEXITCODE | Should -Be 0
}

It 'config set whatif when actual state does not match desired state' -Skip:(!$IsWindows) {
Expand All @@ -36,7 +37,9 @@ Describe 'whatif tests' {
keyPath: 'HKCU\1\2'
"@
$what_if_result = dsc config set -w -i $config_yaml | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$set_result = dsc config set -i $config_yaml | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf'
$what_if_result.results.result.beforeState._exist | Should -Be $set_result.results.result.beforeState._exist
$what_if_result.results.result.beforeState.keyPath | Should -Be $set_result.results.result.beforeState.keyPath
Expand All @@ -46,14 +49,12 @@ Describe 'whatif tests' {
$what_if_result.results.result.changedProperties | Should -Be @('_metadata', '_exist')
$what_if_result.hadErrors | Should -BeFalse
$what_if_result.results.Count | Should -Be 1
$LASTEXITCODE | Should -Be 0

}

It 'config set whatif for group resource' {
$result = dsc config set -f $PSScriptRoot/../examples/groups.dsc.yaml -w 2>&1
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
$LASTEXITCODE | Should -Be 2
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
}

It 'actual execution of WhatIf resource' {
Expand All @@ -66,12 +67,12 @@ Describe 'whatif tests' {
executionType: Actual
"@
$result = $config_yaml | dsc config set -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual'
$result.results.result.afterState.executionType | Should -BeExactly 'Actual'
$result.results.result.changedProperties | Should -Be $null
$result.hadErrors | Should -BeFalse
$result.results.Count | Should -Be 1
$LASTEXITCODE | Should -Be 0
}

It 'what-if execution of WhatIf resource via <alias>' -TestCases @(
Expand All @@ -90,11 +91,51 @@ Describe 'whatif tests' {
executionType: Actual
"@
$result = $config_yaml | dsc config set $alias -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf'
$result.results.result.afterState.executionType | Should -BeExactly 'WhatIf'
$result.results.result.changedProperties | Should -BeExactly 'executionType'
$result.hadErrors | Should -BeFalse
$result.results.Count | Should -Be 1
}

It 'Test/WhatIfNative resource with set operation and WhatIfArgKind works' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: WhatIfArgKind
type: Test/WhatIfArgKind
properties:
executionType: Actual
"@
$what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf'
$what_if_result.results[0].result.afterState.executionType | Should -BeExactly 'WhatIf'
$what_if_result.hadErrors | Should -BeFalse
$set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$set_result.hadErrors | Should -BeFalse
$set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual'
$set_result.results[0].result.afterState.executionType | Should -BeExactly 'Actual'
}

It 'Echo resource with synthetic what-if works' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: SyntheticWhatIf
type: Microsoft.DSC.Debug/Echo
properties:
output: test
"@
$what_if_result = $config_yaml | dsc config set -w -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$what_if_result.hadErrors | Should -BeFalse
$what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf'
$set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$set_result.hadErrors | Should -BeFalse
$set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual'
}
}
1 change: 1 addition & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, an
inDesiredStateNotBool = "'_inDesiredState' is not a boolean"
exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation"
runProcessError = "Failed to run process '%{executable}': %{error}"
whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. See https://github.com/PowerShell/DSC/issues/1361 for migration information."

[dscresources.dscresource]
invokeGet = "Invoking get for '%{resource}'"
Expand Down
4 changes: 0 additions & 4 deletions lib/dsc-lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,10 +770,6 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result<Ds
capabilities.push(Capability::SetHandlesExist);
}
}
if let Some(what_if) = &manifest.what_if {
verify_executable(&manifest.resource_type, "what_if", &what_if.executable, path.parent().unwrap());
capabilities.push(Capability::WhatIf);
}
if let Some(test) = &manifest.test {
verify_executable(&manifest.resource_type, "test", &test.executable, path.parent().unwrap());
capabilities.push(Capability::Test);
Expand Down
104 changes: 83 additions & 21 deletions lib/dsc-lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde_json::{Map, Value};
use std::{collections::HashMap, env, path::Path, process::Stdio};
use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, types::FullyQualifiedTypeName, util::canonicalize_which};
use crate::dscerror::DscError;
use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{GetArgKind, SetDeleteArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
use tracing::{error, warn, info, debug, trace};
use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command};

Expand All @@ -35,7 +35,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_
Some(r) => r,
None => resource.resource_type.clone(),
};
let args = process_args(get.args.as_ref(), filter, &resource_type);
let args = process_get_args(get.args.as_ref(), filter, &resource_type);
if !filter.is_empty() {
verify_json(resource, cwd, filter)?;
command_input = get_command_input(get.input.as_ref(), filter)?;
Expand Down Expand Up @@ -82,22 +82,35 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t
debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.resource_type));
let operation_type: String;
let mut is_synthetic_what_if = false;

let set_method = match execution_type {
ExecutionKind::Actual => {
operation_type = "set".to_string();
&resource.set
},
ExecutionKind::WhatIf => {
operation_type = "whatif".to_string();
if resource.what_if.is_none() {
is_synthetic_what_if = true;
// Check if set supports native what-if
let has_native_whatif = resource.set.as_ref()
.map_or(false, |set| {
let (_, supports_whatif) = process_set_delete_args(set.args.as_ref(), "", &resource.resource_type, execution_type);
supports_whatif
});

if has_native_whatif {
&resource.set
} else {
&resource.what_if
if resource.what_if.is_some() {
warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource.resource_type));
&resource.what_if
} else {
is_synthetic_what_if = true;
&resource.set
}
}
}
};
let Some(set) = set_method else {
let Some(set) = set_method.as_ref() else {
return Err(DscError::NotImplemented("set".to_string()));
};
verify_json(resource, cwd, desired)?;
Expand Down Expand Up @@ -144,7 +157,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t
Some(r) => r,
None => resource.resource_type.clone(),
};
let args = process_args(get.args.as_ref(), desired, &resource_type);
let args = process_get_args(get.args.as_ref(), desired, &resource_type);
let command_input = get_command_input(get.input.as_ref(), desired)?;

info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.resource_type, executable = &get.executable));
Expand Down Expand Up @@ -176,7 +189,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t

let mut env: Option<HashMap<String, String>> = None;
let mut input_desired: Option<&str> = None;
let args = process_args(set.args.as_ref(), desired, &resource_type);
let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &resource_type, execution_type);
match &set.input {
Some(InputKind::Env) => {
env = Some(json_to_hashmap(desired)?);
Expand Down Expand Up @@ -284,7 +297,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, targ
Some(r) => r,
None => resource.resource_type.clone(),
};
let args = process_args(test.args.as_ref(), expected, &resource_type);
let args = process_get_args(test.args.as_ref(), expected, &resource_type);
let command_input = get_command_input(test.input.as_ref(), expected)?;

info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.resource_type, executable = &test.executable));
Expand Down Expand Up @@ -411,6 +424,7 @@ fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str
/// * `resource` - The resource manifest for the command resource.
/// * `cwd` - The current working directory.
/// * `filter` - The filter to apply to the resource in JSON.
/// * `execution_type` - Whether this is an actual delete or what-if.
///
/// # Errors
///
Expand All @@ -426,7 +440,8 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &Path, filter: &str, targ
Some(r) => r,
None => &resource.resource_type,
};
let args = process_args(delete.args.as_ref(), filter, resource_type);
let (args, _) = process_set_delete_args(delete.args.as_ref(), filter, resource_type, &ExecutionKind::Actual);

let command_input = get_command_input(delete.input.as_ref(), filter)?;

info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable));
Expand Down Expand Up @@ -461,7 +476,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &Path, config: &str, ta
Some(r) => r,
None => &resource.resource_type,
};
let args = process_args(validate.args.as_ref(), config, resource_type);
let args = process_get_args(validate.args.as_ref(), config, resource_type);
let command_input = get_command_input(validate.input.as_ref(), config)?;

info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable));
Expand Down Expand Up @@ -549,9 +564,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str
command_input = get_command_input(export.input.as_ref(), input)?;
}

args = process_args(export.args.as_ref(), input, &resource_type);
args = process_get_args(export.args.as_ref(), input, &resource_type);
} else {
args = process_args(export.args.as_ref(), "", &resource_type);
args = process_get_args(export.args.as_ref(), "", &resource_type);
}

let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?;
Expand Down Expand Up @@ -596,7 +611,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &Path, input: &str) -> R
return Err(DscError::Operation(t!("dscresources.commandResource.resolveNotSupported", resource = &resource.resource_type).to_string()));
};

let args = process_args(resolve.args.as_ref(), input, &resource.resource_type);
let args = process_get_args(resolve.args.as_ref(), input, &resource.resource_type);
let command_input = get_command_input(resolve.input.as_ref(), input)?;

info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.resource_type, executable = &resolve.executable));
Expand Down Expand Up @@ -800,17 +815,17 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
}
}

/// Process the arguments for a command resource.
/// Process the arguments for a command resource's get operation.
///
/// # Arguments
///
/// * `args` - The arguments to process
/// * `args` - The Get arguments to process
/// * `value` - The value to use for JSON input arguments
///
/// # Returns
///
/// A vector of strings representing the processed arguments
pub fn process_args(args: Option<&Vec<ArgKind>>, input: &str, resource_type: &str) -> Option<Vec<String>> {
pub fn process_get_args(args: Option<&Vec<GetArgKind>>, input: &str, resource_type: &str) -> Option<Vec<String>> {
let Some(arg_values) = args else {
debug!("{}", t!("dscresources.commandResource.noArgs"));
return None;
Expand All @@ -819,27 +834,74 @@ pub fn process_args(args: Option<&Vec<ArgKind>>, input: &str, resource_type: &st
let mut processed_args = Vec::<String>::new();
for arg in arg_values {
match arg {
ArgKind::String(s) => {
GetArgKind::String(s) => {
processed_args.push(s.clone());
},
ArgKind::Json { json_input_arg, mandatory } => {
GetArgKind::Json { json_input_arg, mandatory } => {
if input.is_empty() && *mandatory != Some(true) {
continue;
}

processed_args.push(json_input_arg.clone());
processed_args.push(input.to_string());
},
ArgKind::ResourceType { resource_type_arg } => {
GetArgKind::ResourceType { resource_type_arg } => {
processed_args.push(resource_type_arg.clone());
processed_args.push(resource_type.to_string());
}
},
}
}

Some(processed_args)
}

/// Process the arguments for a command resource's set or delete operation.
///
/// # Arguments
///
/// * `args` - The Set/Delete arguments to process
/// * `value` - The value to use for JSON input arguments
///
/// # Returns
///
/// A vector of strings representing the processed arguments
pub fn process_set_delete_args(args: Option<&Vec<SetDeleteArgKind>>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> (Option<Vec<String>>, bool) {
let Some(arg_values) = args else {
debug!("{}", t!("dscresources.commandResource.noArgs"));
return (None, false);
};

let mut processed_args = Vec::<String>::new();
let mut supports_whatif = false;
for arg in arg_values {
match arg {
SetDeleteArgKind::String(s) => {
processed_args.push(s.clone());
},
SetDeleteArgKind::Json { json_input_arg, mandatory } => {
if input.is_empty() && *mandatory != Some(true) {
continue;
}

processed_args.push(json_input_arg.clone());
processed_args.push(input.to_string());
},
SetDeleteArgKind::ResourceType { resource_type_arg } => {
processed_args.push(resource_type_arg.clone());
processed_args.push(resource_type.to_string());
},
SetDeleteArgKind::WhatIf { what_if_arg } => {
supports_whatif = true;
if execution_type == &ExecutionKind::WhatIf {
processed_args.push(what_if_arg.clone());
}
}
}
}

(Some(processed_args), supports_whatif)
}

struct CommandInput {
env: Option<HashMap<String, String>>,
stdin: Option<String>,
Expand Down
Loading