diff --git a/adapters/powershell/.project.data.json b/adapters/powershell/.project.data.json index fc4a3a495..f12fc15c6 100644 --- a/adapters/powershell/.project.data.json +++ b/adapters/powershell/.project.data.json @@ -6,12 +6,14 @@ "psDscAdapter/powershell.resource.ps1", "psDscAdapter/psDscAdapter.psd1", "psDscAdapter/psDscAdapter.psm1", - "powershell.dsc.resource.json" + "powershell.dsc.resource.json", + "PowerShell_adapter.dsc.resource.json" ], "Windows": [ "psDscAdapter/win_psDscAdapter.psd1", "psDscAdapter/win_psDscAdapter.psm1", - "windowspowershell.dsc.resource.json" + "windowspowershell.dsc.resource.json", + "WindowsPowerShell_adapter.dsc.resource.json" ] } } \ No newline at end of file diff --git a/adapters/powershell/PowerShell_adapter.dsc.resource.json b/adapters/powershell/PowerShell_adapter.dsc.resource.json new file mode 100644 index 000000000..ef063c875 --- /dev/null +++ b/adapters/powershell/PowerShell_adapter.dsc.resource.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Adapter/PowerShell", + "version": "0.1.0", + "kind": "adapter", + "description": "Resource adapter to classic DSC Powershell resources.", + "tags": [ + "PowerShell" + ], + "adapter": { + "list": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "./psDscAdapter/powershell.resource.ps1", + "List", + "-ResourceType", + "Single" + ] + }, + "config": "single" + }, + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Get", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Set", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "implementsPretest": true + }, + "test": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Test", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Export", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "validate": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error" + } +} diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index 09df8968f..2ceb42de1 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -247,40 +247,53 @@ Describe 'PowerShell adapter resource tests' { $out.results.result.actualState.result.properties.HashTableProp.Name | Should -BeExactly 'DSCv3' } - It 'Config calling PS Resource directly works for ' -TestCases @( - @{ Operation = 'get' } - @{ Operation = 'set' } - @{ Operation = 'test' } + It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'get'; metadata = 'Ignored' } + @{ Operation = 'set'; metadata = 'Ignored' } + @{ Operation = 'test'; metadata = 'Ignored' } ) { - param($Operation) + param($Operation, $metadata, $adapter) $yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Class-resource Info type: TestClassResource/TestClassResource + metadata: + ${metadata}: + requireAdapter: $adapter properties: Name: 'TestClassResource1' HashTableProp: Name: 'DSCv3' + Prop1: foo "@ - $out = dsc -l trace config $operation -i $yaml 2> $TestDrive/tracing.txt $text = $out | Out-String $out = $out | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) switch ($Operation) { 'get' { - $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because $text + $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because ("$text`n" + (Get-Content -Raw -Path $TestDrive/tracing.txt)) } 'set' { $out.results[0].result.beforeState.Name | Should -BeExactly 'TestClassResource1' -Because $text $out.results[0].result.afterState.Name | Should -BeExactly 'TestClassResource1' -Because $text } 'test' { - $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text + $out.results[0].result.inDesiredState | Should -BeFalse -Because $text } } + if ($metadata -eq 'Microsoft.DSC') { + "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + + } } It 'Config works with credential object' { diff --git a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 index 9efd22a72..c5170ad7f 100644 --- a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 @@ -75,13 +75,7 @@ Describe 'PowerShell adapter resource tests' { $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.InDesiredState | Should -Be $True - $res.actualState.InDesiredState.GetType().Name | Should -Be "Boolean" - - # verify that only properties with DscProperty attribute are returned - $propertiesNames = $res.actualState.InDesiredState | Get-Member -MemberType NoteProperty | % Name - $propertiesNames | Should -Not -Contain 'NonDscProperty' - $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' + $res.InDesiredState | Should -Be $True -Because $r } It 'Set works on class-based resource' { diff --git a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 index f5c505478..bb38787b8 100644 --- a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 +++ b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 @@ -230,5 +230,53 @@ resources: $out.resources.count | Should -Be 5 $out.resources[0].properties.Ensure | Should -Be 'Present' # Check for enum property } + + It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'get'; metadata = 'Ignored' } + @{ Operation = 'set'; metadata = 'Ignored' } + @{ Operation = 'test'; metadata = 'Ignored' } + ) { + param($Operation, $metadata, $adapter) + + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Class-resource Info + type: TestClassResource/TestClassResource + metadata: + ${metadata}: + requireAdapter: $adapter + properties: + Name: 'TestClassResource1' + HashTableProp: + Name: 'DSCv3' +"@ + $out = dsc -l trace config $operation -i $yaml 2> $TestDrive/tracing.txt + $text = $out | Out-String + $out = $out | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + switch ($Operation) { + 'get' { + $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because ("$text`n" + (Get-Content -Raw -Path $TestDrive/tracing.txt)) + } + 'set' { + $out.results[0].result.beforeState.Name | Should -BeExactly 'TestClassResource1' -Because $text + $out.results[0].result.afterState.Name | Should -BeExactly 'TestClassResource1' -Because $text + } + 'test' { + $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text + } + } + if ($metadata -eq 'Microsoft.DSC') { + "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + + } + } } diff --git a/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json new file mode 100644 index 000000000..f04334c2c --- /dev/null +++ b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Adapter/WindowsPowerShell", + "version": "0.1.0", + "kind": "adapter", + "description": "Resource adapter to classic DSC Powershell resources in Windows PowerShell.", + "tags": [ + "PowerShell" + ], + "adapter": { + "list": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "./psDscAdapter/powershell.resource.ps1", + "List", + "-ResourceType", + "Single" + ] + }, + "config": "single" + }, + "get": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Get", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin" + }, + "set": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Set", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "implementsPretest": true + }, + "test": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Test", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "export": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1", + "Export", + { + "resourceTypeArg": "-ResourceType" + } + ], + "input": "stdin", + "return": "state" + }, + "validate": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Validate" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error" + } +} diff --git a/adapters/powershell/psDscAdapter/powershell.resource.ps1 b/adapters/powershell/psDscAdapter/powershell.resource.ps1 index aa314da5d..333f65579 100644 --- a/adapters/powershell/psDscAdapter/powershell.resource.ps1 +++ b/adapters/powershell/psDscAdapter/powershell.resource.ps1 @@ -6,7 +6,9 @@ param( [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'ClearCache')] [string]$Operation, [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] - [string]$jsonInput = '@{}' + [string]$jsonInput = '{}', + [Parameter()] + [string]$ResourceType ) function Write-DscTrace { @@ -76,7 +78,7 @@ if ('Validate' -ne $Operation) { } if ($jsonInput) { - if ($jsonInput -ne '@{}') { + if ($jsonInput -ne '{}') { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath @@ -126,10 +128,18 @@ switch ($Operation) { # match adapter to version of powershell if ($PSVersionTable.PSVersion.Major -le 5) { - $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + if ($ResourceType) { + $requireAdapter = 'Microsoft.Adapter/WindowsPowerShell' + } else { + $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' + } } else { - $requireAdapter = 'Microsoft.DSC/PowerShell' + if ($ResourceType) { + $requireAdapter = 'Microsoft.Adapter/PowerShell' + } else { + $requireAdapter = 'Microsoft.DSC/PowerShell' + } } # OUTPUT dsc is expecting the following properties @@ -149,6 +159,40 @@ switch ($Operation) { } } { @('Get','Set','Test','Export') -contains $_ } { + if ($ResourceType) { + Write-DscTrace -Operation Debug -Message "Using resource type override: $ResourceType" + $dscResourceCache = Invoke-DscCacheRefresh -Module $ResourceType.Split('/')[0] + if ($null -eq $dscResourceCache) { + Write-DscTrace -Operation Error -Message ("DSC resource '{0}' module not found." -f $ResourceType) + exit 1 + } + + $desiredState = $psDscAdapter.invoke( { param($jsonInput, $type) Get-DscResourceObject -jsonInput $jsonInput -type $type }, $jsonInput, $ResourceType ) + if ($null -eq $desiredState) { + Write-DscTrace -Operation Error -message 'Failed to create configuration object from provided input JSON.' + exit 1 + } + + $desiredState.Type = $ResourceType + $inDesiredState = $true + $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $desiredState, $dscResourceCache) + if ($null -eq $actualState) { + Write-DscTrace -Operation Error -Message 'Incomplete GET for resource ' + $desiredState.Name + exit 1 + } + if ($actualState.InDesiredState -eq $false) { + $inDesiredState = $false + } + + if ($Operation -eq 'Test') { + $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) + } + + $result = $actualState.Properties | ConvertTo-Json -Depth 10 -Compress + Write-DscTrace -Operation Debug -Message "jsonOutput=$result" + return $result + } + $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) if ($null -eq $desiredState) { Write-DscTrace -Operation Error -message 'Failed to create configuration object from provided input JSON.' diff --git a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 index 1c22fb0b5..fc1b84f0e 100644 --- a/adapters/powershell/psDscAdapter/psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/psDscAdapter.psm1 @@ -153,7 +153,7 @@ function FindAndParseResourceDefinitions { function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName) { $methods = $ResourceType.GetMethods() | Where-Object { $_.Name -eq 'Export' } $method = $null - + if ($HasFilterProperties) { "Properties provided for filtered export" | Write-DscTrace -Operation Trace $method = foreach ($mt in $methods) { @@ -162,7 +162,7 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName break } } - + if ($null -eq $method) { "Export method with parameters not implemented by resource '$ResourceTypeName'. Filtered export is not supported." | Write-DscTrace -Operation Error exit 1 @@ -176,13 +176,13 @@ function GetExportMethod ($ResourceType, $HasFilterProperties, $ResourceTypeName break } } - + if ($null -eq $method) { "Export method not implemented by resource '$ResourceTypeName'" | Write-DscTrace -Operation Error exit 1 } } - + return $method } @@ -394,17 +394,28 @@ function Invoke-DscCacheRefresh { function Get-DscResourceObject { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - $jsonInput + $jsonInput, + [Parameter(Mandatory = $false)] + $type ) # normalize the INPUT object to an array of dscResourceObject objects $inputObj = $jsonInput | ConvertFrom-Json - $desiredState = [System.Collections.Generic.List[Object]]::new() + if ($type) { + $desiredState = [dscResourceObject]@{ + name = '' + type = $type + properties = $inputObj + } + } + else { + $desiredState = [System.Collections.Generic.List[Object]]::new() - $inputObj.resources | ForEach-Object -Process { - $desiredState += [dscResourceObject]@{ - name = $_.name - type = $_.type - properties = $_.properties + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } } } @@ -475,7 +486,7 @@ function Invoke-DscOperation { } else { if ($validateProperty -and $validateProperty.PropertyType -in @('SecureString', 'System.Security.SecureString') -and -not [string]::IsNullOrEmpty($_.Value)) { - $dscResourceInstance.$($_.Name) = ConvertTo-SecureString -AsPlainText $_.Value -Force + $dscResourceInstance.$($_.Name) = ConvertTo-SecureString -AsPlainText $_.Value -Force } else { $dscResourceInstance.$($_.Name) = $_.Value } @@ -487,7 +498,7 @@ function Invoke-DscOperation { 'Get' { $Result = @{} $raw_obj = $dscResourceInstance.Get() - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result[$_] = $raw_obj.$_.ToString() @@ -507,7 +518,7 @@ function Invoke-DscOperation { } 'Export' { $t = $dscResourceInstance.GetType() - $hasFilter = $null -ne $DesiredState.properties -and + $hasFilter = $null -ne $DesiredState.properties -and ($DesiredState.properties.PSObject.Properties | Measure-Object).Count -gt 0 $method = GetExportMethod -ResourceType $t -HasFilterProperties $hasFilter -ResourceTypeName $DesiredState.Type @@ -521,12 +532,12 @@ function Invoke-DscOperation { foreach ($raw_obj in $raw_obj_array) { $Result_obj = @{} - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result_obj[$_] = $raw_obj.$_.ToString() } - else { - $Result_obj[$_] = $raw_obj.$_ + else { + $Result_obj[$_] = $raw_obj.$_ } } $resultArray += $Result_obj diff --git a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 index c206d89d9..38665e720 100644 --- a/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 +++ b/adapters/powershell/psDscAdapter/win_psDscAdapter.psm1 @@ -280,18 +280,28 @@ function Invoke-DscCacheRefresh { function Get-DscResourceObject { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - $jsonInput + $jsonInput, + [Parameter(Mandatory = $false)] + $type ) # normalize the INPUT object to an array of dscResourceObject objects $inputObj = $jsonInput | ConvertFrom-Json - $desiredState = [System.Collections.Generic.List[Object]]::new() - - # change the type from pscustomobject to dscResourceObject - $inputObj.resources | ForEach-Object -Process { - $desiredState += [dscResourceObject]@{ - name = $_.name - type = $_.type - properties = $_.properties + if ($type) { + $desiredState = [dscResourceObject]@{ + name = '' + type = $type + properties = $inputObj + } + } + else { + $desiredState = [System.Collections.Generic.List[Object]]::new() + + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } } } @@ -436,7 +446,7 @@ function Invoke-DscOperation { 'Get' { $Result = @{} $raw_obj = $dscResourceInstance.Get() - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result[$_] = $raw_obj.$_.ToString() } else { @@ -458,11 +468,11 @@ function Invoke-DscOperation { $raw_obj_array = $method.Invoke($null, $null) foreach ($raw_obj in $raw_obj_array) { $Result_obj = @{} - $ValidProperties | ForEach-Object { + $ValidProperties | ForEach-Object { if ($raw_obj.$_ -is [System.Enum]) { $Result_obj[$_] = $raw_obj.$_.ToString() - } else { - $Result_obj[$_] = $raw_obj.$_ + } else { + $Result_obj[$_] = $raw_obj.$_ } } $resultArray += $Result_obj diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index c54d423e4..1fafc3fe5 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -87,5 +87,25 @@ Describe 'Tests for adapter support' { } } } + + + It 'Specifying invalid adapter via metadata fails' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test + type: Microsoft.DSC.Debug/Echo + properties: + output: '1' + metadata: + Microsoft.DSC: + requireAdapter: InvalidAdapter/Invalid +"@ + $out = dsc config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log | Out-String) + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match "Adapter resource 'InvalidAdapter/Invalid' not found" -Because $errorContent + $out | Should -BeNullOrEmpty -Because $errorContent + } } } diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 6f168e692..792a48a5d 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -148,13 +148,13 @@ Describe 'tests for resource discovery' { Test-Path $script:lookupTableFilePath -PathType Leaf | Should -BeFalse # initial invocation should populate and save adapter lookup table - $null = dsc -l trace resource list -a Microsoft.DSC/PowerShell 2> $TestDrive/tracing.txt + $null = dsc -l trace resource list -a 'Microsoft.*/PowerShell' 2> $TestDrive/tracing.txt "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Read 0 items into lookup table" "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Saving lookup table" -Because (Get-Content -Raw "$TestDrive/tracing.txt") # second invocation (without an update) should use but not save adapter lookup table "{'Name':'TestClassResource1'}" | dsc -l trace resource get -r 'TestClassResource/TestClassResource' -f - 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly "Saving lookup table" + "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly "Saving lookup table" -Because (Get-Content -Raw "$TestDrive/tracing.txt") # third invocation (with an update) should save updated adapter lookup table $null = dsc -l trace resource list -a Test/TestGroup 2> $TestDrive/tracing.txt diff --git a/dsc/tests/dsc_group.tests.ps1 b/dsc/tests/dsc_group.tests.ps1 index 1030b7651..e4643914c 100644 --- a/dsc/tests/dsc_group.tests.ps1 +++ b/dsc/tests/dsc_group.tests.ps1 @@ -8,13 +8,13 @@ Describe 'Group resource tests' { $out | Should -BeLike @' metadata: Microsoft.DSC: - version: 3* - operation: Get - executionType: Actual - startDatetime: * - endDatetime: * duration: PT*S + endDatetime: * + executionType: Actual + operation: Get securityContext: * + startDatetime: * + version: 3* results: - metadata: Microsoft.DSC: diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 4ecd5b072..5a6c81988 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -13,9 +13,9 @@ Describe 'metadata tests' { properties: output: hello world '@ - $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $out = dsc -l info config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*" + (Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*INFO Will not add '_metadata' to properties because resource schema does not support it*" -Because (Get-Content $TestDrive/error.log -Raw) $out.results.result.actualState.output | Should -BeExactly 'hello world' } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index d900998e7..6037287d5 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -84,6 +84,7 @@ skippingOutput = "Skipping output for '%{name}' due to condition evaluating to f secureOutputSkipped = "Secure output '%{name}' is skipped" outputTypeNotMatch = "Output '%{name}' type does not match expected type '%{expected_type}'" copyNotSupported = "Copy for output '%{name}' is currently not supported" +requireAdapter = "Resource '%{resource}' requires adapter '%{adapter}'" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" @@ -109,8 +110,8 @@ adapterFound = "Resource adapter '%{adapter}' version %{version} found" resourceFound = "Resource '%{resource}' version %{version} found" executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'" extensionInvalidVersion = "Extension '%{extension}' version '%{version}' is invalid" -invalidResourceManifest = "Invalid manifest for resource '%{resource}'" -invalidExtensionManifest = "Invalid manifest for extension '%{extension}'" +invalidResourceManifest = "Invalid manifest for resource '%{resource}': %{err}" +invalidExtensionManifest = "Invalid manifest for extension '%{extension}': %{err}" invalidManifestList = "Invalid manifest list '%{resource}': %{err}" invalidManifestFile = "Invalid manifest file '%{resource}': %{err}" extensionResourceFound = "Extension found resource '%{resource}'" diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index 367d01456..ce373e60a 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -62,30 +62,33 @@ pub enum RestartRequired { #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct MicrosoftDscMetadata { - /// Version of DSC - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - /// The operation being performed + /// The duration of the configuration operation #[serde(skip_serializing_if = "Option::is_none")] - pub operation: Option, - /// The type of execution - #[serde(rename = "executionType", skip_serializing_if = "Option::is_none")] - pub execution_type: Option, - /// The start time of the configuration operation - #[serde(rename = "startDatetime", skip_serializing_if = "Option::is_none")] - pub start_datetime: Option, + pub duration: Option, /// The end time of the configuration operation #[serde(rename = "endDatetime", skip_serializing_if = "Option::is_none")] pub end_datetime: Option, - /// The duration of the configuration operation + /// The type of execution + #[serde(rename = "executionType", skip_serializing_if = "Option::is_none")] + pub execution_type: Option, + /// The operation being performed #[serde(skip_serializing_if = "Option::is_none")] - pub duration: Option, - /// The security context of the configuration operation, can be specified to be required - #[serde(rename = "securityContext", skip_serializing_if = "Option::is_none")] - pub security_context: Option, + pub operation: Option, + /// Specify specific adapter type used for implicit operations + #[serde(rename = "requireAdapter", skip_serializing_if = "Option::is_none")] + pub require_adapter: Option, /// Indicates what needs to be restarted after the configuration operation #[serde(rename = "restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required: Option>, + /// The security context of the configuration operation, can be specified to be required + #[serde(rename = "securityContext", skip_serializing_if = "Option::is_none")] + pub security_context: Option, + /// The start time of the configuration operation + #[serde(rename = "startDatetime", skip_serializing_if = "Option::is_none")] + pub start_datetime: Option, + /// Version of DSC + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, } impl MicrosoftDscMetadata { diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 04e96a144..8aab1161d 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -206,7 +206,7 @@ fn add_metadata(dsc_resource: &DscResource, mut properties: Option, result: &mut Valu Ok(()) } +fn update_require_adapter_from_metadata(resource: &mut DscResource, resource_metadata: &Option) -> Result<(), DscError> { + if let Some(resource_metadata) = resource_metadata { + if let Some(microsoft_metadata) = &resource_metadata.microsoft { + if let Some(require_adapter) = µsoft_metadata.require_adapter { + info!("{}", t!("configure.mod.requireAdapter", resource = &resource.type_name, adapter = require_adapter)); + resource.require_adapter = Some(require_adapter.clone()); + } + } + } + Ok(()) +} + impl Configurator { /// Create a new `Configurator` instance. /// @@ -367,7 +379,9 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; - let filter = add_metadata(dsc_resource, properties, resource.metadata.clone())?; + let mut dsc_resource = dsc_resource.clone(); + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; + let filter = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { Ok(result) => result, @@ -451,6 +465,8 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; + let mut dsc_resource = dsc_resource.clone(); + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; debug!("resource_type {}", &resource.resource_type); // see if the properties contains `_exist` and is false @@ -467,7 +483,7 @@ impl Configurator { } }; - let desired = add_metadata(dsc_resource, properties, resource.metadata.clone())?; + let desired = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.desired", state = desired)); let start_datetime; @@ -619,8 +635,10 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; + let mut dsc_resource = dsc_resource.clone(); + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; debug!("resource_type {}", &resource.resource_type); - let expected = add_metadata(dsc_resource, properties, resource.metadata.clone())?; + let expected = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.expectedState", state = expected)); let start_datetime = chrono::Local::now(); let mut test_result = match dsc_resource.test(&expected) { @@ -702,9 +720,12 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type.clone(), resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(resource, &dsc_resource.kind)?; - let input = add_metadata(dsc_resource, properties, resource.metadata.clone())?; + let mut dsc_resource = dsc_resource.clone(); + update_require_adapter_from_metadata(&mut dsc_resource, &resource.metadata)?; + debug!("resource_type {}", &resource.resource_type); + let input = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.exportInput", input = input)); - let export_result = match add_resource_export_results_to_configuration(dsc_resource, &mut conf, input.as_str()) { + let export_result = match add_resource_export_results_to_configuration(&dsc_resource, &mut conf, input.as_str()) { Ok(result) => result, Err(e) => { progress.set_failure(get_failure_from_error(&e)); @@ -957,14 +978,15 @@ impl Configurator { Metadata { microsoft: Some( MicrosoftDscMetadata { - version: Some(version), - operation: Some(operation), - execution_type: Some(self.context.execution_type.clone()), - start_datetime: Some(self.context.start_datetime.to_rfc3339()), - end_datetime: Some(end_datetime.to_rfc3339()), + require_adapter: None, duration: Some(end_datetime.signed_duration_since(self.context.start_datetime).to_string()), - security_context: Some(self.context.security_context.clone()), + end_datetime: Some(end_datetime.to_rfc3339()), + execution_type: Some(self.context.execution_type.clone()), + operation: Some(operation), restart_required: self.context.restart_required.clone(), + security_context: Some(self.context.security_context.clone()), + start_datetime: Some(self.context.start_datetime.to_rfc3339()), + version: Some(version), } ), other: Map::new(), diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index afb2612a8..c992fd97b 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -144,6 +144,7 @@ impl DscResource { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + debug!("Using single input kind for adapter '{}'", adapter.type_name); adapter.target_resource = Some(resource_name.to_string()); return adapter.get(filter); }