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 whatif #400

Merged
merged 15 commits into from
May 16, 2024
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
Loading
Loading