From ab293a71c04e605f6fcb30ac297caf1c4fb681b4 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 17 Jul 2025 14:15:47 -0700 Subject: [PATCH 1/4] Add `condition` support to resources within config --- dsc/tests/dsc_resource_condition.tests.ps1 | 42 ++++++++++++++++++++++ dsc_lib/locales/en-us.toml | 1 + dsc_lib/src/configure/config_doc.rs | 3 ++ dsc_lib/src/configure/mod.rs | 32 +++++++++++++++++ dscecho/echo.dsc.resource.json | 9 +++++ 5 files changed, 87 insertions(+) create mode 100644 dsc/tests/dsc_resource_condition.tests.ps1 diff --git a/dsc/tests/dsc_resource_condition.tests.ps1 b/dsc/tests/dsc_resource_condition.tests.ps1 new file mode 100644 index 000000000..c4e0c6890 --- /dev/null +++ b/dsc/tests/dsc_resource_condition.tests.ps1 @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +BeforeDiscovery { + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Microsoft.DSC.Debug/Echo + condition: "[equals('skip', 'yes')]" + properties: + output: "This should not be executed" + - name: test2 + type: Microsoft.DSC.Debug/Echo + condition: "[equals('no', 'no')]" + properties: + output: "This should be executed" +'@ + +} + +Describe 'Resource condition tests' { + It 'resource should be skipped for ' -TestCases @( + @{ operation = 'get'; property = 'actualState' }, + @{ operation = 'set'; property = 'afterState' }, + @{ operation = 'test'; property = 'actualState' } + ) { + param($operation, $property) + $out = dsc config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results.count | Should -Be 1 + $out.results[0].result.$property.Output | Should -BeExactly "This should be executed" + } + + It 'resource should be skipped for export' { + $out = dsc config export -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.resources.count | Should -Be 1 + $out.resources[0].type | Should -BeExactly 'Microsoft.DSC.Debug/Echo' + $out.resources[0].properties.output | Should -BeExactly "This should be executed" + } +} diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 449fb6a4c..350d6da8d 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -3,6 +3,7 @@ _version = 1 [configure.config_doc] configurationDocumentSchemaTitle = "Configuration document schema URI" configurationDocumentSchemaDescription = "Defines the JSON Schema the configuration document adheres to." +skippingResource = "Skipping resource '%{name}' due to condition '%{condition}' with result '%{result}'" [configure.constraints] minLengthIsNull = "Parameter '%{name}' has minimum length constraint but is null" diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 902662528..871abdb3a 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -160,6 +160,8 @@ pub struct Resource { pub properties: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, } impl Default for Configuration { @@ -217,6 +219,7 @@ impl Resource { kind: None, properties: None, metadata: None, + condition: None, } } } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 30c0dc237..1983c7328 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -311,6 +311,14 @@ impl Configurator { for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Get '{}'", resource.name).as_str()); + if let Some(condition) = &resource.condition { + let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; + if condition_result != Value::Bool(true) { + info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); + progress.write_increment(1); + continue; + } + } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; @@ -387,6 +395,14 @@ impl Configurator { for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Set '{}'", resource.name).as_str()); + if let Some(condition) = &resource.condition { + let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; + if condition_result != Value::Bool(true) { + info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); + progress.write_increment(1); + continue; + } + } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; @@ -535,6 +551,14 @@ impl Configurator { for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Test '{}'", resource.name).as_str()); + if let Some(condition) = &resource.condition { + let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; + if condition_result != Value::Bool(true) { + info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); + progress.write_increment(1); + continue; + } + } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); }; @@ -608,6 +632,14 @@ impl Configurator { for resource in &resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Export '{}'", resource.name).as_str()); + if let Some(condition) = &resource.condition { + let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; + if condition_result != Value::Bool(true) { + info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); + progress.write_increment(1); + continue; + } + } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; diff --git a/dscecho/echo.dsc.resource.json b/dscecho/echo.dsc.resource.json index 829a3ce1f..37da7a8f1 100644 --- a/dscecho/echo.dsc.resource.json +++ b/dscecho/echo.dsc.resource.json @@ -30,6 +30,15 @@ } ] }, + "export": { + "executable": "dscecho", + "args": [ + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, "schema": { "command": { "executable": "dscecho" From 721ea469589b24152c6e15507e6a1688f836285e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 17 Jul 2025 14:22:05 -0700 Subject: [PATCH 2/4] refactor common code --- dsc_lib/src/configure/mod.rs | 51 ++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 1983c7328..781517e52 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -311,13 +311,9 @@ impl Configurator { for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Get '{}'", resource.name).as_str()); - if let Some(condition) = &resource.condition { - let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; - if condition_result != Value::Bool(true) { - info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); - progress.write_increment(1); - continue; - } + if self.skip_resource(&resource)? { + progress.write_increment(1); + continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -395,13 +391,9 @@ impl Configurator { for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Set '{}'", resource.name).as_str()); - if let Some(condition) = &resource.condition { - let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; - if condition_result != Value::Bool(true) { - info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); - progress.write_increment(1); - continue; - } + if self.skip_resource(&resource)? { + progress.write_increment(1); + continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -551,13 +543,9 @@ impl Configurator { for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Test '{}'", resource.name).as_str()); - if let Some(condition) = &resource.condition { - let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; - if condition_result != Value::Bool(true) { - info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); - progress.write_increment(1); - continue; - } + if self.skip_resource(&resource)? { + progress.write_increment(1); + continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type)); @@ -632,13 +620,9 @@ impl Configurator { for resource in &resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Export '{}'", resource.name).as_str()); - if let Some(condition) = &resource.condition { - let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; - if condition_result != Value::Bool(true) { - info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); - progress.write_increment(1); - continue; - } + if self.skip_resource(resource)? { + progress.write_increment(1); + continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); @@ -674,6 +658,17 @@ impl Configurator { Ok(result) } + fn skip_resource(&mut self, resource: &Resource) -> Result { + if let Some(condition) = &resource.condition { + let condition_result = self.statement_parser.parse_and_execute(condition, &self.context)?; + if condition_result != Value::Bool(true) { + info!("{}", t!("configure.config_doc.skippingResource", name = resource.name, condition = condition, result = condition_result)); + return Ok(true); + } + } + Ok(false) + } + /// Set the mounted path for the configuration. /// /// # Arguments From c157972db8121cb0e815ad7b3c2616b16d3762fb Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 17 Jul 2025 15:21:57 -0700 Subject: [PATCH 3/4] add tracing to tests --- dsc/tests/dsc_resource_condition.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc/tests/dsc_resource_condition.tests.ps1 b/dsc/tests/dsc_resource_condition.tests.ps1 index c4e0c6890..3adafde3f 100644 --- a/dsc/tests/dsc_resource_condition.tests.ps1 +++ b/dsc/tests/dsc_resource_condition.tests.ps1 @@ -27,14 +27,14 @@ Describe 'Resource condition tests' { ) { param($operation, $property) $out = dsc config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String) $out.results.count | Should -Be 1 $out.results[0].result.$property.Output | Should -BeExactly "This should be executed" } It 'resource should be skipped for export' { $out = dsc config export -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String) $out.resources.count | Should -Be 1 $out.resources[0].type | Should -BeExactly 'Microsoft.DSC.Debug/Echo' $out.resources[0].properties.output | Should -BeExactly "This should be executed" From cf73de3a56ef45672d1ac03846bb9432fd233d06 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 17 Jul 2025 15:34:45 -0700 Subject: [PATCH 4/4] move to BeforeAll --- dsc/tests/dsc_resource_condition.tests.ps1 | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/dsc/tests/dsc_resource_condition.tests.ps1 b/dsc/tests/dsc_resource_condition.tests.ps1 index 3adafde3f..beddffcf5 100644 --- a/dsc/tests/dsc_resource_condition.tests.ps1 +++ b/dsc/tests/dsc_resource_condition.tests.ps1 @@ -1,25 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -BeforeDiscovery { - $configYaml = @' - $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: test - type: Microsoft.DSC.Debug/Echo - condition: "[equals('skip', 'yes')]" - properties: - output: "This should not be executed" - - name: test2 - type: Microsoft.DSC.Debug/Echo - condition: "[equals('no', 'no')]" - properties: - output: "This should be executed" +Describe 'Resource condition tests' { + BeforeAll { + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Microsoft.DSC.Debug/Echo + condition: "[equals('skip', 'yes')]" + properties: + output: "This should not be executed" + - name: test2 + type: Microsoft.DSC.Debug/Echo + condition: "[equals('no', 'no')]" + properties: + output: "This should be executed" '@ + } -} - -Describe 'Resource condition tests' { It 'resource should be skipped for ' -TestCases @( @{ operation = 'get'; property = 'actualState' }, @{ operation = 'set'; property = 'afterState' },