diff --git a/powershell-adapter/Tests/class_ps_resources.dsc.yaml b/powershell-adapter/Tests/class_ps_resources.dsc.yaml index 496845ab..a0eb4aa5 100644 --- a/powershell-adapter/Tests/class_ps_resources.dsc.yaml +++ b/powershell-adapter/Tests/class_ps_resources.dsc.yaml @@ -7,10 +7,6 @@ resources: type: Microsoft.DSC/PowerShell properties: resources: - - name: Repository Info - type: PSTestModule/TestPSRepository - properties: - Name: TestPSRepository1 - name: Class-resource Info type: TestClassResource/TestClassResource properties: diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index 5d0e47ce..269b6d4e 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -4,13 +4,9 @@ Describe 'PowerShell adapter resource tests' { BeforeAll { - if ($isWindows) { - winrm quickconfig -quiet -force - } $OldPSModulePath = $env:PSModulePath $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot $pwshConfigPath = Join-path $PSScriptRoot "class_ps_resources.dsc.yaml" - $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" if ($IsLinux -or $IsMacOS) { $cacheFilePath = Join-Path $env:HOME ".dsc" "PSAdapterCache.json" @@ -18,7 +14,6 @@ Describe 'PowerShell adapter resource tests' { else { $cacheFilePath = Join-Path $env:LocalAppData "dsc" "PSAdapterCache.json" - $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" } } AfterAll { @@ -27,47 +22,32 @@ Describe 'PowerShell adapter resource tests' { BeforeEach { Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 } - It 'Get works on config with class-based and script-based resources' -Skip:(!$IsWindows){ + It 'Get works on config with class-based resources' -Skip:(!$IsWindows){ $r = Get-Content -Raw $pwshConfigPath | dsc config get $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results[0].result.actualState.result[0].properties.PublishLocation | Should -BeExactly 'https://www.powershellgallery.com/api/v2/package/' - $res.results[0].result.actualState.result[1].properties.Prop1 | Should -BeExactly 'ValueForProp1' - $res.results[0].result.actualState.result[1].properties.EnumProp | Should -BeExactly 'Expected' - } - - It 'Get works on config with File resource for WinPS' -Skip:(!$IsWindows){ - - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = (Get-Content -Raw $winpsConfigPath).Replace('c:\test.txt',"$testFile") | dsc config get - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.results[0].result.actualState.result[0].properties.DestinationPath | Should -Be "$testFile" + $res.results[0].result.actualState.result[0].properties.Prop1 | Should -BeExactly 'ValueForProp1' + $res.results[0].result.actualState.result[0].properties.EnumProp | Should -BeExactly 'Expected' } - It 'Test works on config with class-based and script-based resources' -Skip:(!$IsWindows){ + It 'Test works on config with class-based resources' -Skip:(!$IsWindows){ $r = Get-Content -Raw $pwshConfigPath | dsc config test $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json $res.results[0].result.actualState.result[0] | Should -Not -BeNull - $res.results[0].result.actualState.result[1] | Should -Not -BeNull } - It 'Set works on config with class-based and script-based resources' -Skip:(!$IsWindows){ + It 'Set works on config with class-based resources' -Skip:(!$IsWindows){ $r = Get-Content -Raw $pwshConfigPath | dsc config set $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results.result.afterState.result[0].type | Should -Be "PSTestModule/TestPSRepository" - $res.results.result.afterState.result[1].type | Should -Be "TestClassResource/TestClassResource" + $res.results.result.afterState.result[0].type | Should -Be "TestClassResource/TestClassResource" } - It 'Export works on config with class-based resources' -Skip:(!$IsWindows){ diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index 40a7daa7..5dd3dcb1 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -4,9 +4,6 @@ Describe 'PowerShell adapter resource tests' { BeforeAll { - if ($isWindows) { - winrm quickconfig -quiet -force - } $OldPSModulePath = $env:PSModulePath $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot @@ -16,7 +13,6 @@ Describe 'PowerShell adapter resource tests' { else { $cacheFilePath = Join-Path $env:LocalAppData "dsc" "PSAdapterCache.json" - $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" } } AfterAll { @@ -25,7 +21,6 @@ Describe 'PowerShell adapter resource tests' { BeforeEach { Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 } It 'Discovery includes class-based and script-based resources ' -Skip:(!$IsWindows){ @@ -37,34 +32,6 @@ Describe 'PowerShell adapter resource tests' { ($resources | ? {$_.Type -eq 'PSTestModule/TestPSRepository'}).Count | Should -Be 1 } - It 'Windows PowerShell adapter supports File resource' -Skip:(!$IsWindows){ - - $r = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell - $LASTEXITCODE | Should -Be 0 - $resources = $r | ConvertFrom-Json - ($resources | ? {$_.Type -eq 'PSDesiredStateConfiguration/File'}).Count | Should -Be 1 - } - - It 'Get works on Binary "File" resource' -Skip:(!$IsWindows){ - - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = '{"DestinationPath":"' + $testFile.replace('\','\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result.properties.DestinationPath | Should -Be "$testFile" - } - - It 'Get works on traditional "Script" resource' -Skip:(!$IsWindows){ - - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\','\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result.properties.result | Should -Be 'test' - } - It 'Get works on class-based resource' -Skip:(!$IsWindows){ $r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource' @@ -73,14 +40,6 @@ Describe 'PowerShell adapter resource tests' { $res.actualState.result.properties.Prop1 | Should -BeExactly 'ValueForProp1' } - It 'Get works on script-based resource' -Skip:(!$IsWindows){ - - $r = "{'Name':'TestPSRepository1'}" | dsc resource get -r 'PSTestModule/TestPSRepository' - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result.properties.PublishLocation | Should -BeExactly 'https://www.powershellgallery.com/api/v2/package/' - } - It 'Get uses enum names on class-based resource' -Skip:(!$IsWindows){ $r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource' @@ -89,14 +48,6 @@ Describe 'PowerShell adapter resource tests' { $res.actualState.result.properties.EnumProp | Should -BeExactly 'Expected' } - It 'Get uses enum names on script-based resource' -Skip:(!$IsWindows){ - - $r = "{'Name':'TestPSRepository1'}" | dsc resource get -r 'PSTestModule/TestPSRepository' - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result.properties.Ensure | Should -BeExactly 'Present' - } - It 'Test works on class-based resource' -Skip:(!$IsWindows){ $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'TestClassResource/TestClassResource' @@ -105,14 +56,6 @@ Describe 'PowerShell adapter resource tests' { $res.actualState.result.properties.InDesiredState | Should -Be $True } - It 'Test works on script-based resource' -Skip:(!$IsWindows){ - - $r = "{'Name':'TestPSRepository1','PackageManagementProvider':'NuGet'}" | dsc resource test -r 'PSTestModule/TestPSRepository' - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result.properties.InDesiredState | Should -Be $True - } - It 'Set works on class-based resource' -Skip:(!$IsWindows){ $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource set -r 'TestClassResource/TestClassResource' @@ -121,14 +64,6 @@ Describe 'PowerShell adapter resource tests' { $res.afterState.result | Should -Not -BeNull } - It 'Set works on script-based resource' -Skip:(!$IsWindows){ - - $r = "{'Name':'TestPSRepository1'}" | dsc resource set -r 'PSTestModule/TestPSRepository' - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.afterState.result.properties.RebootRequired | Should -Not -BeNull - } - It 'Export works on PS class-based resource' -Skip:(!$IsWindows){ $r = dsc resource export -r TestClassResource/TestClassResource diff --git a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 new file mode 100644 index 00000000..bf8d468f --- /dev/null +++ b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'WindowsPowerShell adapter resource tests' { + + BeforeAll { + if ($isWindows) { + winrm quickconfig -quiet -force + } + $OldPSModulePath = $env:PSModulePath + $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot + + $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" + if ($isWindows) { + $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" + } + } + AfterAll { + $env:PSModulePath = $OldPSModulePath + } + + BeforeEach { + if ($isWindows) { + Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 + } + } + + It 'Windows PowerShell adapter supports File resource' -Skip:(!$IsWindows){ + + $r = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell + $LASTEXITCODE | Should -Be 0 + $resources = $r | ConvertFrom-Json + ($resources | ? {$_.Type -eq 'PSDesiredStateConfiguration/File'}).Count | Should -Be 1 + } + + It 'Get works on Binary "File" resource' -Skip:(!$IsWindows){ + + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = '{"DestinationPath":"' + $testFile.replace('\','\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.result.properties.DestinationPath | Should -Be "$testFile" + } + + It 'Get works on traditional "Script" resource' -Skip:(!$IsWindows){ + + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\','\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.result.properties.result | Should -Be 'test' + } + + It 'Get works on config with File resource for WinPS' -Skip:(!$IsWindows){ + + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = (Get-Content -Raw $winpsConfigPath).Replace('c:\test.txt',"$testFile") | dsc config get + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.results[0].result.actualState.result[0].properties.DestinationPath | Should -Be "$testFile" + } +} diff --git a/powershell-adapter/copy_files.txt b/powershell-adapter/copy_files.txt index 0f565603..f97aeff1 100644 --- a/powershell-adapter/copy_files.txt +++ b/powershell-adapter/copy_files.txt @@ -1,3 +1,5 @@ ./psDscAdapter/powershell.resource.ps1 ./psDscAdapter/psDscAdapter.psd1 -./psDscAdapter/psDscAdapter.psm1 \ No newline at end of file +./psDscAdapter/psDscAdapter.psm1 +./psDscAdapter/win_psDscAdapter.psd1 +./psDscAdapter/win_psDscAdapter.psm1 \ No newline at end of file diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index 56bd1b71..2cb8d99c 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -15,7 +15,13 @@ if ('Validate' -ne $Operation) { $host.ui.WriteErrorLine($trace) # load private functions of psDscAdapter stub module - $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru + if ($PSVersionTable.PSVersion.Major -le 5) { + $psDscAdapter = Import-Module "$PSScriptRoot/win_psDscAdapter.psd1" -Force -PassThru + } + else { + $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru + } + # initialize OUTPUT as array $result = [System.Collections.Generic.List[Object]]::new() diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 1b7712af..6bd234f2 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -15,20 +15,10 @@ function Write-DscTrace { $host.ui.WriteErrorLine($trace) } -# if the version of PowerShell is greater than 5, import the PSDesiredStateConfiguration module -# this is necessary because the module is not included in the PowerShell 7.0+ releases; -# In Windows PowerShell, we should always use version 1.1 that ships in Windows. -if ($PSVersionTable.PSVersion.Major -gt 5) { +function Import-PSDSCModule { $m = Get-Module PSDesiredStateConfiguration -ListAvailable | Sort-Object -Descending | Select-Object -First 1 $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -else { - $env:PSModulePath += ";$env:windir\System32\WindowsPowerShell\v1.0\Modules" - $PSDesiredStateConfiguration = Import-Module -Name 'PSDesiredStateConfiguration' -RequiredVersion '1.1' -Force -PassThru -ErrorAction stop -ErrorVariable $importModuleError - if (-not [string]::IsNullOrEmpty($importModuleError)) { - 'ERROR: Could not import PSDesiredStateConfiguration 1.1 in Windows PowerShell. ' + $importModuleError | Write-DscTrace - } -} <# public function Invoke-DscCacheRefresh .SYNOPSIS @@ -123,31 +113,8 @@ function Invoke-DscCacheRefresh { # create a list object to store cache of Get-DscResource [dscResourceCacheEntry[]]$dscResourceCacheEntries = [System.Collections.Generic.List[Object]]::new() - # improve by performance by having the option to only get details for named modules - # workaround for File and SignatureValidation resources that ship in Windows - if ($null -ne $module -and 'PSDesiredStateConfiguration' -ne $module) { - if ($module.gettype().name -eq 'string') { - $module = @($module) - } - $DscResources = [System.Collections.Generic.List[Object]]::new() - $Modules = [System.Collections.Generic.List[Object]]::new() - foreach ($m in $module) { - $DscResources += Get-DscResource -Module $m - $Modules += Get-Module -Name $m -ListAvailable - } - } - elseif ('PSDesiredStateConfiguration' -eq $module -and $PSVersionTable.PSVersion.Major -le 5 ) { - # the resources in Windows should only load in Windows PowerShell - # workaround: the binary modules don't have a module name, so we have to special case File and SignatureValidation resources that ship in Windows - $DscResources = Get-DscResource | Where-Object { $_.modulename -eq 'PSDesiredStateConfiguration' -or ( $_.modulename -eq $null -and $_.parentpath -like "$env:windir\System32\Configuration\*" ) } - } - else { - # if no module is specified, get all resources - $DscResources = Get-DscResource - $Modules = Get-Module -ListAvailable - } - - $psdscVersion = Get-Module PSDesiredStateConfiguration | Sort-Object -descending | Select-Object -First 1 | ForEach-Object Version + Import-PSDSCModule + $DscResources = Get-DscResource foreach ($dscResource in $DscResources) { # resources that shipped in Windows should only be used with Windows PowerShell @@ -155,8 +122,7 @@ function Invoke-DscCacheRefresh { continue } - # we can't run this check in PSDesiredStateConfiguration 1.1 because the property doesn't exist - if ( $psdscVersion -ge '2.0.7' ) { + if ( $dscResource.ImplementationDetail ) { # only support known dscResourceType if ([dscResourceType].GetEnumNames() -notcontains $dscResource.ImplementationDetail) { 'WARNING: implementation detail not found: ' + $dscResource.ImplementationDetail | Write-DscTrace @@ -164,13 +130,6 @@ function Invoke-DscCacheRefresh { } } - # workaround: if the resource does not have a module name, get it from parent path - # workaround: modulename is not settable, so clone the object without being read-only - # workaround: we have to special case File and SignatureValidation resources that ship in Windows - $binaryBuiltInModulePaths = @( - "$env:windir\system32\Configuration\Schema\MSFT_FileDirectoryConfiguration" - "$env:windir\system32\Configuration\BaseRegistration" - ) $DscResourceInfo = [DscResourceInfo]::new() $dscResource.PSObject.Properties | ForEach-Object -Process { if ($null -ne $_.Value) { @@ -181,30 +140,7 @@ function Invoke-DscCacheRefresh { } } - if ($dscResource.ModuleName) { - $moduleName = $dscResource.ModuleName - } - elseif ($binaryBuiltInModulePaths -contains $dscResource.ParentPath) { - $moduleName = 'PSDesiredStateConfiguration' - $DscResourceInfo.Module = 'PSDesiredStateConfiguration' - $DscResourceInfo.ModuleName = 'PSDesiredStateConfiguration' - $DscResourceInfo.CompanyName = 'Microsoft Corporation' - $DscResourceInfo.Version = '1.0.0' - if ($PSVersionTable.PSVersion.Major -le 5 -and $DscResourceInfo.ImplementedAs -eq 'Binary') { - $DscResourceInfo.ImplementationDetail = 'Binary' - } - } - elseif ($binaryBuiltInModulePaths -notcontains $dscResource.ParentPath -and $null -ne $dscResource.ParentPath) { - # workaround: populate module name from parent path that is three levels up - $moduleName = Split-Path $dscResource.ParentPath | Split-Path | Split-Path -Leaf - $DscResourceInfo.Module = $moduleName - $DscResourceInfo.ModuleName = $moduleName - # workaround: populate module version from psmoduleinfo if available - if ($moduleInfo = $Modules | Where-Object { $_.Name -eq $moduleName }) { - $moduleInfo = $moduleInfo | Sort-Object -Property Version -Descending | Select-Object -First 1 - $DscResourceInfo.Version = $moduleInfo.Version.ToString() - } - } + $moduleName = $dscResource.ModuleName # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} @@ -249,13 +185,7 @@ function Get-DscResourceObject { 'WARNING: The input has a top level property named "resources" but is not a configuration. If the input should be a configuration, include the property: "metadata": {"Microsoft.DSC": {"context": "Configuration"}}' | Write-DscTrace } - # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) { - $adapterName = 'Microsoft.Windows/WindowsPowerShell' - } - else { - $adapterName = 'Microsoft.DSC/PowerShell' - } + $adapterName = 'Microsoft.DSC/PowerShell' if ($null -ne $inputObj.metadata -and $null -ne $inputObj.metadata.'Microsoft.DSC' -and $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { # change the type from pscustomobject to dscResourceObject @@ -315,54 +245,9 @@ function Invoke-DscOperation { if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) } } - 'DSC resource implementation: ' + [dscResourceType]$cachedDscResourceInfo.ImplementationDetail | Write-DscTrace - # workaround: script based resources do not validate Get parameter consistency, so we need to remove any parameters the author chose not to include in Get-TargetResource switch ([dscResourceType]$cachedDscResourceInfo.ImplementationDetail) { - 'ScriptBased' { - - # For Linux/MacOS, only class based resources are supported and are called directly. - if ($IsLinux) { - 'ERROR: Script based resources are only supported on Windows.' | Write-DscTrace - exit 1 - } - - # imports the .psm1 file for the DSC resource as a PowerShell module and stores the list of parameters - Import-Module -Scope Local -Name $cachedDscResourceInfo.path -Force -ErrorAction stop - $validParams = (Get-Command -Module $cachedDscResourceInfo.ResourceType -Name 'Get-TargetResource').Parameters.Keys - - if ($Operation -eq 'Get') { - # prune any properties that are not valid parameters of Get-TargetResource - $DesiredState.properties.psobject.properties | ForEach-Object -Process { - if ($validParams -notcontains $_.Name) { - $DesiredState.properties.psobject.properties.Remove($_.Name) - } - } - } - - # morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource - $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value } - - # using the cmdlet the appropriate dsc module, and handle errors - try { - $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property - - if ($invokeResult.GetType().Name -eq 'Hashtable') { - $invokeResult.keys | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $invokeResult.$_ } - } - else { - # the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties - $invokeResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $invokeResult.$_ } - } - - # set the properties of the OUTPUT object from the result of Get-TargetResource - $addToActualState.properties = $getDscResult - } - catch { - 'ERROR: ' + $_.Exception.Message | Write-DscTrace - exit 1 - } - } + 'ClassBased' { try { # load powershell class from external module @@ -402,42 +287,8 @@ function Invoke-DscOperation { exit 1 } } - 'Binary' { - if ($PSVersionTable.PSVersion.Major -gt 5) { - 'To use a binary resource such as File, Log, or SignatureValidation, use the Microsoft.Windows/WindowsPowerShell adapter.' | Write-DscTrace - exit 1 - } - - if (-not (($cachedDscResourceInfo.ImplementedAs -eq 'Binary') -and ('File', 'Log', 'SignatureValidation' -contains $cachedDscResourceInfo.Name))) { - 'Only File, Log, and SignatureValidation are supported as Binary resources.' | Write-DscTrace - exit 1 - } - - # morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource - $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value } - - # using the cmdlet from PSDesiredStateConfiguration module in Windows - try { - $getResult = $PSDesiredStateConfiguration.invoke({ param($Name, $Property) Invoke-DscResource -Name $Name -Method Get -ModuleName @{ModuleName = 'PSDesiredStateConfiguration'; ModuleVersion = '1.1' } -Property $Property -ErrorAction Stop }, $cachedDscResourceInfo.Name, $property ) - - if ($getResult.GetType().Name -eq 'Hashtable') { - $getResult.keys | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $getResult.$_ } - } - else { - # the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties - $getResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $getResult.$_ } - } - - # set the properties of the OUTPUT object from the result of Get-TargetResource - $addToActualState.properties = $getDscResult - } - catch { - 'ERROR: ' + $_.Exception.Message | Write-DscTrace - exit 1 - } - } Default { - 'Can not find implementation of type: ' + $cachedDscResourceInfo.ImplementationDetail | Write-DscTrace + 'Resource ImplementationDetail not supported: ' + $cachedDscResourceInfo.ImplementationDetail | Write-DscTrace exit 1 } } diff --git a/powershell-adapter/psDscAdapter/win_psDscAdapter.psd1 b/powershell-adapter/psDscAdapter/win_psDscAdapter.psd1 new file mode 100644 index 00000000..5d5e7a4f --- /dev/null +++ b/powershell-adapter/psDscAdapter/win_psDscAdapter.psd1 @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'win_psDscAdapter.psm1' + +# Version number of this module. +moduleVersion = '0.0.1' + +# ID used to uniquely identify this module +GUID = 'e0dd561d-c47f-4132-aac9-cd9dc8739bb1' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'PowerShell Desired State Configuration Module for DSC PowerShell Adapter' + +# 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 = @( + 'Get-DscResource' + 'Invoke-DscResource' + 'Invoke-DscCacheRefresh' + ) + +# 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 = @() + +# 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 = @() + +PrivateData = @{ + PSData = @{ + ProjectUri = 'https://github.com/PowerShell/dsc' + } +} +} diff --git a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 new file mode 100644 index 00000000..1b7712af --- /dev/null +++ b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 @@ -0,0 +1,508 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Write-DscTrace { + param( + [Parameter(Mandatory = $false)] + [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] + [string]$Operation = 'Debug', + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Message + ) + + $trace = @{$Operation = $Message } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) +} + +# if the version of PowerShell is greater than 5, import the PSDesiredStateConfiguration module +# this is necessary because the module is not included in the PowerShell 7.0+ releases; +# In Windows PowerShell, we should always use version 1.1 that ships in Windows. +if ($PSVersionTable.PSVersion.Major -gt 5) { + $m = Get-Module PSDesiredStateConfiguration -ListAvailable | Sort-Object -Descending | Select-Object -First 1 + $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru +} +else { + $env:PSModulePath += ";$env:windir\System32\WindowsPowerShell\v1.0\Modules" + $PSDesiredStateConfiguration = Import-Module -Name 'PSDesiredStateConfiguration' -RequiredVersion '1.1' -Force -PassThru -ErrorAction stop -ErrorVariable $importModuleError + if (-not [string]::IsNullOrEmpty($importModuleError)) { + 'ERROR: Could not import PSDesiredStateConfiguration 1.1 in Windows PowerShell. ' + $importModuleError | Write-DscTrace + } +} + +<# public function Invoke-DscCacheRefresh +.SYNOPSIS + This function caches the results of the Get-DscResource call to optimize performance. + +.DESCRIPTION + This function is designed to improve the performance of DSC operations by caching the results of the Get-DscResource call. + By storing the results, subsequent calls to Get-DscResource can retrieve the cached data instead of making a new call each time. + This can significantly speed up operations that need to repeatedly access DSC resources. + +.EXAMPLE + Invoke-DscCacheRefresh -Module "PSDesiredStateConfiguration" +#> +function Invoke-DscCacheRefresh { + [CmdletBinding(HelpUri = '')] + param( + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Object[]] + $Module + ) + + $refreshCache = $false + + $cacheFilePath = if ($IsWindows) { + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" + } else { + # either WinPS or PS 6+ on Linux/Mac + if ($PSVersionTable.PSVersion.Major -le 5) { + Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" + } else { + Join-Path $env:HOME ".dsc" "PSAdapterCache.json" + } + } + + if (Test-Path $cacheFilePath) { + "Reading from Get-DscResource cache file $cacheFilePath" | Write-DscTrace + + $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json + $dscResourceCacheEntries = $cache.ResourceCache + + if ($dscResourceCacheEntries.Count -eq 0) { + # if there is nothing in the cache file - refresh cache + $refreshCache = $true + + "Filtered DscResourceCache cache is empty" | Write-DscTrace + } + else + { + "Checking cache for stale entries" | Write-DscTrace + + foreach ($cacheEntry in $dscResourceCacheEntries) { + "Checking cache entry '$($cacheEntry.Type) $($cacheEntry.LastWriteTimes)'" | Write-DscTrace -Operation Trace + + $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { + + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) + { + "Detected stale cache entry '$($_.Name)'" | Write-DscTrace + $refreshCache = $true + break + } + } + + if ($refreshCache) {break} + } + + "Checking cache for stale PSModulePath" | Write-DscTrace + + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + + $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) + $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) + $hs_cache.SymmetricExceptWith($hs_live) + $diff = $hs_cache + + "PSModulePath diff '$diff'" | Write-DscTrace + + if ($diff.Count -gt 0) { + $refreshCache = $true + } + } + } + else { + "Cache file not found '$cacheFilePath'" | Write-DscTrace + $refreshCache = $true + } + + if ($refreshCache) { + 'Constructing Get-DscResource cache' | Write-DscTrace + + # create a list object to store cache of Get-DscResource + [dscResourceCacheEntry[]]$dscResourceCacheEntries = [System.Collections.Generic.List[Object]]::new() + + # improve by performance by having the option to only get details for named modules + # workaround for File and SignatureValidation resources that ship in Windows + if ($null -ne $module -and 'PSDesiredStateConfiguration' -ne $module) { + if ($module.gettype().name -eq 'string') { + $module = @($module) + } + $DscResources = [System.Collections.Generic.List[Object]]::new() + $Modules = [System.Collections.Generic.List[Object]]::new() + foreach ($m in $module) { + $DscResources += Get-DscResource -Module $m + $Modules += Get-Module -Name $m -ListAvailable + } + } + elseif ('PSDesiredStateConfiguration' -eq $module -and $PSVersionTable.PSVersion.Major -le 5 ) { + # the resources in Windows should only load in Windows PowerShell + # workaround: the binary modules don't have a module name, so we have to special case File and SignatureValidation resources that ship in Windows + $DscResources = Get-DscResource | Where-Object { $_.modulename -eq 'PSDesiredStateConfiguration' -or ( $_.modulename -eq $null -and $_.parentpath -like "$env:windir\System32\Configuration\*" ) } + } + else { + # if no module is specified, get all resources + $DscResources = Get-DscResource + $Modules = Get-Module -ListAvailable + } + + $psdscVersion = Get-Module PSDesiredStateConfiguration | Sort-Object -descending | Select-Object -First 1 | ForEach-Object Version + + foreach ($dscResource in $DscResources) { + # resources that shipped in Windows should only be used with Windows PowerShell + if ($dscResource.ParentPath -like "$env:windir\System32\*" -and $PSVersionTable.PSVersion.Major -gt 5) { + continue + } + + # we can't run this check in PSDesiredStateConfiguration 1.1 because the property doesn't exist + if ( $psdscVersion -ge '2.0.7' ) { + # only support known dscResourceType + if ([dscResourceType].GetEnumNames() -notcontains $dscResource.ImplementationDetail) { + 'WARNING: implementation detail not found: ' + $dscResource.ImplementationDetail | Write-DscTrace + continue + } + } + + # workaround: if the resource does not have a module name, get it from parent path + # workaround: modulename is not settable, so clone the object without being read-only + # workaround: we have to special case File and SignatureValidation resources that ship in Windows + $binaryBuiltInModulePaths = @( + "$env:windir\system32\Configuration\Schema\MSFT_FileDirectoryConfiguration" + "$env:windir\system32\Configuration\BaseRegistration" + ) + $DscResourceInfo = [DscResourceInfo]::new() + $dscResource.PSObject.Properties | ForEach-Object -Process { + if ($null -ne $_.Value) { + $DscResourceInfo.$($_.Name) = $_.Value + } + else { + $DscResourceInfo.$($_.Name) = '' + } + } + + if ($dscResource.ModuleName) { + $moduleName = $dscResource.ModuleName + } + elseif ($binaryBuiltInModulePaths -contains $dscResource.ParentPath) { + $moduleName = 'PSDesiredStateConfiguration' + $DscResourceInfo.Module = 'PSDesiredStateConfiguration' + $DscResourceInfo.ModuleName = 'PSDesiredStateConfiguration' + $DscResourceInfo.CompanyName = 'Microsoft Corporation' + $DscResourceInfo.Version = '1.0.0' + if ($PSVersionTable.PSVersion.Major -le 5 -and $DscResourceInfo.ImplementedAs -eq 'Binary') { + $DscResourceInfo.ImplementationDetail = 'Binary' + } + } + elseif ($binaryBuiltInModulePaths -notcontains $dscResource.ParentPath -and $null -ne $dscResource.ParentPath) { + # workaround: populate module name from parent path that is three levels up + $moduleName = Split-Path $dscResource.ParentPath | Split-Path | Split-Path -Leaf + $DscResourceInfo.Module = $moduleName + $DscResourceInfo.ModuleName = $moduleName + # workaround: populate module version from psmoduleinfo if available + if ($moduleInfo = $Modules | Where-Object { $_.Name -eq $moduleName }) { + $moduleInfo = $moduleInfo | Sort-Object -Property Version -Descending | Select-Object -First 1 + $DscResourceInfo.Version = $moduleInfo.Version.ToString() + } + } + + # fill in resource files (and their last-write-times) that will be used for up-do-date checks + $lastWriteTimes = @{} + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { + $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) + } + + $dscResourceCacheEntries += [dscResourceCacheEntry]@{ + Type = "$moduleName/$($dscResource.Name)" + DscResourceInfo = $DscResourceInfo + LastWriteTimes = $lastWriteTimes + } + } + + [dscResourceCache]$cache = [dscResourceCache]::new() + $cache.ResourceCache = $dscResourceCacheEntries + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $cache.PSModulePaths = $m.FullName + + # save cache for future use + # TODO: replace this with a high-performance serializer + "Saving Get-DscResource cache to '$cacheFilePath'" | Write-DscTrace + $jsonCache = $cache | ConvertTo-Json -Depth 90 + New-Item -Force -Path $cacheFilePath -Value $jsonCache -Type File | Out-Null + } + + return $dscResourceCacheEntries +} + +# Convert the INPUT to a dscResourceObject object so configuration and resource are standardized as much as possible +function Get-DscResourceObject { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $jsonInput + ) + # normalize the INPUT object to an array of dscResourceObject objects + $inputObj = $jsonInput | ConvertFrom-Json + $desiredState = [System.Collections.Generic.List[Object]]::new() + + # catch potential for improperly formatted configuration input + if ($inputObj.resources -and -not $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { + 'WARNING: The input has a top level property named "resources" but is not a configuration. If the input should be a configuration, include the property: "metadata": {"Microsoft.DSC": {"context": "Configuration"}}' | Write-DscTrace + } + + # match adapter to version of powershell + if ($PSVersionTable.PSVersion.Major -le 5) { + $adapterName = 'Microsoft.Windows/WindowsPowerShell' + } + else { + $adapterName = 'Microsoft.DSC/PowerShell' + } + + if ($null -ne $inputObj.metadata -and $null -ne $inputObj.metadata.'Microsoft.DSC' -and $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { + # change the type from pscustomobject to dscResourceObject + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } + } + } + else { + # mimic a config object with a single resource + $type = $inputObj.type + $inputObj.psobject.properties.Remove('type') + $desiredState += [dscResourceObject]@{ + name = $adapterName + type = $type + properties = $inputObj + } + } + return $desiredState +} + +# Get the actual state using DSC Get method from any type of DSC resource +function Invoke-DscOperation { + param( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Set', 'Test', 'Export')] + [string]$Operation, + [Parameter(Mandatory, ValueFromPipeline = $true)] + [dscResourceObject]$DesiredState, + [Parameter(Mandatory)] + [dscResourceCacheEntry[]]$dscResourceCache + ) + + $osVersion = [System.Environment]::OSVersion.VersionString + 'OS version: ' + $osVersion | Write-DscTrace + + $psVersion = $PSVersionTable.PSVersion.ToString() + 'PowerShell version: ' + $psVersion | Write-DscTrace + + $moduleVersion = Get-Module PSDesiredStateConfiguration | ForEach-Object Version + 'PSDesiredStateConfiguration module version: ' + $moduleVersion | Write-DscTrace + + # get details from cache about the DSC resource, if it exists + $cachedDscResourceInfo = $dscResourceCache | Where-Object Type -EQ $DesiredState.type | ForEach-Object DscResourceInfo + + # if the resource is found in the cache, get the actual state + if ($cachedDscResourceInfo) { + + # formated OUTPUT of each resource + $addToActualState = [dscResourceObject]@{} + + # set top level properties of the OUTPUT object from INPUT object + $DesiredState.psobject.properties | ForEach-Object -Process { + if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) } + } + + 'DSC resource implementation: ' + [dscResourceType]$cachedDscResourceInfo.ImplementationDetail | Write-DscTrace + + # workaround: script based resources do not validate Get parameter consistency, so we need to remove any parameters the author chose not to include in Get-TargetResource + switch ([dscResourceType]$cachedDscResourceInfo.ImplementationDetail) { + 'ScriptBased' { + + # For Linux/MacOS, only class based resources are supported and are called directly. + if ($IsLinux) { + 'ERROR: Script based resources are only supported on Windows.' | Write-DscTrace + exit 1 + } + + # imports the .psm1 file for the DSC resource as a PowerShell module and stores the list of parameters + Import-Module -Scope Local -Name $cachedDscResourceInfo.path -Force -ErrorAction stop + $validParams = (Get-Command -Module $cachedDscResourceInfo.ResourceType -Name 'Get-TargetResource').Parameters.Keys + + if ($Operation -eq 'Get') { + # prune any properties that are not valid parameters of Get-TargetResource + $DesiredState.properties.psobject.properties | ForEach-Object -Process { + if ($validParams -notcontains $_.Name) { + $DesiredState.properties.psobject.properties.Remove($_.Name) + } + } + } + + # morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource + $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value } + + # using the cmdlet the appropriate dsc module, and handle errors + try { + $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property + + if ($invokeResult.GetType().Name -eq 'Hashtable') { + $invokeResult.keys | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $invokeResult.$_ } + } + else { + # the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties + $invokeResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $invokeResult.$_ } + } + + # set the properties of the OUTPUT object from the result of Get-TargetResource + $addToActualState.properties = $getDscResult + } + catch { + 'ERROR: ' + $_.Exception.Message | Write-DscTrace + exit 1 + } + } + 'ClassBased' { + try { + # load powershell class from external module + $resource = GetTypeInstanceFromModule -modulename $cachedDscResourceInfo.ModuleName -classname $cachedDscResourceInfo.Name + $dscResourceInstance = $resource::New() + + if ($DesiredState.properties) { + # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object + $DesiredState.properties.psobject.properties | ForEach-Object -Process { + $dscResourceInstance.$($_.Name) = $_.Value + } + } + + switch ($Operation) { + 'Get' { + $Result = $dscResourceInstance.Get() + $addToActualState.properties = $Result + } + 'Set' { + $dscResourceInstance.Set() + } + 'Test' { + $Result = $dscResourceInstance.Test() + $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} + } + 'Export' { + $t = $dscResourceInstance.GetType() + $method = $t.GetMethod('Export') + $resultArray = $method.Invoke($null,$null) + $addToActualState = $resultArray + } + } + } + catch { + + 'ERROR: ' + $_.Exception.Message | Write-DscTrace + exit 1 + } + } + 'Binary' { + if ($PSVersionTable.PSVersion.Major -gt 5) { + 'To use a binary resource such as File, Log, or SignatureValidation, use the Microsoft.Windows/WindowsPowerShell adapter.' | Write-DscTrace + exit 1 + } + + if (-not (($cachedDscResourceInfo.ImplementedAs -eq 'Binary') -and ('File', 'Log', 'SignatureValidation' -contains $cachedDscResourceInfo.Name))) { + 'Only File, Log, and SignatureValidation are supported as Binary resources.' | Write-DscTrace + exit 1 + } + + # morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource + $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value } + + # using the cmdlet from PSDesiredStateConfiguration module in Windows + try { + $getResult = $PSDesiredStateConfiguration.invoke({ param($Name, $Property) Invoke-DscResource -Name $Name -Method Get -ModuleName @{ModuleName = 'PSDesiredStateConfiguration'; ModuleVersion = '1.1' } -Property $Property -ErrorAction Stop }, $cachedDscResourceInfo.Name, $property ) + + if ($getResult.GetType().Name -eq 'Hashtable') { + $getResult.keys | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $getResult.$_ } + } + else { + # the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties + $getResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $getResult.$_ } + } + + # set the properties of the OUTPUT object from the result of Get-TargetResource + $addToActualState.properties = $getDscResult + } + catch { + 'ERROR: ' + $_.Exception.Message | Write-DscTrace + exit 1 + } + } + Default { + 'Can not find implementation of type: ' + $cachedDscResourceInfo.ImplementationDetail | Write-DscTrace + exit 1 + } + } + + return $addToActualState + } + else { + $dsJSON = $DesiredState | ConvertTo-Json -Depth 10 + $errmsg = 'Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.' + 'ERROR: ' + $errmsg | Write-DscTrace + exit 1 + } +} + +# GetTypeInstanceFromModule function to get the type instance from the module +function GetTypeInstanceFromModule { + param( + [Parameter(Mandatory = $true)] + [string] $modulename, + [Parameter(Mandatory = $true)] + [string] $classname + ) + $instance = & (Import-Module $modulename -PassThru) ([scriptblock]::Create("'$classname' -as 'type'")) + return $instance +} + +# cached resource +class dscResourceCacheEntry { + [string] $Type + [psobject] $DscResourceInfo + [PSCustomObject] $LastWriteTimes +} + +class dscResourceCache { + [string[]] $PSModulePaths + [dscResourceCacheEntry[]] $ResourceCache +} + +# format expected for configuration and resource output +class dscResourceObject { + [string] $name + [string] $type + [psobject] $properties +} + +# dsc resource types +enum dscResourceType { + ScriptBased + ClassBased + Binary + Composite +} + +# dsc resource type (settable clone) +class DscResourceInfo { + [dscResourceType] $ImplementationDetail + [string] $ResourceType + [string] $Name + [string] $FriendlyName + [string] $Module + [string] $ModuleName + [string] $Version + [string] $Path + [string] $ParentPath + [string] $ImplementedAs + [string] $CompanyName + [psobject[]] $Properties +}