diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index c4119c69..99229e0a 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -94,7 +94,7 @@ impl DscResource { } } - fn create_config_for_adapter(self, adapter: &str, input: &str) -> Result { + fn create_config_for_adapter(self, adapter: &str, input: &str, execution_type: &ExecutionKind) -> Result { // create new configuration with adapter and use this as the resource let mut configuration = Configuration::new(); let mut property_map = Map::new(); @@ -115,7 +115,8 @@ impl DscResource { }; configuration.resources.push(adapter_resource); let config_json = serde_json::to_string(&configuration)?; - let configurator = Configurator::new(&config_json, crate::progress::ProgressFormat::None)?; + let mut configurator = Configurator::new(&config_json, crate::progress::ProgressFormat::None)?; + configurator.context.execution_type = execution_type.clone(); Ok(configurator) } } @@ -218,7 +219,7 @@ impl Invoke for DscResource { fn get(&self, filter: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeGet", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; + let mut configurator = self.clone().create_config_for_adapter(adapter, filter, &ExecutionKind::Actual)?; let result = configurator.invoke_get()?; let GetResult::Resource(ref resource_result) = result.results[0].result else { return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "get", resource = self.type_name).to_string())); @@ -252,7 +253,7 @@ impl Invoke for DscResource { fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { debug!("{}", t!("dscresources.dscresource.invokeSet", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, desired)?; + let mut configurator = self.clone().create_config_for_adapter(adapter, desired, execution_type)?; let result = configurator.invoke_set(false)?; let SetResult::Resource(ref resource_result) = result.results[0].result else { return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "set", resource = self.type_name).to_string())); @@ -295,7 +296,7 @@ impl Invoke for DscResource { fn test(&self, expected: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeTest", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, expected)?; + let mut configurator = self.clone().create_config_for_adapter(adapter, expected, &ExecutionKind::Actual)?; let result = configurator.invoke_test()?; let TestResult::Resource(ref resource_result) = result.results[0].result else { return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "test", resource = self.type_name).to_string())); @@ -367,7 +368,7 @@ impl Invoke for DscResource { fn delete(&self, filter: &str) -> Result<(), DscError> { debug!("{}", t!("dscresources.dscresource.invokeDelete", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; + let mut configurator = self.clone().create_config_for_adapter(adapter, filter, &ExecutionKind::Actual)?; configurator.invoke_set(false)?; return Ok(()); } @@ -429,7 +430,7 @@ impl Invoke for DscResource { fn export(&self, input: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeExport", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, input)?; + let mut configurator = self.clone().create_config_for_adapter(adapter, input, &ExecutionKind::Actual)?; let result = configurator.invoke_export()?; let Some(configuration) = result.result else { return Err(DscError::Operation(t!("dscresources.dscresource.invokeExportReturnedNoResult", resource = self.type_name).to_string())); diff --git a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 index c852f0a1..70ee8864 100644 --- a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 +++ b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 @@ -3,48 +3,49 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'TestClassResource.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'TestClassResource.psm1' -# Version number of this module. -ModuleVersion = '0.0.1' + # Version number of this module. + ModuleVersion = '0.0.1' -# ID used to uniquely identify this module -GUID = 'b267fa32-e77d-48e6-9248-676cc6f2327f' + # ID used to uniquely identify this module + GUID = 'b267fa32-e77d-48e6-9248-676cc6f2327f' -# Author of this module -Author = 'Microsoft' + # Author of this module + Author = 'Microsoft' -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' -# Copyright statement for this module -Copyright = '(c) Microsoft. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) Microsoft. All rights reserved.' -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @() + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '*' + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = '*' -# Variables to export from this module -VariablesToExport = @() + # Variables to export from this module + VariablesToExport = @() -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = @() + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() -# DSC resources to export from this module -DscResourcesToExport = @('TestClassResource', 'NoExport') + # DSC resources to export from this module + DscResourcesToExport = @('TestClassResource', 'NoExport') -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ - PSData = @{ - DscCapabilities = @( - 'get' - 'test' - ) + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + DscCapabilities = @( + 'get' + 'test' + 'whatIf' + 'export' + ) + } } } -} - diff --git a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 index a0e988ff..abfd01b0 100644 --- a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 +++ b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 @@ -13,15 +13,13 @@ enum Ensure { Absent } -class BaseTestClass -{ +class BaseTestClass { [DscProperty()] [string] $BaseProperty } [DscResource()] -class TestClassResource : BaseTestClass -{ +class TestClassResource : BaseTestClass { [DscProperty(Key)] [string] $Name @@ -49,44 +47,36 @@ class TestClassResource : BaseTestClass [DscProperty()] [string] $HiddenDscProperty # This property should be in results data, but is an anti-pattern. - [void] Set() - { + [void] Set() { } - [bool] Test() - { - if (($this.Name -eq "TestClassResource1") -and ($this.Prop1 -eq "ValueForProp1")) - { + [bool] Test() { + if (($this.Name -eq "TestClassResource1") -and ($this.Prop1 -eq "ValueForProp1")) { return $true } - else - { + else { return $false } } - [TestClassResource] Get() - { - if ($this.Name -eq "TestClassResource1") - { + [TestClassResource] Get() { + if ($this.Name -eq "TestClassResource1") { $this.Prop1 = "ValueForProp1" } - else - { + else { $this.Prop1 = $env:DSC_CONFIG_ROOT } $this.EnumProp = ([EnumPropEnumeration]::Expected).ToString() return $this } - static [TestClassResource[]] Export() - { + static [TestClassResource[]] Export() { $resultList = [List[TestClassResource]]::new() $resultCount = 5 if ($env:TestClassResourceResultCount) { $resultCount = $env:TestClassResourceResultCount } - 1..$resultCount | %{ + 1..$resultCount | % { $obj = New-Object TestClassResource $obj.Name = "Object$_" $obj.Prop1 = "Property of object$_" @@ -96,20 +86,17 @@ class TestClassResource : BaseTestClass return $resultList.ToArray() } - static [TestClassResource[]] Export([bool]$UseExport) - { - if ($UseExport) - { + static [TestClassResource[]] Export([bool]$UseExport) { + if ($UseExport) { return [TestClassResource]::Export() } - else - { + else { $resultList = [List[TestClassResource]]::new() $resultCount = 5 if ($env:TestClassResourceResultCount) { $resultCount = $env:TestClassResourceResultCount } - 1..$resultCount | %{ + 1..$resultCount | % { $obj = New-Object TestClassResource $obj.Name = "Object$_" $obj.Prop1 = "Property of object$_" @@ -119,11 +106,21 @@ class TestClassResource : BaseTestClass return $resultList.ToArray() } + + [hashtable] WhatIf() { + $out = @{ + Name = $this.Name + _metadata = @{ + whatIf = "A test message from the WhatIf method of TestClassResource" + } + } + + return $out + } } [DscResource()] -class NoExport: BaseTestClass -{ +class NoExport: BaseTestClass { [DscProperty(Key)] [string] $Name @@ -133,22 +130,19 @@ class NoExport: BaseTestClass [DscProperty()] [string] $EnumProp - [void] Set() - { + [void] Set() { } - [bool] Test() - { + [bool] Test() { return $true } - [NoExport] Get() - { + [NoExport] Get() { return $this } } -function Test-World() -{ + +function Test-World() { "Hello world from PSTestModule!" } diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index 56878954..41398d46 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -101,7 +101,7 @@ Describe 'PowerShell adapter resource tests' { $out = $yaml | dsc config export -f - 2>&1 | Out-String $LASTEXITCODE | Should -Be 2 $out | Should -Not -BeNullOrEmpty - $out | Should -BeLike "*ERROR*Export method not implemented by resource 'TestClassResource/NoExport'*" + $out | Should -BeLike "*ERROR*Method 'Export' not implemented by resource 'NoExport'*" } It 'Custom psmodulepath in config works' { @@ -309,5 +309,22 @@ Describe 'PowerShell adapter resource tests' { $out.resources[0].properties.result[0].Name | Should -Be "Object1" $out.resources[0].properties.result[0].Prop1 | Should -Be "Property of object1" } + + It 'Config whatIf works with class-based resources' { + + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Class-resource Info + type: TestClassResource/TestClassResource + properties: + Name: 'TestClassResource' + Ensure: 'Present' +"@ + $out = dsc config set -i $yaml -w | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results.result.afterstate.name | Should -Be "TestClassResource" + $out.results.result.afterstate._metadata.whatIf | Should -Be "A test message from the WhatIf method of TestClassResource" + } } diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index c8be165e..93b644c3 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -199,13 +199,13 @@ Describe 'PowerShell adapter resource tests' { $files | Copy-Item -Destination $path4 $filePath = Join-Path $path1 'TestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'1.0`'") | Set-Content $filePath + (Get-Content -Raw $filePath).Replace("ModuleVersion = '0.0.1'", "ModuleVersion = `'1.0`'") | Set-Content $filePath $filePath = Join-Path $path2 'TestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'1.1`'") | Set-Content $filePath + (Get-Content -Raw $filePath).Replace("ModuleVersion = '0.0.1'", "ModuleVersion = `'1.1`'") | Set-Content $filePath $filePath = Join-Path $path3 'TestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'2.0`'") | Set-Content $filePath + (Get-Content -Raw $filePath).Replace("ModuleVersion = '0.0.1'", "ModuleVersion = `'2.0`'") | Set-Content $filePath $filePath = Join-Path $path4 'TestClassResource.psd1' - (Get-Content -Raw $filePath).Replace("ModuleVersion = `'0.0.1`'", "ModuleVersion = `'2.0.1`'") | Set-Content $filePath + (Get-Content -Raw $filePath).Replace("ModuleVersion = '0.0.1'", "ModuleVersion = '2.0.1'") | Set-Content $filePath $oldPath = $env:PSModulePath diff --git a/powershell-adapter/powershell.dsc.resource.json b/powershell-adapter/powershell.dsc.resource.json index e481b073..15677e0c 100644 --- a/powershell-adapter/powershell.dsc.resource.json +++ b/powershell-adapter/powershell.dsc.resource.json @@ -1,28 +1,14 @@ { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "type": "Microsoft.DSC/PowerShell", - "version": "0.1.0", - "kind": "adapter", - "description": "Resource adapter to classic DSC Powershell resources.", - "tags": [ - "PowerShell" - ], - "adapter": { - "list": { - "executable": "pwsh", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - "./psDscAdapter/powershell.resource.ps1 List" - ] - }, - "config": "full" - }, - "get": { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.DSC/PowerShell", + "version": "0.1.0", + "kind": "adapter", + "description": "Resource adapter to classic DSC Powershell resources.", + "tags": [ + "PowerShell" + ], + "adapter": { + "list": { "executable": "pwsh", "args": [ "-NoLogo", @@ -31,67 +17,95 @@ "-ExecutionPolicy", "Bypass", "-Command", - "$Input | ./psDscAdapter/powershell.resource.ps1 Get" - ], - "input": "stdin" - }, - "set": { - "executable": "pwsh", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - "$Input | ./psDscAdapter/powershell.resource.ps1 Set" - ], - "input": "stdin", - "implementsPretest": true - }, - "test": { - "executable": "pwsh", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - "$Input | ./psDscAdapter/powershell.resource.ps1 Test" - ], - "input": "stdin", - "return": "state" - }, - "export": { - "executable": "pwsh", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - "$Input | ./psDscAdapter/powershell.resource.ps1 Export" - ], - "input": "stdin", - "return": "state" - }, - "validate": { - "executable": "pwsh", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" - ], - "input": "stdin" - }, - "exitCodes": { - "0": "Success", - "1": "Error" - } + "./psDscAdapter/powershell.resource.ps1 List" + ] + }, + "config": "full" + }, + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Get" + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Set" + ], + "input": "stdin", + "implementsPretest": true + }, + "test": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Test" + ], + "input": "stdin", + "return": "state" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Export" + ], + "input": "stdin", + "return": "state" + }, + "whatIf": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 WhatIf" + ], + "input": "stdin", + "return": "state" + }, + "validate": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error" } +} \ No newline at end of file diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index d5417775..99a00e10 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -3,7 +3,7 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate, ClearCache.')] - [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'ClearCache')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'WhatIf', 'Validate', 'ClearCache')] [string]$Operation, [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] [string]$jsonInput = '@{}' @@ -135,7 +135,7 @@ switch ($Operation) { } | ConvertTo-Json -Compress } } - { @('Get','Set','Test','Export') -contains $_ } { + { @('Get','Set','Test','Export', 'WhatIf') -contains $_ } { $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) if ($null -eq $desiredState) { Write-DscTrace -Operation Error -message 'Failed to create configuration object from provided input JSON.' diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index ffee99d9..f9dff194 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -377,7 +377,7 @@ function Get-DscResourceObject { function Invoke-DscOperation { param( [Parameter(Mandatory)] - [ValidateSet('Get', 'Set', 'Test', 'Export')] + [ValidateSet('Get', 'Set', 'Test', 'Export', 'WhatIf')] [string]$Operation, [Parameter(Mandatory, ValueFromPipeline = $true)] [dscResourceObject]$DesiredState, @@ -464,19 +464,7 @@ function Invoke-DscOperation { $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } } 'Export' { - $t = $dscResourceInstance.GetType() - $methods = $t.GetMethods() | Where-Object { $_.Name -eq 'Export' } - $method = foreach ($mt in $methods) { - if ($mt.GetParameters().Count -eq 0) { - $mt - break - } - } - - if ($null -eq $method) { - "Export method not implemented by resource '$($DesiredState.Type)'" | Write-DscTrace -Operation Error - exit 1 - } + $method = ValidateMethod -operation $Operation -class $dscResourceInstance $resultArray = @() $raw_obj_array = $method.Invoke($null, $null) foreach ($raw_obj in $raw_obj_array) { @@ -493,10 +481,14 @@ function Invoke-DscOperation { } $addToActualState = $resultArray } + 'WhatIf' { + $method = ValidateMethod -operation $Operation -class $dscResourceInstance + $raw_obj = $dscResourceInstance.WhatIf() + $addToActualState.properties = $raw_obj + } } } catch { - 'Exception: ' + $_.Exception.Message | Write-DscTrace -Operation Error exit 1 } @@ -529,6 +521,33 @@ function GetTypeInstanceFromModule { return $instance } +# ValidateMethod checks if the specified method exists in the class +function ValidateMethod { + param ( + [Parameter(Mandatory = $true)] + [ValidateSet('Export', 'WhatIf')] + [string] $operation, + [Parameter(Mandatory = $true)] + [object] $class + ) + + $t = $class.GetType() + $methods = $t.GetMethods() | Where-Object -Property Name -EQ $operation + $method = foreach ($mt in $methods) { + if ($mt.GetParameters().Count -eq 0) { + $mt + break + } + } + + if ($null -eq $method) { + "Method '$operation' not implemented by resource '$($t.Name)'" | Write-DscTrace -Operation Error + exit 1 + } + + return $method +} + # cached resource class dscResourceCacheEntry { [string] $Type