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

Add resource support for what if #441

Merged
merged 9 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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: 3 additions & 2 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, stdin:

if let ConfigSubCommand::Set { what_if , .. } = subcommand {
if *what_if {
configurator.context.execution_type = ExecutionKind::WhatIf;
configurator.context.execution_type = ExecutionKind::WhatIfDSC;
}
};

Expand Down Expand Up @@ -511,11 +511,12 @@ fn list_resources(dsc: &mut DscManager, resource_name: &Option<String>, adapter_
write_table = true;
}
for resource in dsc.list_available_resources(&resource_name.clone().unwrap_or("*".to_string()), &adapter_name.clone().unwrap_or_default()) {
let mut capabilities = "-------".to_string();
let mut capabilities = "--------".to_string();
let capability_types = [
(Capability::Get, "g"),
(Capability::Set, "s"),
(Capability::SetHandlesExist, "x"),
(Capability::SetHandlesWhatIf, "w"),
(Capability::Test, "t"),
(Capability::Delete, "d"),
(Capability::Export, "e"),
Expand Down
36 changes: 36 additions & 0 deletions dsc/tests/dsc_whatif.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,40 @@ Describe 'whatif tests' {
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
$LASTEXITCODE | Should -Be 2
}

It 'actual execution of WhatIf resource' {
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: WhatIf
type: Test/WhatIf
properties:
executionType: Actual
"@
$result = $config_yaml | dsc config set | ConvertFrom-Json
$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' {
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: WhatIf
type: Test/WhatIf
properties:
executionType: Actual
"@
$result = $config_yaml | dsc config set -w | ConvertFrom-Json
$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
$LASTEXITCODE | Should -Be 0
}
}
6 changes: 5 additions & 1 deletion dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ pub enum Operation {
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum ExecutionKind {
Actual,
WhatIf,
// differentiate internally whether what-if should be processed by the resource or dsc engine
#[serde(rename = "WhatIf")]
WhatIfDSC,
#[serde(rename = "WhatIf")]
WhatIfResource,
}

#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
2 changes: 1 addition & 1 deletion dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ impl Configurator {
set_result = dsc_resource.set(&desired, skip_test, &self.context.execution_type)?;
end_datetime = chrono::Local::now();
} else if dsc_resource.capabilities.contains(&Capability::Delete) {
if self.context.execution_type == ExecutionKind::WhatIf {
if self.context.execution_type == ExecutionKind::WhatIfDSC {
// TODO: add delete what-if support
return Err(DscError::NotSupported("What-if execution not supported for delete".to_string()));
}
Expand Down
10 changes: 9 additions & 1 deletion dsc_lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use crate::discovery::discovery_trait::ResourceDiscovery;
use crate::discovery::convert_wildcard_to_regex;
use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs};
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest};
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, ArgKind, Kind, ResourceManifest};
use crate::dscresources::command_resource::invoke_command;
use crate::dscerror::DscError;
use indicatif::ProgressStyle;
Expand Down Expand Up @@ -449,6 +449,14 @@ fn load_manifest(path: &Path) -> Result<DscResource, DscError> {
if set.handles_exist == Some(true) {
capabilities.push(Capability::SetHandlesExist);
}
if let Some(arg_values) = &set.args {
for arg in arg_values {
if let &ArgKind::WhatIf { .. } = arg {
capabilities.push(Capability::SetHandlesWhatIf);
break;
}
}
}
}
if manifest.test.is_some() {
capabilities.push(Capability::Test);
Expand Down
34 changes: 19 additions & 15 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
let Some(get) = &resource.get else {
return Err(DscError::NotImplemented("get".to_string()));
};
let args = process_args(&get.args, filter);
let args = process_args(&get.args, filter, &ExecutionKind::Actual);
if !filter.is_empty() {
verify_json(resource, cwd, filter)?;
command_input = get_command_input(&get.input, filter)?;
Expand Down Expand Up @@ -105,7 +105,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
if !skip_test && set.pre_test != Some(true) {
info!("No pretest, invoking test {}", &resource.resource_type);
let test_result = invoke_test(resource, cwd, desired)?;
if execution_type == &ExecutionKind::WhatIf {
if execution_type == &ExecutionKind::WhatIfDSC {
return Ok(test_result.into());
}
let (in_desired_state, actual_state) = match test_result {
Expand All @@ -121,7 +121,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
}
};

if in_desired_state {
if in_desired_state && execution_type == &ExecutionKind::Actual {
return Ok(SetResult::Resource(ResourceSetResponse{
before_state: serde_json::from_str(desired)?,
after_state: actual_state,
Expand All @@ -130,15 +130,14 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
}
}

if ExecutionKind::WhatIf == *execution_type {
// TODO: continue execution when resources can implement what-if; only return an error here temporarily
return Err(DscError::NotImplemented("what-if not yet supported for resources that implement pre-test".to_string()));
if ExecutionKind::WhatIfDSC == *execution_type {
return Err(DscError::NotImplemented("cannot process what-if execution type, as resource implements pre-test and does not support what-if".to_string()));
}

let Some(get) = &resource.get else {
return Err(DscError::NotImplemented("get".to_string()));
};
let args = process_args(&get.args, desired);
let args = process_args(&get.args, desired, &ExecutionKind::Actual);
let command_input = get_command_input(&get.input, desired)?;

info!("Getting current state for set by invoking get {} using {}", &resource.resource_type, &get.executable);
Expand All @@ -158,7 +157,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te

let mut env: Option<HashMap<String, String>> = None;
let mut input_desired: Option<&str> = None;
let args = process_args(&set.args, desired);
let args = process_args(&set.args, desired, execution_type);
match &set.input {
Some(InputKind::Env) => {
env = Some(json_to_hashmap(desired)?);
Expand Down Expand Up @@ -261,7 +260,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re

verify_json(resource, cwd, expected)?;

let args = process_args(&test.args, expected);
let args = process_args(&test.args, expected, &ExecutionKind::Actual);
let command_input = get_command_input(&test.input, expected)?;

info!("Invoking test '{}' using '{}'", &resource.resource_type, &test.executable);
Expand Down Expand Up @@ -375,7 +374,7 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re

verify_json(resource, cwd, filter)?;

let args = process_args(&delete.args, filter);
let args = process_args(&delete.args, filter, &ExecutionKind::Actual);
let command_input = get_command_input(&delete.input, filter)?;

info!("Invoking delete '{}' using '{}'", &resource.resource_type, &delete.executable);
Expand Down Expand Up @@ -406,7 +405,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) ->
return Err(DscError::NotImplemented("validate".to_string()));
};

let args = process_args(&validate.args, config);
let args = process_args(&validate.args, config, &ExecutionKind::Actual);
let command_input = get_command_input(&validate.input, config)?;

info!("Invoking validate '{}' using '{}'", &resource.resource_type, &validate.executable);
Expand Down Expand Up @@ -481,9 +480,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>
command_input = get_command_input(&export.input, input)?;
}

args = process_args(&export.args, input);
args = process_args(&export.args, input, &ExecutionKind::Actual);
} else {
args = process_args(&export.args, "");
args = process_args(&export.args, "", &ExecutionKind::Actual);
}

let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
Expand Down Expand Up @@ -528,7 +527,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Re
return Err(DscError::Operation(format!("Resolve is not supported by resource {}", &resource.resource_type)));
};

let args = process_args(&resolve.args, input);
let args = process_args(&resolve.args, input, &ExecutionKind::Actual);
let command_input = get_command_input(&resolve.input, input)?;

info!("Invoking resolve '{}' using '{}'", &resource.resource_type, &resolve.executable);
Expand Down Expand Up @@ -620,7 +619,7 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
Ok((exit_code, stdout, cleaned_stderr))
}

fn process_args(args: &Option<Vec<ArgKind>>, value: &str) -> Option<Vec<String>> {
fn process_args(args: &Option<Vec<ArgKind>>, value: &str, execution_type: &ExecutionKind) -> Option<Vec<String>> {
let Some(arg_values) = args else {
debug!("No args to process");
return None;
Expand All @@ -640,6 +639,11 @@ fn process_args(args: &Option<Vec<ArgKind>>, value: &str) -> Option<Vec<String>>
processed_args.push(json_input_arg.clone());
processed_args.push(value.to_string());
},
ArgKind::WhatIf { what_if_input_arg } => {
if execution_type == &ExecutionKind::WhatIfResource {
processed_args.push(what_if_input_arg.clone());
}
}
}
}

Expand Down
10 changes: 9 additions & 1 deletion dsc_lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub enum Capability {
Set,
/// The resource supports the `_exist` property directly.
SetHandlesExist,
/// The resource supports the `what-if` execution type directly.
SetHandlesWhatIf,
/// The resource supports validating configuration.
Test,
/// The resource supports deleting configuration.
Expand Down Expand Up @@ -209,7 +211,13 @@ impl Invoke for DscResource {
return Err(DscError::MissingManifest(self.type_name.clone()));
};
let resource_manifest = import_manifest(manifest.clone())?;
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type)
let execution = if self.capabilities.contains(&Capability::SetHandlesWhatIf) && execution_type == &ExecutionKind::WhatIfDSC {
ExecutionKind::WhatIfResource
}
else {
execution_type.clone()
};
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, &execution)
},
}
}
Expand Down
5 changes: 5 additions & 0 deletions dsc_lib/src/dscresources/resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ pub enum ArgKind {
/// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false.
mandatory: Option<bool>,
},
WhatIf {
/// The argument that serves as the what-if switch.
#[serde(rename = "whatIfSwitchArg")]
what_if_input_arg: String,
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
31 changes: 31 additions & 0 deletions tools/dsctest/dscwhatif.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
"type": "Test/WhatIf",
"version": "0.1.0",
"get": {
"executable": "dsctest",
"args": [
"what-if"
]
},
"set": {
"executable": "dsctest",
"args": [
"what-if",
{
"whatIfSwitchArg": "--what-if"
}
],
"return": "state"
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"schema",
"-s",
"what-if"
]
}
}
}
7 changes: 7 additions & 0 deletions tools/dsctest/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub enum Schemas {
Exist,
Sleep,
Trace,
WhatIf,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -54,4 +55,10 @@ pub enum SubCommand {

#[clap(name = "trace", about = "The trace level")]
Trace,

#[clap(name = "what-if", about = "Check if it is a what-if operation")]
WhatIf {
#[clap(name = "what-if", short, long, help = "Run as a what-if executionType instead of actual executionType")]
what_if: bool,
}
}
13 changes: 13 additions & 0 deletions tools/dsctest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod echo;
mod exist;
mod sleep;
mod trace;
mod whatif;

use args::{Args, Schemas, SubCommand};
use clap::Parser;
Expand All @@ -16,6 +17,7 @@ use crate::echo::Echo;
use crate::exist::{Exist, State};
use crate::sleep::Sleep;
use crate::trace::Trace;
use crate::whatif::WhatIf;
use std::{thread, time::Duration};

fn main() {
Expand Down Expand Up @@ -75,6 +77,9 @@ fn main() {
Schemas::Trace => {
schema_for!(Trace)
},
Schemas::WhatIf => {
schema_for!(WhatIf)
},
};
serde_json::to_string(&schema).unwrap()
},
Expand All @@ -100,6 +105,14 @@ fn main() {
};
serde_json::to_string(&trace).unwrap()
},
SubCommand::WhatIf { what_if } => {
let result: WhatIf = if what_if {
WhatIf { execution_type: "WhatIf".to_string() }
} else {
WhatIf { execution_type: "Actual".to_string() }
};
serde_json::to_string(&result).unwrap()
},
};

println!("{json}");
Expand Down
12 changes: 12 additions & 0 deletions tools/dsctest/src/whatif.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WhatIf {
#[serde(rename = "executionType")]
pub execution_type: String,
}
Loading