diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index bb18008c9..d599e3c88 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -55,12 +55,24 @@ pub fn get(dsc: &DscManager, resource_type: &str, input: &Option, stdin: } pub fn get_all(dsc: &DscManager, resource_type: &str, _input: &Option, _stdin: &Option, format: &Option) { - let Some(resource) = get_resource(dsc, resource_type) else { + let mut input = String::new() ; + let Some(mut resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); return }; + debug!("resource.type_name - {} implemented_as - {:?}", resource.type_name, resource.implemented_as); - let export_result = match resource.export() { + if let Some(requires) = &resource.requires { + input = add_type_name_to_json(input, resource.type_name.clone()); + if let Some(pr) = get_resource(dsc, requires) { + resource = pr; + } else { + error!("Provider '{}' not found", requires); + return; + }; + } + + let export_result = match resource.export(&input) { Ok(export) => { export } Err(err) => { error!("Error: {err}"); @@ -203,14 +215,26 @@ pub fn schema(dsc: &DscManager, resource_type: &str, format: &Option) { + let mut input = String::new(); let Some(dsc_resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); return }; + let mut provider_resource: Option<&DscResource> = None; + if let Some(requires) = &dsc_resource.requires { + input = add_type_name_to_json(input, dsc_resource.type_name.clone()); + if let Some(pr) = get_resource(dsc, requires) { + provider_resource = Some(pr); + } else { + error!("Provider '{}' not found", requires); + return; + }; + } + let mut conf = Configuration::new(); - if let Err(err) = add_resource_export_results_to_configuration(dsc_resource, &mut conf) { + if let Err(err) = add_resource_export_results_to_configuration(dsc_resource, provider_resource, &mut conf, &input) { error!("Error: {err}"); exit(EXIT_DSC_ERROR); } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 0ec023bf6..19871df55 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -43,11 +43,19 @@ pub enum ErrorAction { /// * `resource` - The resource to export. /// * `conf` - The configuration to add the results to. /// +/// # Panics +/// +/// Doesn't panic because there is a match/Some check before unwrap(); false positive. +/// /// # Errors /// /// This function will return an error if the underlying resource fails. -pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf: &mut Configuration) -> Result<(), DscError> { - let export_result = resource.export()?; +pub fn add_resource_export_results_to_configuration(resource: &DscResource, provider_resource: Option<&DscResource>, conf: &mut Configuration, input: &str) -> Result<(), DscError> { + + let export_result = match provider_resource { + Some(_) => provider_resource.unwrap().export(input)?, + _ => resource.export(input)? + }; for (i, instance) in export_result.actual_state.iter().enumerate() { let mut r = config_doc::Resource::new(); @@ -269,7 +277,9 @@ impl Configurator { let Some(dsc_resource) = self.discovery.find_resource(&resource.resource_type.to_lowercase()) else { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; - add_resource_export_results_to_configuration(dsc_resource, &mut conf)?; + + let input = serde_json::to_string(&resource.properties)?; + add_resource_export_results_to_configuration(dsc_resource, Some(dsc_resource), &mut conf, input.as_str())?; } result.result = Some(conf); diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 2c530dd1a..766d82d93 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -360,6 +360,7 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result Result Result { +pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>) -> Result { let Some(export) = resource.export.as_ref() else { return Err(DscError::Operation(format!("Export is not supported by resource {}", &resource.resource_type))) }; - let (exit_code, stdout, stderr) = invoke_command(&export.executable, export.args.clone(), None, Some(cwd), None)?; + let (exit_code, stdout, stderr) = invoke_command(&export.executable, export.args.clone(), input, Some(cwd), None)?; if exit_code != 0 { return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); } diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 2dc3f8938..7bdab2116 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -164,11 +164,15 @@ pub trait Invoke { fn schema(&self) -> Result; /// Invoke the export operation on the resource. + /// + /// # Arguments + /// + /// * `input` - Input for export operation. /// /// # Errors /// /// This function will return an error if the underlying resource fails. - fn export(&self) -> Result; + fn export(&self, input: &str) -> Result; } impl Invoke for DscResource { @@ -266,19 +270,12 @@ impl Invoke for DscResource { } } - fn export(&self) -> Result { - match &self.implemented_as { - ImplementedAs::Custom(_custom) => { - Err(DscError::NotImplemented("export custom resources".to_string())) - }, - ImplementedAs::Command => { - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_export(&resource_manifest, &self.directory) - }, - } + fn export(&self, input: &str) -> Result { + let Some(manifest) = &self.manifest else { + return Err(DscError::MissingManifest(self.type_name.clone())); + }; + let resource_manifest = import_manifest(manifest.clone())?; + command_resource::invoke_export(&resource_manifest, &self.directory, Some(input)) } } diff --git a/powershellgroup/Tests/PSTestModule/TestClassResource.psm1 b/powershellgroup/Tests/PSTestModule/TestClassResource.psm1 index 5fb0fa94d..01092b521 100644 --- a/powershellgroup/Tests/PSTestModule/TestClassResource.psm1 +++ b/powershellgroup/Tests/PSTestModule/TestClassResource.psm1 @@ -1,3 +1,5 @@ +using namespace System.Collections.Generic + enum EnumPropEnumeration { Unexpected Expected @@ -40,6 +42,19 @@ class TestClassResource $this.EnumProp = [EnumPropEnumeration]::Expected return $this } + + static [TestClassResource[]] Export() + { + $resultList = [List[TestClassResource]]::new() + 1..5 | %{ + $obj = New-Object TestClassResource + $obj.Name = "Object$_" + $obj.Prop1 = "Property of object$_" + $resultList.Add($obj) + } + + return $resultList.ToArray() + } } function Hello-World() diff --git a/powershellgroup/Tests/powershellgroup.config.tests.ps1 b/powershellgroup/Tests/powershellgroup.config.tests.ps1 index 14907d768..e6b4d04f7 100644 --- a/powershellgroup/Tests/powershellgroup.config.tests.ps1 +++ b/powershellgroup/Tests/powershellgroup.config.tests.ps1 @@ -39,4 +39,26 @@ Describe 'PowerShellGroup resource tests' { $res.results.result.afterState[0].RebootRequired | Should -Not -BeNull $res.results.result.afterState[1].RebootRequired | Should -Not -BeNull } + + It 'Export works on config with class-based resources' -Skip:(!$IsWindows){ + + $yaml = @' + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json + resources: + - name: Working with class-based resources + type: DSC/PowerShellGroup + properties: + resources: + - name: Class-resource Info + type: PSTestModule/TestClassResource +'@ + $out = $yaml | dsc config export + $LASTEXITCODE | Should -Be 0 + $res = $out | ConvertFrom-Json + $res.'$schema' | Should -BeExactly 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json' + $res.'resources' | Should -Not -BeNullOrEmpty + $res.resources.count | Should -Be 5 + $res.resources[0].properties.Name | Should -Be "Object1" + $res.resources[0].properties.Prop1 | Should -Be "Property of object1" + } } diff --git a/powershellgroup/Tests/powershellgroup.resource.tests.ps1 b/powershellgroup/Tests/powershellgroup.resource.tests.ps1 index 408c3d060..9ce6956ac 100644 --- a/powershellgroup/Tests/powershellgroup.resource.tests.ps1 +++ b/powershellgroup/Tests/powershellgroup.resource.tests.ps1 @@ -83,4 +83,24 @@ Describe 'PowerShellGroup resource tests' { $res = $r | ConvertFrom-Json $res.afterState.RebootRequired | Should -Not -BeNull } + + It 'Export works on PS class-based resource' -Skip:(!$IsWindows){ + + $r = dsc resource export -r PSTestModule/TestClassResource + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.resources.count | Should -Be 5 + $res.resources[0].type | Should -Be "PSTestModule/TestClassResource" + $res.resources[0].properties.Name | Should -Be "Object1" + $res.resources[0].properties.Prop1 | Should -Be "Property of object1" + } + + It 'Get --all works on PS class-based resource' -Skip:(!$IsWindows){ + + $r = dsc resource get --all -r PSTestModule/TestClassResource + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.count | Should -Be 5 + $res | % {$_.actualState | Should -Not -BeNullOrEmpty} + } } diff --git a/powershellgroup/powershellgroup.dsc.resource.json b/powershellgroup/powershellgroup.dsc.resource.json index 7896ec744..4b01ebbed 100644 --- a/powershellgroup/powershellgroup.dsc.resource.json +++ b/powershellgroup/powershellgroup.dsc.resource.json @@ -55,6 +55,18 @@ "input": "stdin", "return": "state" }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./powershellgroup.resource.ps1 Export" + ], + "input": "stdin", + "return": "state" + }, "validate": { "executable": "pwsh", "args": [ diff --git a/powershellgroup/powershellgroup.resource.ps1 b/powershellgroup/powershellgroup.resource.ps1 index dcc3ba1d7..9d6dae343 100644 --- a/powershellgroup/powershellgroup.resource.ps1 +++ b/powershellgroup/powershellgroup.resource.ps1 @@ -3,7 +3,7 @@ [CmdletBinding()] param( - [ValidateSet('List','Get','Set','Test','Validate')] + [ValidateSet('List','Get','Set','Test','Export','Validate')] $Operation = 'List', [Switch] $WinPS = $false, @@ -327,6 +327,80 @@ elseif ($Operation -eq 'Test') $result | ConvertTo-Json } +elseif ($Operation -eq 'Export') +{ + $inputobj_pscustomobj = $null + if ($stdinput) + { + $inputobj_pscustomobj = $stdinput | ConvertFrom-Json + } + + $result = @() + + RefreshCache + + if ($inputobj_pscustomobj.resources) # we are processing a config batch + { + foreach($r in $inputobj_pscustomobj.resources) + { + $cachedResourceInfo = $script:ResourceCache[$r.type] + if ($cachedResourceInfo) + { + $path = $cachedResourceInfo.Path # for class-based resources - this is path to psd1 of their defining module + + $typeparts = $r.type -split "/" + $ResourceTypeName = $typeparts[1] + + $scriptBody = "using module '$path'" + $script = [ScriptBlock]::Create($scriptBody) + . $script + + $t = [Type]$ResourceTypeName + $method = $t.GetMethod('Export') + $resultArray = $method.Invoke($null,$null) + foreach ($instance in $resultArray) + { + $instance | ConvertTo-Json -Compress | Write-Output + } + } + else + { + $errmsg = "Can not find type " + $r.type + "; please ensure that Get-DscResource returns this resource type" + Write-Error $errmsg + exit 1 + } + } + } + else # we are processing an individual resource call + { + $cachedResourceInfo = $script:ResourceCache[$inputobj_pscustomobj.type] + if ($cachedResourceInfo) + { + $path = $cachedResourceInfo.Path # for class-based resources - this is path to psd1 of their defining module + + $typeparts = $inputobj_pscustomobj.type -split "/" + $ResourceTypeName = $typeparts[1] + + $scriptBody = "using module '$path'" + $script = [ScriptBlock]::Create($scriptBody) + . $script + + $t = [Type]$ResourceTypeName + $method = $t.GetMethod('Export') + $resultArray = $method.Invoke($null,$null) + foreach ($instance in $resultArray) + { + $instance | ConvertTo-Json -Compress | Write-Output + } + } + else + { + $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-DscResource returns this resource type" + Write-Error $errmsg + exit 1 + } + } +} elseif ($Operation -eq 'Validate') { # TODO: this is placeholder