Skip to content

Commit

Permalink
Merge pull request #400 from tgauth/add-whatif
Browse files Browse the repository at this point in the history
Add whatif
  • Loading branch information
SteveL-MSFT committed May 16, 2024
2 parents 2fc0525 + 144ac00 commit ab558d4
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 59 deletions.
2 changes: 2 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ pub enum ConfigSubCommand {
path: Option<String>,
#[clap(short = 'f', long, help = "The output format to use")]
format: Option<OutputFormat>,
#[clap(short = 'w', long, help = "Run as a what-if operation instead of executing the configuration or resource")]
what_if: bool,
},
#[clap(name = "test", about = "Test the current configuration")]
Test {
Expand Down
4 changes: 2 additions & 2 deletions dsc/src/resource_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use crate::args::OutputFormat;
use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, add_type_name_to_json, write_output};
use dsc_lib::configure::config_doc::Configuration;
use dsc_lib::configure::config_doc::{Configuration, ExecutionKind};
use dsc_lib::configure::add_resource_export_results_to_configuration;
use dsc_lib::dscresources::invoke_result::{GetResult, ResourceGetResponse};
use dsc_lib::dscerror::DscError;
Expand Down Expand Up @@ -117,7 +117,7 @@ pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: &Op
};
}

match resource.set(input.as_str(), true) {
match resource.set(input.as_str(), true, &ExecutionKind::Actual) {
Ok(result) => {
// convert to json
let json = match serde_json::to_string(&result) {
Expand Down
8 changes: 7 additions & 1 deletion dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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_result::ResourceGetResult};
use dsc_lib::configure::{Configurator, config_doc::ExecutionKind, config_result::ResourceGetResult};
use dsc_lib::dscerror::DscError;
use dsc_lib::dscresources::invoke_result::{
GroupResourceSetResponse, GroupResourceTestResponse, ResolveResult, TestResult
Expand Down Expand Up @@ -248,6 +248,12 @@ 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;
}
};

let parameters: Option<serde_json::Value> = match if new_parameters.is_some() {
&new_parameters
} else {
Expand Down
70 changes: 70 additions & 0 deletions dsc/tests/dsc_whatif.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Describe 'whatif tests' {
AfterEach {
if ($IsWindows) {
Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore
}
}

It 'config set whatif when actual state matches desired state' {
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: Hello
type: Test/Echo
properties:
output: hello
"@
$what_if_result = $config_yaml | dsc config set -w | ConvertFrom-Json
$set_result = $config_yaml | dsc config set | ConvertFrom-Json
$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) {
# TODO: change/create cross-plat resource that implements set without just matching desired state
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: Registry
type: Microsoft.Windows/Registry
properties:
keyPath: 'HKCU\1\2'
"@
$what_if_result = $config_yaml | dsc config set -w | ConvertFrom-Json
$set_result = $config_yaml | dsc config set | ConvertFrom-Json
$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
$what_if_result.results.result.afterState.KeyPath | Should -Be $set_result.results.result.afterState.keyPath
$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 for delete is not supported' {
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: Delete
type: Test/Delete
properties:
_exist: false
"@
$result = $config_yaml | dsc config set -w 2>&1
$result | Should -Match 'ERROR.*?Not supported.*?what-if'
$LASTEXITCODE | Should -Be 2
}

It 'config set whatif for group resource' {
$result = dsc config set -p $PSScriptRoot/../examples/groups.dsc.yaml -w 2>&1
$result | Should -Match 'ERROR.*?Not implemented.*?what-if'
$LASTEXITCODE | Should -Be 2
}
}
11 changes: 11 additions & 0 deletions dsc_lib/src/configure/config_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ pub struct ResourceSetResult {
pub result: SetResult,
}

impl From<ResourceTestResult> for ResourceSetResult {
fn from(test_result: ResourceTestResult) -> Self {
Self {
metadata: None,
name: test_result.name,
resource_type: test_result.resource_type,
result: test_result.result.into(),
}
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct GroupResourceSetResult {
Expand Down
1 change: 0 additions & 1 deletion dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pub struct Context {
pub parameters: HashMap<String, (Value, DataType)>,
pub security_context: SecurityContextKind,
_variables: HashMap<String, Value>,

pub start_datetime: DateTime<Local>,
}

Expand Down
79 changes: 35 additions & 44 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::configure::config_doc::Metadata;
use crate::configure::config_doc::{ExecutionKind, Metadata};
use crate::configure::parameters::Input;
use crate::dscerror::DscError;
use crate::dscresources::dscresource::get_diff;
Expand Down Expand Up @@ -59,7 +59,7 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, adap

for (i, instance) in export_result.actual_state.iter().enumerate() {
let mut r = config_doc::Resource::new();
r.resource_type = resource.type_name.clone();
r.resource_type.clone_from(&resource.type_name);
r.name = format!("{}-{i}", r.resource_type);
let props: Map<String, Value> = serde_json::from_value(instance.clone())?;
r.properties = escape_property_values(&props)?;
Expand Down Expand Up @@ -309,73 +309,64 @@ impl Configurator {
let desired = add_metadata(&dsc_resource.kind, properties)?;
trace!("desired: {desired}");

let start_datetime;
let end_datetime;
let set_result;
if exist || dsc_resource.capabilities.contains(&Capability::SetHandlesExist) {
debug!("Resource handles _exist or _exist is true");
let start_datetime = chrono::Local::now();
let set_result = dsc_resource.set(&desired, skip_test)?;
let end_datetime = chrono::Local::now();
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
let resource_result = config_result::ResourceSetResult {
metadata: Some(
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
..Default::default()
}
)
}
),
name: resource.name.clone(),
resource_type: resource.resource_type.clone(),
result: set_result,
};
result.results.push(resource_result);
start_datetime = chrono::Local::now();
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 {
// TODO: add delete what-if support
return Err(DscError::NotSupported("What-if execution not supported for delete".to_string()));
}
debug!("Resource implements delete and _exist is false");
let before_result = dsc_resource.get(&desired)?;
let start_datetime = chrono::Local::now();
start_datetime = chrono::Local::now();
dsc_resource.delete(&desired)?;
let end_datetime = chrono::Local::now();
let after_result = dsc_resource.get(&desired)?;
// convert get result to set result
let set_result = match before_result {
set_result = match before_result {
GetResult::Resource(before_response) => {
let GetResult::Resource(after_result) = after_result else {
return Err(DscError::NotSupported("Group resources not supported for delete".to_string()))
};
let before_value = serde_json::to_value(&before_response.actual_state)?;
let after_value = serde_json::to_value(&after_result.actual_state)?;
ResourceSetResponse {
SetResult::Resource(ResourceSetResponse {
before_state: before_response.actual_state,
after_state: after_result.actual_state,
changed_properties: Some(get_diff(&before_value, &after_value)),
}
})
},
GetResult::Group(_) => {
return Err(DscError::NotSupported("Group resources not supported for delete".to_string()));
},
};
self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
let resource_result = config_result::ResourceSetResult {
metadata: Some(
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
..Default::default()
}
)
}
),
name: resource.name.clone(),
resource_type: resource.resource_type.clone(),
result: SetResult::Resource(set_result),
};
result.results.push(resource_result);
end_datetime = chrono::Local::now();
} else {
return Err(DscError::NotImplemented(format!("Resource '{}' does not support `delete` and does not handle `_exist` as false", resource.resource_type)));
}

self.context.outputs.insert(format!("{}:{}", resource.resource_type, resource.name), serde_json::to_value(&set_result)?);
let resource_result = config_result::ResourceSetResult {
metadata: Some(
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
duration: Some(end_datetime.signed_duration_since(start_datetime).to_string()),
..Default::default()
}
)
}
),
name: resource.name.clone(),
resource_type: resource.resource_type.clone(),
result: set_result,
};
result.results.push(resource_result);
}

result.metadata = Some(
Expand Down
19 changes: 14 additions & 5 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
use jsonschema::JSONSchema;
use serde_json::Value;
use std::{collections::HashMap, env, io::{Read, Write}, process::{Command, Stdio}};
use crate::{configure::{config_result::ResourceGetResult, parameters, Configurator}, util::parse_input_to_json};
use crate::{dscerror::DscError, dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}};
use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
use crate::{configure::{config_doc::ExecutionKind, {config_result::ResourceGetResult, parameters, Configurator}}, util::parse_input_to_json};
use crate::dscerror::DscError;
use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
use tracing::{error, warn, info, debug, trace};

pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;
Expand Down Expand Up @@ -93,7 +93,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
///
/// Error returned if the resource does not successfully set the desired state
#[allow(clippy::too_many_lines)]
pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool) -> Result<SetResult, DscError> {
pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError> {
// TODO: support import resources

let Some(set) = &resource.set else {
Expand All @@ -104,7 +104,11 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
// if resource doesn't implement a pre-test, we execute test first to see if a set is needed
if !skip_test && set.pre_test != Some(true) {
info!("No pretest, invoking test {}", &resource.resource_type);
let (in_desired_state, actual_state) = match invoke_test(resource, cwd, desired)? {
let test_result = invoke_test(resource, cwd, desired)?;
if execution_type == &ExecutionKind::WhatIf {
return Ok(test_result.into());
}
let (in_desired_state, actual_state) = match test_result {
TestResult::Group(group_response) => {
let mut result_array: Vec<Value> = Vec::new();
for result in group_response.results {
Expand All @@ -126,6 +130,11 @@ 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()));
}

let Some(get) = &resource.get else {
return Err(DscError::NotImplemented("get".to_string()));
};
Expand Down
8 changes: 4 additions & 4 deletions dsc_lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::dscresources::resource_manifest::Kind;
use crate::{configure::config_doc::ExecutionKind, dscresources::resource_manifest::Kind};
use dscerror::DscError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -118,7 +118,7 @@ pub trait Invoke {
/// # Errors
///
/// This function will return an error if the underlying resource fails.
fn set(&self, desired: &str, skip_test: bool) -> Result<SetResult, DscError>;
fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError>;

/// Invoke the test operation on the resource.
///
Expand Down Expand Up @@ -199,7 +199,7 @@ impl Invoke for DscResource {
}
}

fn set(&self, desired: &str, skip_test: bool) -> Result<SetResult, DscError> {
fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result<SetResult, DscError> {
match &self.implemented_as {
ImplementedAs::Custom(_custom) => {
Err(DscError::NotImplemented("set custom resources".to_string()))
Expand All @@ -209,7 +209,7 @@ 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)
command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type)
},
}
}
Expand Down
21 changes: 21 additions & 0 deletions dsc_lib/src/dscresources/invoke_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ pub enum SetResult {
Group(GroupResourceSetResponse),
}

impl From<TestResult> for SetResult {
fn from(value: TestResult) -> Self {
match value {
TestResult::Group(group) => {
let mut results = Vec::<ResourceSetResult>::new();
for result in group.results {
results.push(result.into());
}
SetResult::Group(GroupResourceSetResponse { results })
},
TestResult::Resource(resource) => {
SetResult::Resource(ResourceSetResponse {
before_state: resource.actual_state,
after_state: resource.desired_state,
changed_properties: if resource.diff_properties.is_empty() { None } else { Some(resource.diff_properties) },
})
}
}
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ResourceSetResponse {
Expand Down
3 changes: 2 additions & 1 deletion dsc_lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use configure::config_doc::ExecutionKind;
use dscerror::DscError;
use dscresources::{dscresource::{DscResource, Invoke}, invoke_result::{GetResult, SetResult, TestResult}};

Expand Down Expand Up @@ -75,7 +76,7 @@ impl DscManager {
/// This function will return an error if the underlying resource fails.
///
pub fn resource_set(&self, resource: &DscResource, input: &str, skip_test: bool) -> Result<SetResult, DscError> {
resource.set(input, skip_test)
resource.set(input, skip_test, &ExecutionKind::Actual)
}

/// Invoke the test operation on a resource.
Expand Down

0 comments on commit ab558d4

Please sign in to comment.