diff --git a/.gitignore b/.gitignore index 5ef3e4cd..9bf9a74f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ node_modules/ tree-sitter-dscexpression/bindings/ tree-sitter-dscexpression/src/ tree-sitter-dscexpression/parser.* -tree-sitter-dscexpression/binding.gyp +tree-sitter-dscexpression/binding.gyp \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index c35303a9..6d7b0d95 100644 --- a/build.ps1 +++ b/build.ps1 @@ -188,7 +188,18 @@ $skip_test_projects_on_windows = @("tree-sitter-dscexpression") if (Test-Path "./copy_files.txt") { Get-Content "./copy_files.txt" | ForEach-Object { - Copy-Item $_ $target -Force -ErrorAction Ignore + # if the line contains a '\' character, throw an error + if ($_ -match '\\') { + throw "copy_files.txt should use '/' as the path separator" + } + # copy the file to the target directory, creating the directory path if needed + $fileCopyPath = $_.split('/') + if ($fileCopyPath.Length -gt 1) { + $fileCopyPath = $fileCopyPath[0..($fileCopyPath.Length - 2)] + $fileCopyPath = $fileCopyPath -join '/' + New-Item -ItemType Directory -Path "$target/$fileCopyPath" -Force -ErrorAction Ignore | Out-Null + } + Copy-Item $_ "$target/$_" -Force -ErrorAction Ignore } } @@ -199,6 +210,8 @@ $skip_test_projects_on_windows = @("tree-sitter-dscexpression") } } + Save-PSResource -Path $target -Name 'PSDesiredStateConfiguration' -Version '2.0.7' -Repository PSGallery -TrustRepository + if ($failed) { Write-Host -ForegroundColor Red "Build failed" exit 1 @@ -243,14 +256,12 @@ if ($Test) { $FullyQualifiedName = @{ModuleName="PSDesiredStateConfiguration";ModuleVersion="2.0.7"} if (-not(Get-Module -ListAvailable -FullyQualifiedName $FullyQualifiedName)) { "Installing module PSDesiredStateConfiguration 2.0.7" - Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted - Install-Module PSDesiredStateConfiguration -RequiredVersion 2.0.7 + Install-PSResource -Name PSDesiredStateConfiguration -Version 2.0.7 -Repository PSGallery -TrustRepository } if (-not(Get-Module -ListAvailable -Name Pester)) { "Installing module Pester" - Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted - Install-Module Pester -WarningAction Ignore + Install-PSResource Pester -WarningAction Ignore -Repository PSGallery -TrustRepository } foreach ($project in $projects) { @@ -295,7 +306,7 @@ if ($Test) { if (-not(Get-Module -ListAvailable -Name Pester)) { "Installing module Pester" $InstallTargetDir = ($env:PSModulePath -split ";")[0] - Find-Module -Name 'Pester' -Repository 'PSGallery' | Save-Module -Path $InstallTargetDir + Find-PSResource -Name 'Pester' -Repository 'PSGallery' | Save-PSResource -Path $InstallTargetDir -TrustRepository } "Updated Pester module location:" diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index b2604193..a83aedc0 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -27,7 +27,8 @@ Describe 'dsc config get tests' { It 'will fail if resource schema does not match' -Skip:(!$IsWindows) { $jsonPath = Join-Path $PSScriptRoot '../examples/invalid_schema.dsc.yaml' $config = Get-Content $jsonPath -Raw - $null = $config | dsc config get | ConvertFrom-Json + $testError = & {$config | dsc config get get 2>&1} + $testError[0] | Should -match 'error:' $LASTEXITCODE | Should -Be 2 } diff --git a/dsc/tests/dsc_get.tests.ps1 b/dsc/tests/dsc_get.tests.ps1 index 96500bb7..a5586e91 100644 --- a/dsc/tests/dsc_get.tests.ps1 +++ b/dsc/tests/dsc_get.tests.ps1 @@ -44,7 +44,8 @@ Describe 'resource get tests' { "Name": "ProductName" } '@ - $json | dsc resource get -r Microsoft.Windows/registry + $testError = & {$json | dsc resource get -r Microsoft.Windows/registry get 2>&1} + $testError[0] | SHould -match 'error:' $LASTEXITCODE | Should -Be 2 } } diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index 7841fcb0..dd22be43 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -81,7 +81,8 @@ Describe 'Parameters tests' { "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json - $null = $config_yaml | dsc config -p $params_json get + $testError = & {$config_yaml | dsc config -p $params_json get 2>&1} + $testError | Should -match 'Parameter input failure:' $LASTEXITCODE | Should -Be 4 } @@ -108,8 +109,9 @@ Describe 'Parameters tests' { "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json - $null = $config_yaml | dsc config -p $params_json get - $LASTEXITCODE | Should -Be 4 + $testError = & {$config_yaml | dsc config -p $params_json get get 2>&1} + $testError[0] | Should -match 'error' + $LASTEXITCODE | Should -Be 2 } It 'Input number value is out of range for and ' -TestCases @( @@ -134,8 +136,9 @@ Describe 'Parameters tests' { "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json - $null = $config_yaml | dsc config -p $params_json get - $LASTEXITCODE | Should -Be 4 + $testError = & {$config_yaml | dsc config -p $params_json get get 2>&1} + $testError[0] | Should -match 'error' + $LASTEXITCODE | Should -Be 2 } It 'Input is not in the allowed value list for ' -TestCases @( @@ -158,8 +161,9 @@ Describe 'Parameters tests' { "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json - $null = $config_yaml | dsc config -p $params_json get - $LASTEXITCODE | Should -Be 4 + $testError = & {$config_yaml | dsc config -p $params_json get get 2>&1} + $testError[0] | Should -match 'error' + $LASTEXITCODE | Should -Be 2 } It 'Length constraint is incorrectly applied to with ' -TestCases @( @@ -184,8 +188,9 @@ Describe 'Parameters tests' { "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json - $null = $config_yaml | dsc config -p $params_json get | ConvertFrom-Json - $LASTEXITCODE | Should -Be 4 + $testError = & {$config_yaml | dsc config -p $params_json get get 2>&1} + $testError[0] | Should -match 'error' + $LASTEXITCODE | Should -Be 2 } It 'Default value is used when not provided' { diff --git a/powershell-adapter/Tests/PSTestModule/DscResources/TestPSRepository/TestPSRepository.psm1 b/powershell-adapter/Tests/PSTestModule/1.0.0/DscResources/TestPSRepository/TestPSRepository.psm1 similarity index 90% rename from powershell-adapter/Tests/PSTestModule/DscResources/TestPSRepository/TestPSRepository.psm1 rename to powershell-adapter/Tests/PSTestModule/1.0.0/DscResources/TestPSRepository/TestPSRepository.psm1 index 7e05639c..3070f102 100644 --- a/powershell-adapter/Tests/PSTestModule/DscResources/TestPSRepository/TestPSRepository.psm1 +++ b/powershell-adapter/Tests/PSTestModule/1.0.0/DscResources/TestPSRepository/TestPSRepository.psm1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + enum EnsureEnumeration { Absent Present @@ -14,7 +17,7 @@ function Get-TargetResource { ) $returnValue = @{ - Ensure = [EnsureEnumeration]::Absent + Ensure = ([EnsureEnumeration]::Absent).ToString() Name = $Name SourceLocation = $null ScriptSourceLocation = $null @@ -27,7 +30,7 @@ function Get-TargetResource { } if ($Name -eq "TestPSRepository1") { - $returnValue.Ensure = [EnsureEnumeration]::Present + $returnValue.Ensure = ([EnsureEnumeration]::Present).ToString() $returnValue.SourceLocation = 'https://www.powershellgallery.com/api/v2' $returnValue.ScriptSourceLocation = 'https://www.powershellgallery.com/api/v2/items/psscript' $returnValue.PublishLocation = 'https://www.powershellgallery.com/api/v2/package/' diff --git a/powershell-adapter/Tests/PSTestModule/DscResources/TestPSRepository/TestPSRepository.schema.mof b/powershell-adapter/Tests/PSTestModule/1.0.0/DscResources/TestPSRepository/TestPSRepository.schema.mof similarity index 100% rename from powershell-adapter/Tests/PSTestModule/DscResources/TestPSRepository/TestPSRepository.schema.mof rename to powershell-adapter/Tests/PSTestModule/1.0.0/DscResources/TestPSRepository/TestPSRepository.schema.mof diff --git a/powershell-adapter/Tests/PSTestModule/PSTestModule.psd1 b/powershell-adapter/Tests/PSTestModule/1.0.0/PSTestModule.psd1 similarity index 74% rename from powershell-adapter/Tests/PSTestModule/PSTestModule.psd1 rename to powershell-adapter/Tests/PSTestModule/1.0.0/PSTestModule.psd1 index 04b40171..7cb6fdf0 100644 --- a/powershell-adapter/Tests/PSTestModule/PSTestModule.psd1 +++ b/powershell-adapter/Tests/PSTestModule/1.0.0/PSTestModule.psd1 @@ -1,16 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. @{ - RootModule = 'TestClassResource.psm1' ModuleVersion = '1.0.0' GUID = '5d73a601-4a6c-43c5-ba3f-619b18bbb404' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' Copyright = '(c) Microsoft Corporation. All rights reserved.' Description = 'PowerShell module for testing DSCv3' - PowerShellVersion = '5.0' - DscResourcesToExport = 'TestClassResource' + PowerShellVersion = '5.1' + DscResourcesToExport = 'TestPSRepository' FunctionsToExport = @( 'Test-World') - VariablesToExport = '@()' + VariablesToExport = @() AliasesToExport = @() PrivateData = @{ PSData = @{ @@ -20,6 +21,7 @@ 'PSEdition_Core', 'Linux', 'Mac') + DscCapabilities = @('Get', 'Test') } } } diff --git a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 new file mode 100644 index 00000000..62e3fdeb --- /dev/null +++ b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psd1 @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'TestClassResource.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# ID used to uniquely identify this module +GUID = 'b267fa32-e77d-48e6-9248-676cc6f2327f' + +# Author of this module +Author = 'Microsoft' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# 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 = @() + +# 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 = @() + +# DSC resources to export from this module +DscResourcesToExport = 'TestClassResource' + +# 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' + ) + } +} + +} + diff --git a/powershell-adapter/Tests/PSTestModule/TestClassResource.psm1 b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 similarity index 87% rename from powershell-adapter/Tests/PSTestModule/TestClassResource.psm1 rename to powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 index 58c5d31d..ab9d19ea 100644 --- a/powershell-adapter/Tests/PSTestModule/TestClassResource.psm1 +++ b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + using namespace System.Collections.Generic enum EnumPropEnumeration { @@ -15,7 +18,7 @@ class TestClassResource [string] $Prop1 [DscProperty()] - [EnumPropEnumeration] $EnumProp + [string] $EnumProp [void] Set() { @@ -43,7 +46,7 @@ class TestClassResource { $this.Prop1 = $env:DSC_CONFIG_ROOT } - $this.EnumProp = [EnumPropEnumeration]::Expected + $this.EnumProp = ([EnumPropEnumeration]::Expected).ToString() return $this } diff --git a/powershell-adapter/Tests/class_ps_resources.dsc.yaml b/powershell-adapter/Tests/class_ps_resources.dsc.yaml index 8eb72c44..a6692b6c 100644 --- a/powershell-adapter/Tests/class_ps_resources.dsc.yaml +++ b/powershell-adapter/Tests/class_ps_resources.dsc.yaml @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json resources: - name: Working with classic DSC resources @@ -9,6 +12,8 @@ resources: properties: Name: TestPSRepository1 - name: Class-resource Info - type: PSTestModule/TestClassResource + type: TestClassResource/TestClassResource properties: Name: TestClassResource1 + Prop1: ValueForProp1 + EnumProp: Expected diff --git a/powershell-adapter/Tests/native_and_powershell.dsc.yaml b/powershell-adapter/Tests/native_and_powershell.dsc.yaml index e99936b0..9c5664b5 100644 --- a/powershell-adapter/Tests/native_and_powershell.dsc.yaml +++ b/powershell-adapter/Tests/native_and_powershell.dsc.yaml @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + # Example configuration mixing native app resources with classic PS resources $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json resources: @@ -20,4 +23,4 @@ resources: properties: keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion valueName: ProductName - _ensure: Present + _ensure: Present \ No newline at end of file diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index ccc261bb..5c18d678 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -4,10 +4,13 @@ Describe 'PowerShell adapter resource tests' { BeforeAll { + if ($isWindows) { + winrm quickconfig -quiet + } $OldPSModulePath = $env:PSModulePath - $env:PSModulePath += ";" + $PSScriptRoot - - $configPath = Join-path $PSScriptRoot "class_ps_resources.dsc.yaml" + $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot + $pwshConfigPath = Join-path $PSScriptRoot "class_ps_resources.dsc.yaml" + $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" } AfterAll { $env:PSModulePath = $OldPSModulePath @@ -15,30 +18,43 @@ Describe 'PowerShell adapter resource tests' { It 'Get works on config with class-based and script-based resources' -Skip:(!$IsWindows){ - $r = Get-Content -Raw $configPath | dsc config get + $r = Get-Content -Raw $pwshConfigPath | dsc config get $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results[0].result.actualState[0].PublishLocation | Should -BeExactly 'https://www.powershellgallery.com/api/v2/package/' - $res.results[0].result.actualState[1].Prop1 | Should -BeExactly 'ValueForProp1' + $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 = 'c:\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" + } + + <# It 'Test works on config with class-based and script-based resources' -Skip:(!$IsWindows){ - $r = Get-Content -Raw $configPath | dsc config test + $r = Get-Content -Raw $pwshConfigPath | dsc config test $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results[0].result.actualState[0] | Should -Not -BeNull - $res.results[0].result.actualState[1] | Should -Not -BeNull + $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){ - $r = Get-Content -Raw $configPath | dsc config set + $r = Get-Content -Raw $pwshConfigPath | dsc config set $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results.result.afterState[0].RebootRequired | Should -Not -BeNull - $res.results.result.afterState[1].RebootRequired | Should -Not -BeNull + $res.results.result.afterState.result[0].RebootRequired | Should -Not -BeNull + $res.results.result.afterState.result[1].RebootRequired | Should -Not -BeNull } + It 'Export works on config with class-based resources' -Skip:(!$IsWindows){ @@ -62,6 +78,8 @@ Describe 'PowerShell adapter resource tests' { $res.resources[0].properties.Prop1 | Should -Be "Property of object1" } + #> + It 'Custom psmodulepath in config works' -Skip:(!$IsWindows){ $OldPSModulePath = $env:PSModulePath @@ -80,6 +98,7 @@ Describe 'PowerShell adapter resource tests' { - name: Class-resource Info type: PSTestModule/TestClassResource "@ + <# $out = $yaml | dsc config export $LASTEXITCODE | Should -Be 0 $res = $out | ConvertFrom-Json @@ -88,6 +107,7 @@ Describe 'PowerShell adapter resource tests' { $res.resources.count | Should -Be 5 $res.resources[0].properties.Name | Should -Be "Object1" $res.resources[0].properties.Prop1 | Should -Be "Property of object1" + #> } finally { Rename-Item -Path "$PSScriptRoot/_PSTestModule" -NewName "PSTestModule" @@ -105,7 +125,7 @@ Describe 'PowerShell adapter resource tests' { properties: resources: - name: Class-resource Info - type: PSTestModule/TestClassResource + type: TestClassResource/TestClassResource properties: Name: "[envvar('DSC_CONFIG_ROOT')]" "@ @@ -116,8 +136,8 @@ Describe 'PowerShell adapter resource tests' { $out = dsc config get --path $config_path $LASTEXITCODE | Should -Be 0 $res = $out | ConvertFrom-Json - $res.results[0].result.actualState.Name | Should -Be $TestDrive - $res.results[0].result.actualState.Prop1 | Should -Be $TestDrive + $res.results.result.actualState.result.properties.Name | Should -Be $TestDrive + $res.results.result.actualState.result.properties.Prop1 | Should -Be $TestDrive } It 'DSC_CONFIG_ROOT env var does not exist when config is piped from stdin' -Skip:(!$IsWindows){ @@ -130,11 +150,12 @@ Describe 'PowerShell adapter resource tests' { properties: resources: - name: Class-resource Info - type: PSTestModule/TestClassResource + type: TestClassResource/TestClassResource properties: Name: "[envvar('DSC_CONFIG_ROOT')]" "@ - $null = $yaml | dsc config get + $testError = & {$yaml | dsc config get 2>&1} + $testError | Select-String 'Environment variable not found' -Quiet | Should -BeTrue $LASTEXITCODE | Should -Be 2 } } diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index 68e96bb5..ae311395 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -4,6 +4,9 @@ Describe 'PowerShell adapter resource tests' { BeforeAll { + if ($isWindows) { + winrm quickconfig -quiet + } $OldPSModulePath = $env:PSModulePath $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot } @@ -16,61 +19,90 @@ Describe 'PowerShell adapter resource tests' { $r = dsc resource list * -a *PowerShell* $LASTEXITCODE | Should -Be 0 $resources = $r | ConvertFrom-Json - ($resources | ? {$_.Type -eq 'PSTestModule/TestClassResource'}).Count | Should -Be 1 + ($resources | ? {$_.Type -eq 'TestClassResource/TestClassResource'}).Count | Should -Be 1 ($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 = 'c:\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 = 'c:\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 PSTestModule/TestClassResource + $r = "{'Name':'TestClassResource1', 'Type':'TestClassResource/TestClassResource'}" | dsc resource get -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.Prop1 | Should -BeExactly 'ValueForProp1' + $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 + $r = "{'Name':'TestPSRepository1','Type':'PSTestModule/TestPSRepository'}" | dsc resource get -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.PublishLocation | Should -BeExactly 'https://www.powershellgallery.com/api/v2/package/' + $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 PSTestModule/TestClassResource + $r = "{'Name':'TestClassResource1','Type':'TestClassResource/TestClassResource'}" | dsc resource get -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.EnumProp | Should -BeExactly 'Expected' + $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 + $r = "{'Name':'TestPSRepository1','Type':'PSTestModule/TestPSRepository'}" | dsc resource get -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.Ensure | Should -BeExactly 'Present' + $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 PSTestModule/TestClassResource + $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1','Type':'TestClassResource/TestClassResource'}" | dsc resource test -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.InDesiredState | Should -Be $True + $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 + $r = "{'Name':'TestPSRepository1','PackageManagementProvider':'NuGet','Type':'PSTestModule/TestPSRepository'}" | dsc resource test -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.InDesiredState | Should -Be $True + $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 PSTestModule/TestClassResource + $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1','Type':'TestClassResource/TestClassResource'}" | dsc resource set -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json $res.afterState.RebootRequired | Should -Not -BeNull @@ -78,7 +110,7 @@ Describe 'PowerShell adapter resource tests' { It 'Set works on script-based resource' -Skip:(!$IsWindows){ - $r = "{'Name':'TestPSRepository1'}" | dsc resource set -r PSTestModule/TestPSRepository + $r = "{'Name':'TestPSRepository1','Type':'PSTestModule/TestPSRepository'}" | dsc resource set -r 'Microsoft.Dsc/PowerShell' $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json $res.afterState.RebootRequired | Should -Not -BeNull @@ -86,21 +118,22 @@ Describe 'PowerShell adapter resource tests' { It 'Export works on PS class-based resource' -Skip:(!$IsWindows){ - $r = dsc resource export -r PSTestModule/TestClassResource + $r = dsc resource export -r TestClassResource/TestClassResource $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json $res.resources.count | Should -Be 5 - $res.resources[0].type | Should -Be "PSTestModule/TestClassResource" + $res.resources[0].type | Should -Be "TestClassResource/TestClassResource" $res.resources[0].properties.Name | Should -Be "Object1" $res.resources[0].properties.Prop1 | Should -Be "Property of object1" } It 'Get --all works on PS class-based resource' -Skip:(!$IsWindows){ - $r = dsc resource get --all -r PSTestModule/TestClassResource + $r = dsc resource get --all -r TestClassResource/TestClassResource $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json $res.count | Should -Be 5 $res | % {$_.actualState | Should -Not -BeNullOrEmpty} } + #> } diff --git a/powershell-adapter/Tests/winps_reference_resource.dsc.yaml b/powershell-adapter/Tests/winps_reference_resource.dsc.yaml new file mode 100644 index 00000000..d9c5e433 --- /dev/null +++ b/powershell-adapter/Tests/winps_reference_resource.dsc.yaml @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json +resources: +- name: Copy contents from one file to another + type: Microsoft.Windows/WindowsPowerShell + properties: + resources: + - name: From + type: PSDesiredStateConfiguration/File + properties: + DestinationPath: $env:TEMP\testFrom.txt + - name: To + type: PSDesiredStateConfiguration/File + properties: + DestinationPath: $env:TEMP\testTo.txt + Contents: "[reference(resourceId('PSDesiredStateConfiguration/File','From')).contents)]" + output: Contents + dependsOn: + - "[resourceId('PSDesiredStateConfiguration/File','From')]" + diff --git a/powershell-adapter/Tests/winps_resource.dsc.yaml b/powershell-adapter/Tests/winps_resource.dsc.yaml index c4e04473..8d123aa0 100644 --- a/powershell-adapter/Tests/winps_resource.dsc.yaml +++ b/powershell-adapter/Tests/winps_resource.dsc.yaml @@ -1,11 +1,14 @@ -# Example configuration mixing native app resources with classic PS resources +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json resources: - name: Get info from classic DSC resources type: Microsoft.Windows/WindowsPowerShell properties: resources: - - name: Get Info - type: PSDesiredStateConfiguration/MSFT_ServiceResource + - name: File + type: PSDesiredStateConfiguration/File properties: - Name: sshd + DestinationPath: c:\test.txt + Contents: 'Hello, World!' diff --git a/powershell-adapter/copy_files.txt b/powershell-adapter/copy_files.txt index 141bc529..b14c6fea 100644 --- a/powershell-adapter/copy_files.txt +++ b/powershell-adapter/copy_files.txt @@ -1 +1,8 @@ powershell.resource.ps1 +./psDscAdapter/psDscAdapter.psd1 +./psDscAdapter/psDscAdapter.psm1 +./psDscAdapter/Configuration/BaseRegistration/BaseResource.Schema.mof +./psDscAdapter/Configuration/BaseRegistration/MSFT_MetaConfigurationExtensionClasses.Schema.mof +./psDscAdapter/Configuration/BaseRegistration/en-us/BaseResource.Schema.mfl +./psDscAdapter/Configuration/BaseRegistration/en-us/MSFT_MetaConfigurationExtensionClasses.Schema.mfl +./psDscAdapter/helpers/DscResourceInfo.psm1 \ No newline at end of file diff --git a/powershell-adapter/powershell.resource.ps1 b/powershell-adapter/powershell.resource.ps1 index ca3a7b90..d1d26743 100644 --- a/powershell-adapter/powershell.resource.ps1 +++ b/powershell-adapter/powershell.resource.ps1 @@ -1,427 +1,178 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - [CmdletBinding()] param( - [ValidateSet('List','Get','Set','Test','Export','Validate')] - $Operation = 'List', - [Switch] - $WinPS = $false, - [Parameter(ValueFromPipeline)] - $stdinput + [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate.')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate')] + [string]$Operation, + [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] + [string]$jsonInput = '@{}' ) -$ProgressPreference = 'Ignore' -$WarningPreference = 'Ignore' -$VerbosePreference = 'Ignore' -$script:ResourceCache = @{} - -function RefreshCache -{ - $script:ResourceCache = @{} - - $DscResources = Get-DscResource - - foreach ($r in $DscResources) - { - $moduleName = ""; - if ($r.ModuleName) { $moduleName = $r.ModuleName } - elseif ($r.ParentPath) { $moduleName = Split-Path $r.ParentPath | Split-Path | Split-Path -Leaf } - - $fullResourceTypeName = "$moduleName/$($r.ResourceType)" - $script:ResourceCache[$fullResourceTypeName] = $r - } -} - -function IsConfiguration($obj) { - if ($null -ne $obj.metadata -and $null -ne $obj.metadata.'Microsoft.DSC' -and $obj.metadata.'Microsoft.DSC'.context -eq 'Configuration') { - return $true - } +if ('Validate' -ne $Operation) { + # write $jsonInput to STDERR for debugging + $trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) - return $false -} + # load private functions of psDscAdapter stub module + $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter/psDscAdapter.psd1" -Force -PassThru -if (($PSVersionTable.PSVersion.Major -eq 7) -and ($PSVersionTable.PSVersion.Minor -eq 4) ` - -and ($PSVersionTable.PSVersion.PreReleaseLabel.StartsWith("preview"))) -{ - throw "PowerShell 7.4-previews are not supported by PowerShell adapter resource; please use PS 7.4.0-rc.1 or newer." + # initialize OUTPUT as array + $result = [System.Collections.Generic.List[Object]]::new() } -$inputobj_pscustomobj = $null -if ($stdinput) -{ - $inputobj_pscustomobj = $stdinput | ConvertFrom-Json - $new_psmodulepath = $inputobj_pscustomobj.psmodulepath - if ($new_psmodulepath) - { - $env:PSModulePath = $ExecutionContext.InvokeCommand.ExpandString($new_psmodulepath) - } -} - -$DscModule = Get-Module -Name PSDesiredStateConfiguration -ListAvailable | - Sort-Object -Property Version -Descending | - Select-Object -First 1 - -if ($null -eq $DscModule) -{ - Write-Error "Could not find and import the PSDesiredStateConfiguration module." - # Missing module is okay for listing resources - if ($Operation -eq 'List') { exit 0 } - - exit 1 -} - -Import-Module $DscModule -DisableNameChecking - -# Adding some debug info to STDERR -$m = gmo PSDesiredStateConfiguration -$trace = @{"Debug"="PSVersion="+$PSVersionTable.PSVersion.ToString()} | ConvertTo-Json -Compress -$host.ui.WriteErrorLine($trace) -$trace = @{"Debug"="PSPath="+$PSHome} | ConvertTo-Json -Compress -$host.ui.WriteErrorLine($trace) -$trace = @{"Debug"="ModuleVersion="+$m.Version.ToString()} | ConvertTo-Json -Compress -$host.ui.WriteErrorLine($trace) -$trace = @{"Debug"="ModulePath="+$m.Path} | ConvertTo-Json -Compress -$host.ui.WriteErrorLine($trace) -$trace = @{"Debug"="PSModulePath="+$env:PSModulePath} | ConvertTo-Json -Compress -$host.ui.WriteErrorLine($trace) - -if ($Operation -eq 'List') -{ - $DscResources= Get-DscResource - - foreach ($r in $DscResources) - { - if ($r.ImplementedAs -eq "Binary") - { - continue - } - - $version_string = ""; - if ($r.Version) { $version_string = $r.Version.ToString() } - $author_string = ""; - if ($r.author) { $author_string = $r.CompanyName.ToString() } - $moduleName = ""; - if ($r.ModuleName) { $moduleName = $r.ModuleName } - elseif ($r.ParentPath) { $moduleName = Split-Path $r.ParentPath | Split-Path | Split-Path -Leaf } - - $propertyList = @() - foreach ($p in $r.Properties) - { - if ($p.Name) - { - $propertyList += $p.Name - } - } - - $fullResourceTypeName = "$moduleName/$($r.ResourceType)" - $script:ResourceCache[$fullResourceTypeName] = $r - if ($WinPS) {$requiresString = "Microsoft.Windows/WindowsPowerShell"} else {$requiresString = "Microsoft.DSC/PowerShell"} - - $t = [Type]$r.ResourceType - $exportMethod = $t.GetMethod('Export') - - $capabilities = @('Get', 'Set', 'Test') - if ($null -ne $exportMethod) { - $capabilities += 'Export' - } - - $z = [pscustomobject]@{ - type = $fullResourceTypeName; - kind = 'Resource'; - version = $version_string; - capabilities = $capabilities; - path = $r.Path; - directory = $r.ParentPath; - implementedAs = $r.ImplementationDetail; - author = $author_string; - properties = $propertyList; - requireAdapter = $requiresString - } - - $z | ConvertTo-Json -Compress - } -} -elseif ($Operation -eq 'Get') -{ - $result = @() - - RefreshCache - - if (IsConfiguration $inputobj_pscustomobj) # we are processing a config batch - { - foreach($r in $inputobj_pscustomobj.resources) - { - #Write-Output $r.type - $cachedResourceInfo = $script:ResourceCache[$r.type] - if ($cachedResourceInfo) - { - $inputht = @{} - $typeparts = $r.type -split "/" - $ModuleName = $typeparts[0] - $ResourceTypeName = $typeparts[1] - $r.properties.psobject.properties | %{ $inputht[$_.Name] = $_.Value } - $e = $null - $op_result = Invoke-DscResource -Method Get -ModuleName $ModuleName -Name $ResourceTypeName -Property $inputht -ErrorVariable e - if ($e) - { - # By this point Invoke-DscResource already wrote error message to stderr stream, - # so we just need to signal error to the caller by non-zero exit code. - exit 1 +# process the operation requested to the script +switch ($Operation) { + 'List' { + $dscResourceCache = Invoke-DscCacheRefresh + + # cache was refreshed on script load + foreach ($dscResource in $dscResourceCache) { + + # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo + $DscResourceInfo = $dscResource.DscResourceInfo + + # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test + if ($DscResourceInfo.ModuleName) { + $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 + if ($module.PrivateData.PSData.DscCapabilities) { + $capabilities = $module.PrivateData.PSData.DscCapabilities } - $result += $op_result - } - else - { - $errmsg = "Can not find type " + $r.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg - exit 1 - } - } - } - else # we are processing an individual resource call - { - $cachedResourceInfo = $script:ResourceCache[$inputobj_pscustomobj.type] - if ($cachedResourceInfo) - { - $inputht = @{} - $ResourceTypeName = ($inputobj_pscustomobj.type -split "/")[1] - $inputobj_pscustomobj.psobject.properties | %{ - if ($_.Name -ne "type") - { - $inputht[$_.Name] = $_.Value + else { + $capabilities = @('Get', 'Set', 'Test') } } - $e = $null - $op_result = Invoke-DscResource -Method Get -Name $ResourceTypeName -Property $inputht -ErrorVariable e -WarningAction SilentlyContinue - if ($e) - { - # By this point Invoke-DscResource already wrote error message to stderr stream, - # so we just need to signal error to the caller by non-zero exit code. - exit 1 - } - $result = $op_result - } - else - { - $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg - exit 1 - } - } - $result | ConvertTo-Json -EnumsAsStrings -} -elseif ($Operation -eq 'Set') -{ - $result = @() - - RefreshCache - - if (IsConfiguration $inputobj_pscustomobj) # we are processing a config batch - { - foreach($r in $inputobj_pscustomobj.resources) - { - #Write-Output $r.type - $cachedResourceInfo = $script:ResourceCache[$r.type] - if ($cachedResourceInfo) - { - $inputht = @{} - $ResourceTypeName = ($r.type -split "/")[1] - $r.properties.psobject.properties | %{ $inputht[$_.Name] = $_.Value } - $e = $null - $op_result = Invoke-DscResource -Method Set -Name $ResourceTypeName -Property $inputht -ErrorVariable e - if ($e) - { - # By this point Invoke-DscResource already wrote error message to stderr stream, - # so we just need to signal error to the caller by non-zero exit code. - exit 1 - } - $result += $op_result + # this text comes directly from the resource manifest for v3 native resources + if ($DscResourceInfo.Description) { + $description = $DscResourceInfo.Description } - else - { - $errmsg = "Can not find type " + $r.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg - exit 1 - } - } - } - else # we are processing an individual resource call - { - $cachedResourceInfo = $script:ResourceCache[$inputobj_pscustomobj.type] - if ($cachedResourceInfo) - { - $inputht = @{} - $ResourceTypeName = ($inputobj_pscustomobj.type -split "/")[1] - $inputobj_pscustomobj.psobject.properties | %{ - if ($_.Name -ne "type") - { - $inputht[$_.Name] = $_.Value - } + elseif ($module.Description) { + # some modules have long multi-line descriptions. to avoid issue, use only the first line. + $description = $module.Description.split("`r`n")[0] } - $e = $null - $op_result = Invoke-DscResource -Method Set -Name $ResourceTypeName -Property $inputht -ErrorVariable e - if ($e) - { - # By this point Invoke-DscResource already wrote error message to stderr stream, - # so we just need to signal error to the caller by non-zero exit code. - exit 1 + else { + $description = '' } - $result = $op_result - } - else - { - $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg - exit 1 - } - } - $result | ConvertTo-Json -} -elseif ($Operation -eq 'Test') -{ - $result = @() - - RefreshCache - - if (IsConfiguration $inputobj_pscustomobj) # we are processing a config batch - { - foreach($r in $inputobj_pscustomobj.resources) - { - #Write-Output $r.type - $cachedResourceInfo = $script:ResourceCache[$r.type] - if ($cachedResourceInfo) - { - $inputht = @{} - $ResourceTypeName = ($r.type -split "/")[1] - $r.properties.psobject.properties | %{ $inputht[$_.Name] = $_.Value } - $e = $null - $op_result = Invoke-DscResource -Method Test -Name $ResourceTypeName -Property $inputht -ErrorVariable e - if ($e) - { - # By this point Invoke-DscResource already wrote error message to stderr stream, - # so we just need to signal error to the caller by non-zero exit code. - exit 1 - } - $result += $op_result + # match adapter to version of powershell + if ($PSVersionTable.PSVersion.Major -le 5) { + $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' } - else - { - $errmsg = "Can not find type " + $r.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg - exit 1 + else { + $requireAdapter = 'Microsoft.DSC/PowerShell' } + + # OUTPUT dsc is expecting the following properties + [resourceOutput]@{ + type = $dscResource.Type + kind = 'Resource' + version = $DscResourceInfo.version.ToString() + capabilities = $capabilities + path = $DscResourceInfo.Path + directory = $DscResourceInfo.ParentPath + implementedAs = $DscResourceInfo.ImplementationDetail + author = $DscResourceInfo.CompanyName + properties = $DscResourceInfo.Properties.Name + requireAdapter = $requireAdapter + description = $description + } | ConvertTo-Json -Compress } } - else # we are processing an individual resource call - { - $cachedResourceInfo = $script:ResourceCache[$inputobj_pscustomobj.type] - if ($cachedResourceInfo) - { - $inputht = @{} - $ResourceTypeName = ($inputobj_pscustomobj.type -split "/")[1] - $inputobj_pscustomobj.psobject.properties | %{ - if ($_.Name -ne "type") - { - $inputht[$_.Name] = $_.Value - } - } - $e = $null - $op_result = Invoke-DscResource -Method Test -Name $ResourceTypeName -Property $inputht -ErrorVariable e - if ($e) - { - # By this point Invoke-DscResource already wrote error message to stderr stream, - # so we just need to signal error to the caller by non-zero exit code. - exit 1 - } - $result = $op_result - } - else - { - $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg + 'Get' { + $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) + if ($null -eq $desiredState) { + $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) exit 1 } - } - $result | ConvertTo-Json -} -elseif ($Operation -eq 'Export') -{ - $result = @() - - RefreshCache - - if (IsConfiguration $inputobj_pscustomobj) # we are processing a config batch - { - foreach($r in $inputobj_pscustomobj.resources) - { - $cachedResourceInfo = $script:ResourceCache[$r.type] - if ($cachedResourceInfo) - { - $path = $cachedResourceInfo.Path # for class-based resources - this is path to psd1 of their defining module - - $typeparts = $r.type -split "/" - $ResourceTypeName = $typeparts[1] + # only need to cache the resources that are used + $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } + if ($null -eq $dscResourceModules) { + $trace = @{'Debug' = 'ERROR: Could not get list of DSC resource types from provided JSON.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } - $scriptBody = "using module '$path'" - $script = [ScriptBlock]::Create($scriptBody) - . $script + $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules + if ($dscResourceCache.count -lt $dscResourceModules.count) { + $trace = @{'Debug' = 'ERROR: DSC resource module not found.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } - $t = [Type]$ResourceTypeName - $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null,$null) - foreach ($instance in $resultArray) - { - $instance | ConvertTo-Json -Compress | Write-Output - } - } - else - { - $errmsg = "Can not find type " + $r.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg + foreach ($ds in $desiredState) { + # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState + $actualState = $psDscAdapter.invoke( { param($ds, $dscResourceCache) Get-ActualState -DesiredState $ds -dscResourceCache $dscResourceCache }, $ds, $dscResourceCache) + if ($null -eq $actualState) { + $trace = @{'Debug' = 'ERROR: Incomplete GET for resource ' + $ds.Name } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) exit 1 } + $result += $actualState } + + # OUTPUT json to stderr for debug, and to stdout + $result = @{ result = $result } | ConvertTo-Json -Depth 10 -Compress + $trace = @{'Debug' = 'jsonOutput=' + $result } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + return $result } - else # we are processing an individual resource call - { - $cachedResourceInfo = $script:ResourceCache[$inputobj_pscustomobj.type] - if ($cachedResourceInfo) - { - $path = $cachedResourceInfo.Path # for class-based resources - this is path to psd1 of their defining module - - $typeparts = $inputobj_pscustomobj.type -split "/" - $ResourceTypeName = $typeparts[1] - - $scriptBody = "using module '$path'" - $script = [ScriptBlock]::Create($scriptBody) - . $script - - $t = [Type]$ResourceTypeName - $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null,$null) - foreach ($instance in $resultArray) - { - $instance | ConvertTo-Json -Compress | Write-Output - } - } - else - { - $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-DscResource returns this resource type" - Write-Error $errmsg - exit 1 - } + 'Set' { + throw 'SET not implemented' + + # OUTPUT + $result += @{} + @{ result = $result } | ConvertTo-Json -Depth 10 -Compress + } + 'Test' { + throw 'TEST not implemented' + + # OUTPUT + $result += @{} + @{ result = $result } | ConvertTo-Json -Depth 10 -Compress + } + 'Export' { + throw 'EXPORT not implemented' + + # OUTPUT + $result += @{} + @{ result = $result } | ConvertTo-Json -Depth 10 -Compress + } + 'Validate' { + # VALIDATE not implemented + + # OUTPUT + @{ valid = $true } | ConvertTo-Json + } + Default { + Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' } } -elseif ($Operation -eq 'Validate') -{ - # TODO: this is placeholder - @{ valid = $true } | ConvertTo-Json + +# output format for resource list +class resourceOutput { + [string] $type + [string] $kind + [string] $version + [string[]] $capabilities + [string] $path + [string] $directory + [string] $implementedAs + [string] $author + [string[]] $properties + [string] $requireAdapter + [string] $description } -else -{ - "ERROR: Unsupported operation requested from powershell.resource.ps1" -} \ No newline at end of file + +# Adding some debug info to STDERR +$trace = @{'Debug' = 'PSVersion=' + $PSVersionTable.PSVersion.ToString() } | ConvertTo-Json -Compress +$host.ui.WriteErrorLine($trace) +$trace = @{'Debug' = 'PSPath=' + $PSHome } | ConvertTo-Json -Compress +$host.ui.WriteErrorLine($trace) +$m = Get-Command 'Get-DscResource' +$trace = @{'Debug' = 'Module=' + $m.Source.ToString() } | ConvertTo-Json -Compress +$host.ui.WriteErrorLine($trace) +$trace = @{'Debug' = 'PSModulePath=' + $env:PSModulePath } | ConvertTo-Json -Compress +$host.ui.WriteErrorLine($trace) \ No newline at end of file diff --git a/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/BaseResource.schema.mof b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/BaseResource.schema.mof new file mode 100644 index 00000000..0b35f587 --- /dev/null +++ b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/BaseResource.schema.mof @@ -0,0 +1,90 @@ +[ClassVersion("1.0.0")] +class MSFT_Credential +{ + [MaxLen ( 256 )] + string UserName; + string Password; +}; + +[Abstract, ClassVersion("1.0.0")] +class OMI_BaseResource +{ + [required] string ResourceId; + [write] string SourceInfo; + [write] string DependsOn[]; + [required] string ModuleName; + [required] string ModuleVersion; + [write] string ConfigurationName; + [write, EmbeddedInstance("MSFT_Credential")] string PsDscRunAsCredential; +}; + +[Abstract, ClassVersion("1.0.0")] +class MSFT_KeyValuePair +{ + [Key] + string Key; + + [write] + string Value; +}; + +[Abstract, ClassVersion("1.0.0")] +class MSFT_BaseConfigurationProviderRegistration +{ + [key] String ClassName; + String DSCEngineCompatVersion; + String DSCModuleVersion; +}; + +[ClassVersion("1.0.0")] +class MSFT_CimConfigurationProviderRegistration : MSFT_BaseConfigurationProviderRegistration +{ + String Namespace; +}; + +[ClassVersion("1.0.0")] +class MSFT_PSConfigurationProviderRegistration : MSFT_BaseConfigurationProviderRegistration +{ + String ModuleName; + String ProviderPath; + String ModulePath; +}; + +[ClassVersion("1.0.0")] +class OMI_ConfigurationDocument +{ + String Version; + String Author; + String Copyright; + String HelpInfoUri; + String ContentType; + String GenerationDate; + String GenerationHost; + String Name; + String MinimumCompatibleVersion; + String CompatibleVersionAdditionalProperties[]; + boolean UseCms; +}; + +[Abstract,ClassVersion("1.0.0")] +class OMI_MetaConfigurationResource +{ + [required] string ResourceId; + [write] string SourceInfo; +}; + +[Abstract,ClassVersion("1.0.0")] +class OMI_ResourceModuleManager : OMI_MetaConfigurationResource +{ +}; + +[Abstract,ClassVersion("1.0.0")] +class OMI_ConfigurationDownloadManager : OMI_MetaConfigurationResource +{ +}; + + +[Abstract,ClassVersion("1.0.0")] +class OMI_ReportManager : OMI_MetaConfigurationResource +{ +}; diff --git a/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/MSFT_MetaConfigurationExtensionClasses.schema.mof b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/MSFT_MetaConfigurationExtensionClasses.schema.mof new file mode 100644 index 00000000..1706d69e --- /dev/null +++ b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/MSFT_MetaConfigurationExtensionClasses.schema.mof @@ -0,0 +1,96 @@ +[ClassVersion("1.0.0"), FriendlyName("ConfigurationRepositoryWeb")] +class MSFT_WebDownloadManager : OMI_ConfigurationDownloadManager +{ + [Key] string ServerURL; + string CertificateID; + boolean AllowUnsecureConnection; + string RegistrationKey; + string ConfigurationNames[]; + string ProxyURL; + [EmbeddedInstance("MSFT_Credential")] string ProxyCredential; +}; + +[ClassVersion("1.0.0"), FriendlyName("ConfigurationRepositoryShare")] +class MSFT_FileDownloadManager : OMI_ConfigurationDownloadManager +{ + [Key] string SourcePath; + [EmbeddedInstance("MSFT_Credential")] string Credential; +}; + +[ClassVersion("1.0.0"), FriendlyName("ResourceRepositoryWeb")] +class MSFT_WebResourceManager : OMI_ResourceModuleManager +{ + [Key] string ServerURL; + string CertificateID; + boolean AllowUnsecureConnection; + string RegistrationKey; + string ProxyURL; + [EmbeddedInstance("MSFT_Credential")] string ProxyCredential; +}; + +[ClassVersion("1.0.0"), FriendlyName("ResourceRepositoryShare")] +class MSFT_FileResourceManager : OMI_ResourceModuleManager +{ + [Key] string SourcePath; + [EmbeddedInstance("MSFT_Credential")] string Credential; +}; + +[ClassVersion("1.0.0"), FriendlyName("ReportServerWeb")] +class MSFT_WebReportManager : OMI_ReportManager +{ + [Key] string ServerURL; + string CertificateID; + boolean AllowUnsecureConnection; + string RegistrationKey; + string ProxyURL; + [EmbeddedInstance("MSFT_Credential")] string ProxyCredential; +}; + +[ClassVersion("1.0.0"), FriendlyName("PartialConfiguration")] +class MSFT_PartialConfiguration : OMI_MetaConfigurationResource +{ + [Write] String Description; + [Write] String ExclusiveResources[]; + [Write] String ConfigurationSource[]; + [Write] String ResourceModuleSource[]; + [Write] String DependsOn[]; + [ValueMap{"Push", "Pull", "Disabled"},Values{"Push", "Pull", "Disabled"}] string RefreshMode; +}; + +[ClassVersion("1.0.0"), FriendlyName("SignatureValidation")] +class MSFT_SignatureValidation : OMI_MetaConfigurationResource +{ + [Write]string TrustedStorePath; + [ValueMap{"Configuration","Module"},Values{"Configuration","Module"}]string SignedItemType[]; +}; + +[ClassVersion("2.0.0"),FriendlyName("Settings")] +class MSFT_DSCMetaConfigurationV2 +{ + uint32 ConfigurationModeFrequencyMins; + boolean RebootNodeIfNeeded; + [ValueMap{"ApplyOnly", "ApplyAndMonitor", "ApplyAndAutoCorrect", "MonitorOnly"},Values{"ApplyOnly", "ApplyAndMonitor", "ApplyAndAutoCorrect", "MonitorOnly"}] string ConfigurationMode; + [ValueMap {"ContinueConfiguration","StopConfiguration"}, Values {"ContinueConfiguration","StopConfiguration"}] + string ActionAfterReboot; + [ValueMap{"Push", "Pull", "Disabled"},Values{"Push", "Pull", "Disabled"}] string RefreshMode; + string CertificateID; + string ConfigurationID; + uint32 RefreshFrequencyMins; + boolean AllowModuleOverwrite; + [ValueMap {"None","ForceModuleImport", "All", "ResourceScriptBreakAll", "ResourceScriptBreakpoint"}, Values {"None","ForceModuleImport", "All", "ResourceScriptBreakAll", "ResourceScriptBreakpoint"}] + string DebugMode[]; + [Read] string LCMVersion; + [Read] string LCMCompatibleVersions[]; + [Read,ValueMap{"Idle", "Busy", "PendingReboot", "PendingConfiguration"},Values{"Idle", "Busy", "PendingReboot", "PendingConfiguration"}] string LCMState; + [Read] string LCMStateDetail; + [EmbeddedInstance("OMI_ConfigurationDownloadManager")] string ConfigurationDownloadManagers[]; + [EmbeddedInstance("OMI_ResourceModuleManager")] string ResourceModuleManagers[]; + [EmbeddedInstance("OMI_ReportManager")] string ReportManagers[]; + [EmbeddedInstance("MSFT_PartialConfiguration")] string PartialConfigurations[]; + uint32 StatusRetentionTimeInDays; + [Read] string AgentId; + string SignatureValidationPolicy; + [EmbeddedInstance("MSFT_SignatureValidation")] + string SignatureValidations[]; + uint32 MaximumDownloadSizeMB; +}; diff --git a/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/en-US/BaseResource.Schema.mfl b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/en-US/BaseResource.Schema.mfl new file mode 100644 index 00000000..677d3bd5 --- /dev/null +++ b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/en-US/BaseResource.Schema.mfl @@ -0,0 +1,87 @@ +#pragma namespace("\\\\.\\root\\default") +instance of __namespace{ name="ms_409";}; +#pragma namespace("\\\\.\\root\\default\\ms_409") + +[Description("Base schema for all configuration providers that will be imported by powershell extension.") : Amended,AMENDMENT, LOCALE("ms_409")] +class OMI_BaseResource +{ + [Description("Unique Id for a resource instance.") : Amended] string ResourceId; + [Description("Source Info to correlate it back to powershell configuration script.") : Amended] string SourceInfo; + [Description("List of resources this resource depends on.") : Amended] string DependsOn[]; + [Description("Name of the module that supports this resource.")] string ModuleName; + [Description("Version of the module that supports this resource.")] string ModuleVersion; + [Description("Name of the Partial Configuration that this resource belongs to.")] string ConfiguratioName; + [Description("Credentials under which the resource runs. ")] string PsDscRunAsCredential; +}; + +[Description("This class represents a key-value pair.") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_KeyValuePair +{ + +}; + +[Description("Base schema for configuration provider registration that maps a provider to some configuration engine related information.") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_BaseConfigurationProviderRegistration +{ +}; + +[Description("Base schema for configuration provider registration that maps a cim provider to namespace.") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_CimConfigurationProviderRegistration : MSFT_BaseConfigurationProviderRegistration +{ +}; + +[Description("Base schema for configuration provider registration that maps a powershell provider to its module.") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_PSConfigurationProviderRegistration : MSFT_BaseConfigurationProviderRegistration +{ +}; + +[Description("Base schema for configuration document information.") : Amended,AMENDMENT, LOCALE("ms_409")] +class OMI_ConfigurationDocument +{ + [Description("Configuration document version information, configuration engine can use to log.") : Amended] String Version; + [Description("Configuration document Author information.") : Amended] String Author; + [Description("Configuration document Copyright information.") : Amended] String Copyright; + [Description("Configuration document Help URI.") : Amended] String HelpInfoUri; + [Description("Configuration document Content Type. Only PasswordEncrypted and NotEncrypted are supported. Default value is NotEncrypted.") : Amended] String ContentType; + [Description("Configuration document generation date.") : Amended] String GenerationDate; + [Description("Configuration document generation host.") : Amended] String GenerationHost; + [Description("Configuration document name.") : Amended] String Name; + [Description("Configuration document minimum version requirement for compatibility with the target DSC version.") : Amended] String MinimumCompatibleVersion; + [Description("Additional properties (if any) that are required by the version of this document over 1.0.0 for version compatibility.") : Amended] String CompatibleVersionAdditionalProperties[]; + [Description("Configuration document is encrypted using Cryptographic Message Syntax format.") : Amended] boolean UseCms; +}; + + +[Description ("Credential to use for DSC configuration providers." ) : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_Credential +{ + [Description("UserName is the name of the user for an authorization service to map to an identity." ) : Amended] string UserName; + [Description("UserPassword property may contain a password used to access resources." ) : Amended] string Password; +}; + + +[Description("Base schema for all Metaconfiguration resources .") : Amended,AMENDMENT, LOCALE("ms_409")] +class OMI_MetaConfigurationResource +{ +}; + +[Description("Class defining the structure of resource module managers") : Amended,AMENDMENT, LOCALE("MS_409")] +class OMI_ResourceModuleManager : OMI_MetaConfigurationResource +{ + [Description("Name of the resource module Manager.") : Amended] string Name; + [Description("Priority of the resource module manager.") : Amended] uint32 Priority; +}; + +[Description("Class defining the configuration download manager") : Amended,AMENDMENT, LOCALE("MS_409")] +class OMI_ConfigurationDownloadManager : OMI_MetaConfigurationResource +{ + [Description("Name of the configuration download Manager.") : Amended] string Name; +}; + + +[Description("Class defining a report manager") : Amended,AMENDMENT, LOCALE("MS_409")] +class OMI_ReportManager : OMI_MetaConfigurationResource +{ + [Description("Name of the report manager") : Amended] string Name; + [Description("Custom data that is specific to Report Manager.") : Amended] string CustomData[]; +}; diff --git a/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/en-US/MSFT_MetaConfigurationExtensionClasses.Schema.mfl b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/en-US/MSFT_MetaConfigurationExtensionClasses.Schema.mfl new file mode 100644 index 00000000..9b22f93b --- /dev/null +++ b/powershell-adapter/psDscAdapter/Configuration/BaseRegistration/en-US/MSFT_MetaConfigurationExtensionClasses.Schema.mfl @@ -0,0 +1,89 @@ +#pragma namespace("\\\\.\\root\\default") +instance of __namespace{ name="ms_409";}; +#pragma namespace("\\\\.\\root\\default\\ms_409") + +[Description("Web download manager class inheriting from OMI_ConfigurationDownloadManager") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_WebDownloadManager : OMI_ConfigurationDownloadManager +{ + [Description("String URL of the download manager location") : Amended] string ServerURL; + [Description("The certificate ID used to locate the certificate.") : Amended] string CertificateID; + [Description("Specifies whether report manager can use unsecure connection over http.") : Amended] boolean AllowUnsecureConnection; + [Description("Registration Key with which to register with the Pull Server") : Amended ToSubclass] string RegistrationKey; + [Description("The set of configuration names with which to register with the Pull Server.") : Amended] string ConfigurationNames[]; + [Description("String URL of the proxy server") : Amended] string ProxyURL; + [Description("Credential to access the proxy server") : Amended] MSFT_Credential ProxyCredential; +}; + +[Description("File configuration download manager class inheriting from OMI_ConfigurationDownloadManager") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_FileDownloadManager : OMI_ConfigurationDownloadManager +{ + [Description("String UNC path of the download manager location") : Amended] string SourcePath; + [Description("Default credential to access the file location") : Amended] string Credential; +}; + + +[Description("Web resource module manager class inheriting from OMI_ResourceModuleManager") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_WebResourceManager : OMI_ResourceModuleManager +{ + [Description("String URL of the resource module manager location") : Amended] string ServerURL; + [Description("The certificate ID used to locate the certificate.") : Amended] string CertificateID; + [Description ("Boolean variable to allow unsecure connections" ) : Amended] boolean AllowUnsecureConnection; + [Description("Registration Key with which to register with the Resource Repository Web") : Amended ToSubclass] string RegistrationKey; + [Description("String URL of the proxy server") : Amended] string ProxyURL; + [Description("Credential to access the proxy server") : Amended] MSFT_Credential ProxyCredential; +}; + +[Description("File resource module manager class inheriting from OMI_ResourceModuleManager class") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_FileResourceManager : OMI_ResourceModuleManager +{ + [Description("String UNC path of the File resource manager") : Amended] string SourcePath; + [Description("Default credential to access resources.") : Amended] string Credential; +}; + +[Description("Web report manager class inheriting from OMI_ReportManager class") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_WebReportManager : OMI_ReportManager +{ + [Description("String URL of the report manager location") : Amended] string ServerURL; + [Description ("The certificate ID used to locate the certificate for secure connections." ) : Amended] string CertificateID; + [Description("Specifies whether report manager can use unsecure connection over http.") : Amended] boolean AllowUnsecureConnection; + [Description("Registration Key with which to register with the Reporting Server") : Amended ToSubclass] string RegistrationKey; + [Description("String URL of the proxy server") : Amended] string ProxyURL; + [Description("Credential to access the proxy server") : Amended] MSFT_Credential ProxyCredential; +}; + +[Description("This represents a Partial Configuration class.") : Amended,AMENDMENT, LOCALE("ms_409")] +class MSFT_PartialConfiguration : OMI_MetaConfigurationResource +{ + [Description("Description of the partial configuration") : Amended] String Description; + [Description("Defines the resources that are exclusive to this particular partial configuration") : Amended] String ExclusiveResources[]; + [Description("The configuration repository source that this partial configuration will use") : Amended] String ConfigurationSource; + [Description("A dependency variable indicating which partial configuration must be applied prior to this") : Amended] String DependsOn[]; + [Description("The refresh mode for the server. Valid values are Pull, Push and Disabled.") : Amended] string RefreshMode; +}; + +[Description("Local Configuration Manager settings.") : Amended,AMENDMENT, LOCALE("MS_409")] +class MSFT_DSCMetaConfigurationV2 +{ + [Description("The time interval between consecutive runs for reapplying the configuration to get to the desired state.") : Amended] uint32 ConfigurationModeFrequencyMins; + [Description("Reboot node if needed.") : Amended] boolean RebootNodeIfNeeded; + [Description("The configuration apply mode for the server.") : Amended] string ConfigurationMode; + [Description("The refresh mode for the server. Valid values are Pull, Push and Disabled.") : Amended] string RefreshMode; + [Description("The action after reboot of the server. Valid values are ContinueConfiguration andStopConfiguration.") : Amended] string ActionAfterReboot; + [Description("The configuration ID used to get the configuration from the pull server.") : Amended] string ConfigurationID; + [Description("The time interval between consecutive runs to get the action from the server.") : Amended] uint32 RefreshFrequencyMins; + [Description("Overwrite modules when downloading from Pull Server.") : Amended] boolean AllowModuleOverwrite; + [Description("Debug mode. Valid values are None, ForceModuleImport, ResourceScriptBreakAll, ResourceScriptBreakpoint or All") : Amended] string DebugMode[]; + [Description("Current version of local configuration manager.") : Amended] string LCMVersion; + [Description("Compatible versions of current local configuration manager.") : Amended] string LCMCompatibleVersions[]; + [Description("Current state of local configuration manager.") : Amended] string LCMState; + [Description("State detail of local configuration manager.") : Amended] string LCMStateDetail; + [Description("Array of configuration download manager objects that contain location information to download configurations") : Amended] string ConfigurationDownloadManagers[]; + [Description("Array of resource module managers pointing to a location to download missing DSCResources") : Amended] string ResourceModuleManagers[]; + [Description("Array of report managers pointing to a location that would help generate reports for DSC") : Amended] string ReportManagers[]; + [Description("Array of partial configurations that are specified to be applied") : Amended] string PartialConfigurations[]; + [Description("Number of days to retain configuration status history.") : Amended] uint32 StatusRetentionTimeInDays; + [Description("AgentId of the current Dsc Agent.") : Amended] string AgentId; + [Description("Current signature validation policy.") : Amended] string SignatureValidationPolicy; + [Description ("The signature validation options of the node.") : Amended] string SignatureValidations[]; + [Description ("The maximum module size in MB that can be downloaded.") : Amended] uint32 MaximumDownloadSizeMB; +}; diff --git a/powershell-adapter/psDscAdapter/helpers/DscResourceInfo.psm1 b/powershell-adapter/psDscAdapter/helpers/DscResourceInfo.psm1 new file mode 100644 index 00000000..d2b99885 --- /dev/null +++ b/powershell-adapter/psDscAdapter/helpers/DscResourceInfo.psm1 @@ -0,0 +1,182 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Set-StrictMode -Version latest +$ErrorActionPreference = 'Stop' + +$AddDscResourceInfoTypeScript = @" +//----------------------------------------------------------------------- +// +// Copyright (C) 2013 Microsoft Corporation +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System; +using System.Management.Automation; +using System.IO; + +namespace Microsoft.PowerShell.DesiredStateConfiguration +{ + /// + /// Enumerated values for DSC resource implementation type + /// + public enum ImplementedAsType + { + /// + /// DSC resource implementation type not known + /// + None = 0, + + /// + /// DSC resource is implemented using PowerShell module + /// + PowerShell = 1, + + /// + /// DSC resource is implemented using a CIM provider + /// + Binary = 2, + + /// + /// DSC resource is a composite and implemented using configuration keyword + /// + Composite = 3 + } + + /// + /// Contains a DSC resource information + /// + public sealed class DscResourceInfo + { + /// + /// Initializes a new instance of the DscResourceInfo class + /// + public DscResourceInfo() + { + this.Properties = new List(); + } + + /// + /// Gets or sets resource type name + /// + public string ResourceType { get; set; } + + /// + /// Gets or sets Name of the resource. This name is used to access the resource + /// + public string Name { get; set; } + + /// + /// Gets or sets friendly name defined for the resource + /// + public string FriendlyName { get; set; } + + /// + /// Gets or sets module which implements the resource. This could point to parent module, if the DSC resource is implemented + /// by one of nested modules. + /// + public PSModuleInfo Module { get; set; } + + /// + /// Gets name of the module which implements the resource. + /// + public string ModuleName + { + get + { + if (this.Module == null) return null; + return this.Module.Name; + } + } + + /// + /// Gets version of the module which implements the resource. + /// + public Version Version + { + get + { + if (this.Module == null) return null; + return this.Module.Version; + } + } + + /// + /// Gets or sets of the file which implements the resource. For the reosurces which are defined using + /// MOF file, this will be path to a module which resides in the same folder where schema.mof file is present. + /// For composite resources, this will be the module which implements the resource + /// + public string Path { get; set; } + + /// + /// Gets or sets parent folder, where the resource is defined + /// It is the folder containing either the implementing module(=Path) or folder containing ".schema.mof". + /// For native providers, Path will be null and only ParentPath will be present. + /// + public string ParentPath { get; set; } + + /// + /// Gets or sets a value which indicate how DSC resource is implemented + /// + public ImplementedAsType ImplementedAs { get; set; } + + /// + /// Gets or sets company which owns this resource + /// + public string CompanyName { get; set; } + + /// + /// Gets or sets properties of the resource + /// + public List Properties { get; private set; } + + /// + /// Updates properties of the resource + /// + /// Updated properties + public void UpdateProperties(List properties) + { + this.Properties = properties; + } + } + + /// + /// Contains a DSC resource property information + /// + public sealed class DscResourcePropertyInfo + { + /// + /// Initializes a new instance of the DscResourcePropertyInfo class + /// + public DscResourcePropertyInfo() + { + this.Values = new List(); + } + + /// + /// Gets or sets name of the property + /// + public string Name { get; set; } + + /// + /// Gets or sets type of the property + /// + public string PropertyType { get; set; } + + /// + /// Gets or sets a value indicating whether the property is mandatory or not + /// + public bool IsMandatory { get; set; } + + /// + /// Gets Values for a resource property + /// + public List Values { get; private set; } + } +} +"@ + +if(-not ([System.Management.Automation.PSTypeName]'Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo').Type) { + Add-Type -TypeDefinition $AddDscResourceInfoTypeScript +} diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psd1 b/powershell-adapter/psDscAdapter/psDscAdapter.psd1 new file mode 100644 index 00000000..b84b2ebc --- /dev/null +++ b/powershell-adapter/psDscAdapter/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 = '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/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 new file mode 100644 index 00000000..b5e38eaf --- /dev/null +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -0,0 +1,390 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# 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 +# PSDesiredStateConfiguration 2.0.7 module will be saved in the DSC build +# in Windows PowerShell, we should always use version 1.1 that ships in Windows +if ($PSVersionTable.PSVersion.Major -gt 5) { + $parentFolder = (Get-Item (Resolve-Path $PSScriptRoot).Path).Parent + $PSDesiredStateConfiguration = Import-Module "$parentFolder/PSDesiredStateConfiguration/2.0.7/PSDesiredStateConfiguration.psd1" -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)) { + $trace = @{'Debug' = 'ERROR: Could not import PSDesiredStateConfiguration 1.1 in Windows PowerShell. ' + $importModuleError } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + } +} + +<# 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 + ) + + # create a list object to store cache of Get-DscResource + [dscResourceCache[]]$dscResourceCache = [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) { + $trace = @{'Debug' = 'WARNING: implementation detail not found: ' + $dscResource.ImplementationDetail } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + 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() + } + } + + $dscResourceCache += [dscResourceCache]@{ + Type = "$moduleName/$($dscResource.Name)" + DscResourceInfo = $DscResourceInfo + } + } + return $dscResourceCache +} + +# Convert the INPUT to a dscResourceObject object so configuration and resource are standardized as moch 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') { + $trace = @{'Debug' = '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"}}' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + } + + # 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 Get-ActualState { + param( + [Parameter(Mandatory, ValueFromPipeline = $true)] + [dscResourceObject]$DesiredState, + [Parameter(Mandatory)] + [dscResourceCache[]]$dscResourceCache + ) + + $osVersion = [System.Environment]::OSVersion.VersionString + $trace = @{'Debug' = 'OS version: ' + $osVersion } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + + $psVersion = $PSVersionTable.PSVersion.ToString() + $trace = @{'Debug' = 'PowerShell version: ' + $psVersion } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + + $moduleVersion = Get-Module PSDesiredStateConfiguration | ForEach-Object Version + $trace = @{'Debug' = 'PSDesiredStateConfiguration module version: ' + $moduleVersion } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + + # 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) } + } + + $trace = @{'Debug' = 'DSC resource implementation: ' + [dscResourceType]$cachedDscResourceInfo.ImplementationDetail } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + + # 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) { + $trace = @{'Debug' = 'ERROR: Script based resources are only supported on Windows.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + 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 + # 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 { + $getResult = Invoke-DscResource -Method Get -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $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 { + $trace = @{'Debug' = 'ERROR: ' + $_.Exception.Message } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } + } + 'ClassBased' { + try { + # load powershell class from external module + $resource = GetTypeInstanceFromModule -modulename $cachedDscResourceInfo.ModuleName -classname $cachedDscResourceInfo.Name + $dscResourceInstance = $resource::New() + + # 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 + } + $getResult = $dscResourceInstance.Get() + + # set the properties of the OUTPUT object from the result of Get-TargetResource + $addToActualState.properties = $getResult + } + catch { + + $trace = @{'Debug' = 'ERROR: ' + $_.Exception.Message } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } + } + 'Binary' { + if ($PSVersionTable.PSVersion.Major -gt 5) { + $trace = @{'Debug' = 'To use a binary resource such as File, Log, or SignatureValidation, use the Microsoft.Windows/WindowsPowerShell adapter.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } + + if (-not (($cachedDscResourceInfo.ImplementedAs -eq 'Binary') -and ('File', 'Log', 'SignatureValidation' -contains $cachedDscResourceInfo.Name))) { + $trace = @{'Debug' = 'Only File, Log, and SignatureValidation are supported as Binary resources.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + 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 { + $trace = @{'Debug' = 'ERROR: ' + $_.Exception.Message } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } + } + Default { + $trace = @{'Debug' = 'Can not find implementation of type: ' + $cachedDscResourceInfo.ImplementationDetail } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + 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.' + $trace = @{'Debug' = 'ERROR: ' + $errmsg } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + 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 dscResourceCache { + [string] $Type + [psobject] $DscResourceInfo +} + +# 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 +} diff --git a/powershell-adapter/windowspowershell.resource.json_todo b/powershell-adapter/windowspowershell.dsc.resource.json similarity index 64% rename from powershell-adapter/windowspowershell.resource.json_todo rename to powershell-adapter/windowspowershell.dsc.resource.json index 78e93e23..e9d52193 100644 --- a/powershell-adapter/windowspowershell.resource.json_todo +++ b/powershell-adapter/windowspowershell.dsc.resource.json @@ -1,8 +1,12 @@ { - "manifestVersion": "1.0", + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", "type": "Microsoft.Windows/WindowsPowerShell", "version": "0.1.0", + "kind": "Adapter", "description": "Resource adapter to classic DSC Powershell resources in Windows PowerShell.", + "tags": [ + "PowerShell" + ], "adapter": { "list": { "executable": "powershell", @@ -11,7 +15,7 @@ "-NonInteractive", "-NoProfile", "-Command", - "./powershell.resource.ps1 -WinPS List" + "./powershell.resource.ps1 List" ] }, "config": "full" @@ -23,7 +27,7 @@ "-NonInteractive", "-NoProfile", "-Command", - "$Input | ./powershell.resource.ps1 -WinPS Get" + "$Input | ./powershell.resource.ps1 Get" ] }, "set": { @@ -33,7 +37,7 @@ "-NonInteractive", "-NoProfile", "-Command", - "$Input | ./powershell.resource.ps1 -WinPS Set" + "$Input | ./powershell.resource.ps1 Set" ], "input": "stdin", "preTest": true, @@ -46,11 +50,21 @@ "-NonInteractive", "-NoProfile", "-Command", - "$Input | ./powershell.resource.ps1 -WinPS Test" + "$Input | ./powershell.resource.ps1 Test" ], "input": "stdin", "return": "state" }, + "validate": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./powershell.resource.ps1 Validate" + ] + }, "exitCodes": { "0": "Success", "1": "Error" diff --git a/resources/brew/brew.dsc.resource.sh b/resources/brew/brew.dsc.resource.sh old mode 100755 new mode 100644