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..69cd23d9a --- /dev/null +++ b/adapters/powershell/PowerShell_adapter.dsc.resource.json @@ -0,0 +1,117 @@ +{ + "$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, + "return": "state" + }, + "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..c3f9ddc7b 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -247,40 +247,52 @@ 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..519e26971 100644 --- a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 +++ b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 @@ -224,11 +224,62 @@ resources: } It 'Export works with class-based PS DSC resources' { - $out = dsc resource export -r PSClassResource/PSClassResource 2> "$testdrive/error.log" | ConvertFrom-Json + $out = dsc -l trace resource export -r PSClassResource/PSClassResource 2> "$testdrive/error.log" | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) $out | Should -Not -BeNullOrEmpty $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: PSClassResource/PSClassResource + metadata: + ${metadata}: + requireAdapter: $adapter + properties: + Name: TestInstance + Credential: + UserName: 'MyUser' + Password: 'MyPassword' +"@ + $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 'TestInstance' -Because ("$text`n" + (Get-Content -Raw -Path $TestDrive/tracing.txt)) + } + 'set' { + $out.results[0].result.beforeState.Name | Should -BeExactly 'TestInstance' -Because $text + if ($adapter -eq 'Microsoft.Adapter/WindowsPowerShell') { + $out.results[0].result.afterState.Name | Should -BeExactly 'TestInstance' -Because $text + } + } + 'test' { + $out.results[0].result.inDesiredState | Should -BeTrue -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..cb5c2f840 --- /dev/null +++ b/adapters/powershell/WindowsPowerShell_adapter.dsc.resource.json @@ -0,0 +1,117 @@ +{ + "$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, + "return": "state" + }, + "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..6a98c27c7 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,23 @@ 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' + } + } + + $properties = @() + foreach ($prop in $DscResourceInfo.Properties) { + $properties += $prop.Name } # OUTPUT dsc is expecting the following properties @@ -142,13 +157,58 @@ switch ($Operation) { directory = $DscResourceInfo.ParentPath implementedAs = $DscResourceInfo.ImplementationDetail author = $DscResourceInfo.CompanyName - properties = $DscResourceInfo.Properties.Name + properties = $properties requireAdapter = $requireAdapter description = $description } | ConvertTo-Json -Compress } } { @('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.Properties.InDesiredState -eq $false) { + $inDesiredState = $false + } + + if ($Operation -in @('Set', 'Test')) { + $actualState = $psDscAdapter.Invoke( { param($ds, $dscResourceCache) Invoke-DscOperation -Operation 'Get' -DesiredState $ds -dscResourceCache $dscResourceCache }, $desiredState, $dscResourceCache) + } + + if ($Operation -eq 'Test') { + $actualState.Properties | Add-Member -MemberType NoteProperty -Name _inDesiredState -Value $inDesiredState -Force + } + + if ($Operation -eq 'Export') { + foreach ($instance in $actualState) { + $instance | ConvertTo-Json -Depth 10 -Compress + } + exit 0 + } + + $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/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs index 9d46d6eab..eb4482230 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -4,6 +4,7 @@ use crate::mcp::mcp_server::McpServer; use dsc_lib::{ configure::config_doc::ExecutionKind, + discovery::discovery_trait::DiscoveryFilter, dscresources::{ dscresource::Invoke, invoke_result::{ @@ -71,7 +72,7 @@ impl McpServer { pub async fn invoke_dsc_resource(&self, Parameters(InvokeDscResourceRequest { operation, resource_type, properties_json }): Parameters) -> Result, McpError> { let result = task::spawn_blocking(move || { let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&resource_type, None) else { + let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, None, None)).unwrap_or(None) else { return Err(McpError::invalid_request(t!("mcp.invoke_dsc_resource.resourceNotFound", resource = resource_type), None)); }; match operation { diff --git a/dsc/src/mcp/list_dsc_resources.rs b/dsc/src/mcp/list_dsc_resources.rs index 2f347fb87..d0894b62a 100644 --- a/dsc/src/mcp/list_dsc_resources.rs +++ b/dsc/src/mcp/list_dsc_resources.rs @@ -5,7 +5,7 @@ use crate::mcp::mcp_server::McpServer; use dsc_lib::{ DscManager, discovery::{ command_discovery::ImportedManifest::Resource, - discovery_trait::DiscoveryKind, + discovery_trait::{DiscoveryFilter, DiscoveryKind}, }, dscresources::resource_manifest::Kind, progress::ProgressFormat }; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; @@ -52,7 +52,7 @@ impl McpServer { let mut dsc = DscManager::new(); let adapter_filter = match adapter { Some(adapter) => { - if let Some(resource) = dsc.find_resource(&adapter, None) { + if let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&adapter, None, None)).unwrap_or(None) { if resource.kind != Kind::Adapter { return Err(McpError::invalid_params(t!("mcp.list_dsc_resources.resourceNotAdapter", adapter = adapter), None)); } diff --git a/dsc/src/mcp/show_dsc_resource.rs b/dsc/src/mcp/show_dsc_resource.rs index 660fbf312..5d9d77954 100644 --- a/dsc/src/mcp/show_dsc_resource.rs +++ b/dsc/src/mcp/show_dsc_resource.rs @@ -4,6 +4,7 @@ use crate::mcp::mcp_server::McpServer; use dsc_lib::{ DscManager, + discovery::discovery_trait::DiscoveryFilter, dscresources::{ dscresource::{Capability, Invoke}, resource_manifest::Kind @@ -58,7 +59,7 @@ impl McpServer { pub async fn show_dsc_resource(&self, Parameters(ShowResourceRequest { r#type }): Parameters) -> Result, McpError> { let result = task::spawn_blocking(move || { let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&r#type, None) else { + let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&r#type, None, None)).unwrap_or(None) else { return Err(McpError::invalid_params(t!("mcp.show_dsc_resource.resourceNotFound", type_name = r#type), None)) }; let schema = match resource.schema() { diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index d61b09422..f9243b67b 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -5,6 +5,7 @@ use crate::args::{GetOutputFormat, OutputFormat}; use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, EXIT_DSC_RESOURCE_NOT_FOUND, write_object}; use dsc_lib::configure::config_doc::{Configuration, ExecutionKind}; use dsc_lib::configure::add_resource_export_results_to_configuration; +use dsc_lib::discovery::discovery_trait::DiscoveryFilter; use dsc_lib::dscresources::{resource_manifest::Kind, invoke_result::{GetResult, ResourceGetResponse, ResourceSetResponse, SetResult}}; use dsc_lib::dscresources::dscresource::{Capability, get_diff}; use dsc_lib::dscerror::DscError; @@ -335,5 +336,5 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, #[must_use] pub fn get_resource<'a>(dsc: &'a mut DscManager, resource: &str, version: Option<&str>) -> Option<&'a DscResource> { //TODO: add dynamically generated resource to dsc - dsc.find_resource(resource, version) + dsc.find_resource(&DiscoveryFilter::new(resource, version, None)).unwrap_or(None) } diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 3132dfedd..6ad1d9c70 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -496,9 +496,9 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat) let Some(type_name) = resource_block["type"].as_str() else { return Err(DscError::Validation(t!("subcommand.resourceTypeNotSpecified").to_string())); }; - resource_types.push(DiscoveryFilter::new(type_name, resource_block["api_version"].as_str().map(std::string::ToString::to_string))); + resource_types.push(DiscoveryFilter::new(type_name, resource_block["api_version"].as_str(), None)); } - dsc.find_resources(&resource_types, progress_format); + dsc.find_resources(&resource_types, progress_format)?; for resource_block in resources { let Some(type_name) = resource_block["type"].as_str() else { @@ -549,16 +549,25 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat list_resources(&mut dsc, resource_name.as_ref(), adapter_name.as_ref(), description.as_ref(), tags.as_ref(), output_format.as_ref(), progress_format); }, ResourceSubCommand::Schema { resource , version, output_format } => { - dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + if let Err(err) = dsc.find_resources(&[DiscoveryFilter::new(resource, version.as_deref(), None)], progress_format) { + error!("{}: {err}", t!("subcommand.failedDiscoverResource")); + exit(EXIT_DSC_ERROR); + } resource_command::schema(&mut dsc, resource, version.as_deref(), output_format.as_ref()); }, ResourceSubCommand::Export { resource, version, input, file, output_format } => { - dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + if let Err(err) = dsc.find_resources(&[DiscoveryFilter::new(resource, version.as_deref(), None)], progress_format) { + error!("{}: {err}", t!("subcommand.failedDiscoverResource")); + exit(EXIT_DSC_ERROR); + } let parsed_input = get_input(input.as_ref(), file.as_ref()); resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => { - dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + if let Err(err) = dsc.find_resources(&[DiscoveryFilter::new(resource, version.as_deref(), None)], progress_format) { + error!("{}: {err}", t!("subcommand.failedDiscoverResource")); + exit(EXIT_DSC_ERROR); + } if *all { resource_command::get_all(&mut dsc, resource, version.as_deref(), output_format.as_ref()); } @@ -572,17 +581,26 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat } }, ResourceSubCommand::Set { resource, version, input, file: path, output_format } => { - dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + if let Err(err) = dsc.find_resources(&[DiscoveryFilter::new(resource, version.as_deref(), None)], progress_format) { + error!("{}: {err}", t!("subcommand.failedDiscoverResource")); + exit(EXIT_DSC_ERROR); + } let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Test { resource, version, input, file: path, output_format } => { - dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + if let Err(err) = dsc.find_resources(&[DiscoveryFilter::new(resource, version.as_deref(), None)], progress_format) { + error!("{}: {err}", t!("subcommand.failedDiscoverResource")); + exit(EXIT_DSC_ERROR); + } let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Delete { resource, version, input, file: path } => { - dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + if let Err(err) = dsc.find_resources(&[DiscoveryFilter::new(resource, version.as_deref(), None)], progress_format) { + error!("{}: {err}", t!("subcommand.failedDiscoverResource")); + exit(EXIT_DSC_ERROR); + } let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input); }, diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index c54d423e4..4daf757b1 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -3,6 +3,16 @@ Describe 'Tests for adapter support' { Context 'Adapter support single resource' { + BeforeAll { + $OldPSModulePath = $env:PSModulePath + $env:PSModulePath += [System.IO.Path]::PathSeparator + (Resolve-Path "$PSScriptRoot/../../adapters/powershell/Tests") + } + + AfterAll { + $env:PSModulePath = $OldPSModulePath + } + + It 'Direct resource invocation for: ' -TestCases @( @{ operation = 'get' }, @{ operation = 'set' }, @@ -87,5 +97,57 @@ 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: Test/Invalid + 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 not found: InvalidAdapter/Invalid" -Because $errorContent + $out | Should -BeNullOrEmpty -Because $errorContent + } + + It 'Specifying two adapters for same resource works' { + $config_yaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test + type: TestClassResource/TestClassResource + properties: + Name: 'Hello' + metadata: + Microsoft.DSC: + requireAdapter: Microsoft.DSC/PowerShell + - name: Test2 + type: TestClassResource/TestClassResource + properties: + Name: 'Bye' + metadata: + Microsoft.DSC: + requireAdapter: Microsoft.Adapter/PowerShell +'@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + $out.results.Count | Should -Be 2 + $out.results[0].type | Should -BeExactly 'TestClassResource/TestClassResource' + $out.results[0].Name | Should -Be 'Test' + $out.results[0].result.actualState.Name | Should -BeExactly 'Hello' + $out.results[1].type | Should -BeExactly 'TestClassResource/TestClassResource' + $out.results[1].Name | Should -Be 'Test2' + $out.results[1].result.actualState.Name | Should -BeExactly 'Bye' + "$TestDrive/error.log" | Should -FileContentMatch "Invoking get for 'Microsoft.DSC/PowerShell'" -Because (Get-Content $TestDrive/error.log | Out-String) + "$TestDrive/error.log" | Should -FileContentMatch "Invoking get for 'Microsoft.Adapter/PowerShell'" -Because (Get-Content $TestDrive/error.log | Out-String) + } } } 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..dce048834 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -109,8 +109,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..79b8c6afb 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) -> Option { + if let Some(resource_metadata) = resource_metadata { + if let Some(microsoft_metadata) = &resource_metadata.microsoft { + if let Some(require_adapter) = µsoft_metadata.require_adapter { + return Some(require_adapter.clone()); + } + } + } + None +} + fn check_security_context(metadata: Option<&Metadata>) -> Result<(), DscError> { if metadata.is_none() { return Ok(()); @@ -363,11 +374,12 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { 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 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, @@ -447,7 +459,8 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { 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)?; @@ -467,7 +480,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; @@ -615,12 +628,13 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { 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)?; 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) { @@ -698,13 +712,15 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { 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())?; + 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 +973,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(), @@ -980,7 +997,8 @@ impl Configurator { let mut discovery_filter: Vec = Vec::new(); let config_copy = config.clone(); for resource in config_copy.resources { - let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.clone()); + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()); if !discovery_filter.contains(&filter) { discovery_filter.push(filter); } @@ -996,7 +1014,7 @@ impl Configurator { } } - self.discovery.find_resources(&discovery_filter, self.progress_format); + self.discovery.find_resources(&discovery_filter, self.progress_format)?; self.config = config; Ok(()) } diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 8479b8cb6..58390c089 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, parser::Statement}; +use crate::{discovery::{discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, matches_adapter_requirement}, parser::Statement}; use crate::{locked_is_empty, locked_extend, locked_clone, locked_get}; use crate::configure::context::Context; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; @@ -391,7 +391,6 @@ impl ResourceDiscovery for CommandDiscovery { } found_adapter = true; - info!("Enumerating resources for adapter '{}'", adapter_name); let mut adapter_progress = ProgressBar::new(1, self.progress_format)?; adapter_progress.write_activity(format!("Enumerating resources for adapter '{adapter_name}'").as_str()); let manifest = if let Some(manifest) = &adapter.manifest { @@ -514,8 +513,22 @@ impl ResourceDiscovery for CommandDiscovery { return Ok(found_resources); } - // now go through the adapters, this is for implicit adapters so version can't be specified so use latest version - for adapter_name in locked_clone!(ADAPTERS).keys() { + // store the keys of the ADAPTERS into a vec + let mut adapters: Vec = locked_clone!(ADAPTERS).keys().cloned().collect(); + // sort the adapters by ones specified in the required resources first + + for filter in required_resource_types { + if let Some(required_adapter) = filter.require_adapter() { + if !adapters.contains(&required_adapter.to_string()) { + return Err(DscError::AdapterNotFound(required_adapter.to_string())); + } + // otherwise insert at the front of the list + adapters.retain(|a| a != required_adapter); + adapters.insert(0, required_adapter.to_string()); + } + } + + for adapter_name in &adapters { self.discover_adapted_resources("*", adapter_name)?; add_resources_to_lookup_table(&locked_clone!(ADAPTED_RESOURCES)); for filter in required_resource_types { @@ -547,7 +560,7 @@ fn filter_resources(found_resources: &mut BTreeMap>, re if let Some(required_version) = filter.version() { if let Ok(resource_version) = Version::parse(&resource.version) { if let Ok(version_req) = VersionReq::parse(required_version) { - if version_req.matches(&resource_version) { + if version_req.matches(&resource_version) && matches_adapter_requirement(resource, filter) { found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); required_resources.insert(filter.clone(), true); debug!("{}", t!("discovery.commandDiscovery.foundResourceWithVersion", resource = resource.type_name, version = resource.version)); @@ -556,7 +569,7 @@ fn filter_resources(found_resources: &mut BTreeMap>, re } } else { // if not semver, we do a string comparison - if resource.version == *required_version { + if resource.version == *required_version && matches_adapter_requirement(resource, filter) { found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); required_resources.insert(filter.clone(), true); debug!("{}", t!("discovery.commandDiscovery.foundResourceWithVersion", resource = resource.type_name, version = resource.version)); @@ -564,10 +577,9 @@ fn filter_resources(found_resources: &mut BTreeMap>, re } } } else { - // if no version specified, get first one which will be latest - if let Some(resource) = resources.first() { - required_resources.insert(filter.clone(), true); + if matches_adapter_requirement(resource, filter) { found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); + required_resources.insert(filter.clone(), true); break; } } @@ -853,7 +865,6 @@ fn add_resources_to_lookup_table(adapted_resources: &BTreeMap, r#type: String, version: Option, } impl DiscoveryFilter { #[must_use] - pub fn new(resource_type: &str, version: Option) -> Self { + pub fn new(resource_type: &str, version: Option<&str>, adapter: Option<&str>) -> Self { let version = version.map(|v| fix_semver(&v)); Self { + require_adapter: adapter.map(|a| a.to_lowercase()), r#type: resource_type.to_lowercase(), version, } } + #[must_use] + pub fn require_adapter(&self) -> Option<&String> { + self.require_adapter.as_ref() + } + #[must_use] pub fn resource_type(&self) -> &str { &self.r#type @@ -61,6 +68,7 @@ pub trait ResourceDiscovery { /// /// * `name_filter` - The filter for the resource name. /// * `adapter_filter` - The filter for the adapter name. + /// * `fail_on_unknown_adapter` - Whether to fail if the adapter is unknown. /// /// # Returns /// diff --git a/lib/dsc-lib/src/discovery/mod.rs b/lib/dsc-lib/src/discovery/mod.rs index f96f94d09..7e5069cd1 100644 --- a/lib/dsc-lib/src/discovery/mod.rs +++ b/lib/dsc-lib/src/discovery/mod.rs @@ -5,6 +5,7 @@ pub mod command_discovery; pub mod discovery_trait; use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery, DiscoveryFilter}; +use crate::dscerror::DscError; use crate::extensions::dscextension::{Capability, DscExtension}; use crate::{dscresources::dscresource::DscResource, progress::ProgressFormat}; use core::result::Result::Ok; @@ -87,38 +88,42 @@ impl Discovery { } #[must_use] - pub fn find_resource(&mut self, type_name: &str, version_string: Option<&str>) -> Option<&DscResource> { + pub fn find_resource(&mut self, filter: &DiscoveryFilter) -> Result, DscError> { if self.resources.is_empty() { - let discovery_filter = DiscoveryFilter::new(type_name, version_string.map(std::string::ToString::to_string)); - self.find_resources(&[discovery_filter], ProgressFormat::None); + self.find_resources(&[filter.clone()], ProgressFormat::None)?; } - let type_name = type_name.to_lowercase(); + let type_name = filter.resource_type().to_lowercase(); if let Some(resources) = self.resources.get(&type_name) { - if let Some(version) = version_string { + if let Some(version) = filter.version() { let version = fix_semver(version); if let Ok(version_req) = VersionReq::parse(&version) { for resource in resources { if let Ok(resource_version) = Version::parse(&resource.version) { - if version_req.matches(&resource_version) { - return Some(resource); + if version_req.matches(&resource_version) && matches_adapter_requirement(resource, filter) { + return Ok(Some(resource)); } } } - None + Ok(None) } else { for resource in resources { - if resource.version == version { - return Some(resource); + if resource.version == version && matches_adapter_requirement(resource, filter) { + return Ok(Some(resource)); } } - None + Ok(None) } } else { - resources.first() + for resource in resources { + if matches_adapter_requirement(resource, filter) { + return Ok(Some(resource)); + } + } + Ok(None) } } else { - None + Ok(None) } } @@ -127,10 +132,10 @@ impl Discovery { /// # Arguments /// /// * `required_resource_types` - The required resource types. - pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) { + pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) -> Result<(), DscError> { if !self.resources.is_empty() { // If resources are already discovered, no need to re-discover. - return; + return Ok(()); } let command_discovery = CommandDiscovery::new(progress_format); @@ -139,14 +144,7 @@ impl Discovery { ]; for mut discovery_type in discovery_types { - let discovered_resources = match discovery_type.find_resources(required_resource_types) { - Ok(value) => value, - Err(err) => { - error!("{err}"); - continue; - } - }; - + let discovered_resources = discovery_type.find_resources(required_resource_types)?; for (resource_name, resources) in discovered_resources { self.resources.entry(resource_name).or_default().extend(resources); } @@ -155,6 +153,27 @@ impl Discovery { self.extensions.extend(extensions); } } + Ok(()) + } +} + +/// Check if a resource matches the adapter requirement specified in the filter. +/// +/// # Arguments +/// * `resource` - The resource to check. +/// * `filter` - The discovery filter containing the adapter requirement. +/// +/// # Returns +/// `true` if the resource matches the adapter requirement, `false` otherwise. +pub fn matches_adapter_requirement(resource: &DscResource, filter: &DiscoveryFilter) -> bool { + if let Some(required_adapter) = filter.require_adapter() { + if let Some(resource_adapter) = &resource.require_adapter { + required_adapter.to_lowercase() == resource_adapter.to_lowercase() + } else { + false + } + } else { + true } } diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index afb2612a8..1f7bb3c76 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}}; +use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use crate::schemas::transforms::idiomaticize_string_enum; use dscerror::DscError; @@ -144,6 +145,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); } @@ -198,9 +200,11 @@ impl DscResource { } fn invoke_test_with_adapter(&self, adapter: &str, resource_name: &str, expected: &str) -> Result { + info!("Invoking test on resource '{}' using adapter '{}'", self.type_name, adapter); let mut configurator = self.clone().create_config_for_adapter(adapter, expected)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + info!("Using single input kind for adapter '{}'", adapter.type_name); adapter.target_resource = Some(resource_name.to_string()); return adapter.test(expected); } @@ -277,7 +281,7 @@ impl DscResource { } fn get_adapter_resource(configurator: &mut Configurator, adapter: &str) -> Result { - if let Some(adapter_resource) = configurator.discovery().find_resource(adapter, None) { + if let Some(adapter_resource) = configurator.discovery().find_resource(&DiscoveryFilter::new(adapter, None, None))? { return Ok(adapter_resource.clone()); } Err(DscError::Operation(t!("dscresources.dscresource.adapterResourceNotFound", adapter = adapter).to_string())) diff --git a/lib/dsc-lib/src/lib.rs b/lib/dsc-lib/src/lib.rs index 32b0cd320..d3ceadd6d 100644 --- a/lib/dsc-lib/src/lib.rs +++ b/lib/dsc-lib/src/lib.rs @@ -50,16 +50,16 @@ impl DscManager { /// * `name` - The name of the resource to find, can have wildcards. /// #[must_use] - pub fn find_resource(&mut self, name: &str, version: Option<&str>) -> Option<&DscResource> { - self.discovery.find_resource(name, version) + pub fn find_resource(&mut self, filter: &DiscoveryFilter) -> Result, DscError> { + self.discovery.find_resource(filter) } pub fn list_available(&mut self, kind: &DiscoveryKind, type_name_filter: &str, adapter_name_filter: &str, progress_format: ProgressFormat) -> Vec { self.discovery.list_available(kind, type_name_filter, adapter_name_filter, progress_format) } - pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) { - self.discovery.find_resources(required_resource_types, progress_format); + pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) -> Result<(), DscError> { + self.discovery.find_resources(required_resource_types, progress_format) } /// Invoke the get operation on a resource. ///