diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 1ba90ac1c..44d06a48d 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -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"), diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 3e750fa03..a0e6b6256 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -189,7 +189,6 @@ Describe 'tests for resource discovery' { @{ operation = 'delete' } @{ operation = 'export' } @{ operation = 'resolve' } - @{ operation = 'whatIf' } ) { param($operation) @@ -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 { diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 7841a01b1..1e69302d7 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -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) { @@ -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 @@ -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' { @@ -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 ' -TestCases @( @@ -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' } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index fc8523cf6..04e250a55 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -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}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 2be6f6dd3..be0601fd1 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -770,10 +770,6 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result 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)?; @@ -82,6 +82,7 @@ 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(); @@ -89,15 +90,27 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t }, 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)?; @@ -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)); @@ -176,7 +189,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let mut env: Option> = 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)?); @@ -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)); @@ -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 /// @@ -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)); @@ -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)); @@ -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())?; @@ -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)); @@ -800,17 +815,17 @@ pub fn invoke_command(executable: &str, args: Option>, 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>, input: &str, resource_type: &str) -> Option> { +pub fn process_get_args(args: Option<&Vec>, input: &str, resource_type: &str) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -819,10 +834,10 @@ pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &st let mut processed_args = Vec::::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; } @@ -830,16 +845,63 @@ pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &st 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>, input: &str, resource_type: &str, execution_type: &ExecutionKind) -> (Option>, bool) { + let Some(arg_values) = args else { + debug!("{}", t!("dscresources.commandResource.noArgs")); + return (None, false); + }; + + let mut processed_args = Vec::::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>, stdin: Option, diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index d0dd3b48e..be63aa589 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -96,8 +96,8 @@ pub struct ResourceManifest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(untagged)] -#[dsc_repo_schema(base_name = "commandArgs", folder_path = "definitions")] -pub enum ArgKind { +#[dsc_repo_schema(base_name = "commandArgs.get", folder_path = "definitions")] +pub enum GetArgKind { /// The argument is a string. String(String), /// The argument accepts the JSON input object. @@ -112,6 +112,33 @@ pub enum ArgKind { /// The argument that accepts the resource type name. #[serde(rename = "resourceTypeArg")] resource_type_arg: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(untagged)] +#[dsc_repo_schema(base_name = "commandArgs.setDelete", folder_path = "definitions")] +pub enum SetDeleteArgKind { + /// The argument is a string. + String(String), + /// The argument accepts the JSON input object. + Json { + /// The argument that accepts the JSON input object. + #[serde(rename = "jsonInputArg")] + json_input_arg: String, + /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. + mandatory: Option, + }, + ResourceType { + /// The argument that accepts the resource type name. + #[serde(rename = "resourceTypeArg")] + resource_type_arg: String, + }, + /// The argument is passed when the resource is invoked in what-if mode. + WhatIf { + /// The argument to pass when in what-if mode. + #[serde(rename = "whatIfArg")] + what_if_arg: String, } } @@ -164,7 +191,7 @@ pub struct GetMethod { /// The command to run to get the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Get. - pub args: Option>, + pub args: Option>, /// How to pass optional input for a Get. #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, @@ -176,7 +203,7 @@ pub struct SetMethod { /// The command to run to set the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Set. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Set. pub input: Option, /// Whether to run the Test method before the Set method. True means the resource will perform its own test before running the Set method. @@ -196,7 +223,7 @@ pub struct TestMethod { /// The command to run to test the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Test. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Test. pub input: Option, /// The type of return value expected from the Test method. @@ -210,7 +237,7 @@ pub struct DeleteMethod { /// The command to run to delete the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Delete. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Delete. pub input: Option, } @@ -221,7 +248,7 @@ pub struct ValidateMethod { // TODO: enable validation via schema or command /// The command to run to validate the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Validate. - pub args: Option>, + pub args: Option>, /// How to pass required input for a Validate. pub input: Option, } @@ -232,7 +259,7 @@ pub struct ExportMethod { /// The command to run to enumerate instances of the resource. pub executable: String, /// The arguments to pass to the command to perform a Export. - pub args: Option>, + pub args: Option>, /// How to pass input for a Export. pub input: Option, } @@ -243,7 +270,7 @@ pub struct ResolveMethod { /// The command to run to enumerate instances of the resource. pub executable: String, /// The arguments to pass to the command to perform a Export. - pub args: Option>, + pub args: Option>, /// How to pass input for a Export. pub input: Option, } diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 5aff5e121..d24714869 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -8,10 +8,10 @@ use crate::{ dscerror::DscError, dscresources::{ command_resource::{ - invoke_command, process_args + invoke_command, process_get_args }, dscresource::DscResource, - resource_manifest::ArgKind, + resource_manifest::GetArgKind, }, extensions::{ dscextension::{ @@ -34,7 +34,7 @@ pub struct DiscoverMethod { /// The command to run to get the state of the resource. pub executable: String, /// The arguments to pass to the command to perform a Get. - pub args: Option>, + pub args: Option>, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -68,7 +68,7 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let args = process_args(discover.args.as_ref(), "", self.type_name.as_ref()); + let args = process_get_args(discover.args.as_ref(), "", self.type_name.as_ref()); let (_exit_code, stdout, _stderr) = invoke_command( &discover.executable, args, diff --git a/lib/dsc-lib/tests/integration/schemas/schema_for.rs b/lib/dsc-lib/tests/integration/schemas/schema_for.rs index ef35cc1cb..d503c522b 100644 --- a/lib/dsc-lib/tests/integration/schemas/schema_for.rs +++ b/lib/dsc-lib/tests/integration/schemas/schema_for.rs @@ -103,7 +103,8 @@ macro_rules! test_schema_for { #[allow(unused_must_use)] #[cfg(test)] mod resource_manifest { test_schema_for!(dsc_lib::dscresources::resource_manifest::Kind); - test_schema_for!(dsc_lib::dscresources::resource_manifest::ArgKind); + test_schema_for!(dsc_lib::dscresources::resource_manifest::GetArgKind); + test_schema_for!(dsc_lib::dscresources::resource_manifest::SetDeleteArgKind); test_schema_for!(dsc_lib::dscresources::resource_manifest::InputKind); test_schema_for!(dsc_lib::dscresources::resource_manifest::SchemaKind); test_schema_for!(dsc_lib::dscresources::resource_manifest::SchemaCommand); diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index b5afea1fc..806e61aea 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -791,6 +791,38 @@ ] } } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/WhatIfArgKind", + "version": "0.1.0", + "description": "Test resource for validating native WhatIf functionality with whatIfArg", + "get": { + "executable": "dsctest", + "args": [ + "whatif" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "whatif", + { + "whatIfArg": "-w" + } + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "what-if" + ] + } + } } ] }